From 45900138fc146ef77ffbcfdaac3ef21c32db3252 Mon Sep 17 00:00:00 2001 From: jaydee Date: Fri, 5 Dec 2025 17:09:05 +0100 Subject: [PATCH] build --- port.py | 4 +- portainer.py | 172 ++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 146 insertions(+), 30 deletions(-) diff --git a/port.py b/port.py index 3f1d327..af80f34 100644 --- a/port.py +++ b/port.py @@ -27,6 +27,7 @@ class Portainer: def __init__(self, site, timeout=10): self.base_url = None self.token = None + self.action = None self._debug = False self.timeout = timeout self.git_url = "git@gitlab.sectorq.eu:home/docker-compose.git" @@ -36,6 +37,7 @@ class Portainer: self.stack_ids = [] self.endpoint_name = None self.endpoint_id = None + # self.git_url = "https://gitlab.sectorq.eu/home/docker-compose.git" self.git_url = "git@gitlab.sectorq.eu:home/docker-compose.git" self.repo_dir = "/tmp/docker-compose" @@ -192,7 +194,7 @@ class Portainer: self.get_containers(self) return True - def get_stacks(self, endpoint_id="all", timeout=10): + def get_stacks(self, endpoint_id="all", timeout=20): '''Get a list of stacks for a specific endpoint or all endpoints.''' if endpoint_id != "all": endpoint_id = self.get_endpoint_id(endpoint_id) diff --git a/portainer.py b/portainer.py index 7fca6f2..73f55e9 100755 --- a/portainer.py +++ b/portainer.py @@ -19,7 +19,12 @@ from port import Portainer from prompt_toolkit import prompt from prompt_toolkit.completion import WordCompleter from prompt_toolkit.shortcuts import checkboxlist_dialog - +from prompt_toolkit.shortcuts import radiolist_dialog +from prompt_toolkit.application import Application +from prompt_toolkit.layout import Layout +from prompt_toolkit.widgets import RadioList +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.layout.containers import HSplit VERSION = "0.1.3" defaults = { @@ -32,6 +37,91 @@ defaults = { } cur_config = {} + +def menu(title, options): + """ + Display a menu and return selected value. + options must be list of (value, text) or text (auto-wrapped) + """ + if isinstance(options[0], str): + options = [(item, item) for item in options] + + radio = RadioList(options) + + kb = KeyBindings() + + @kb.add("enter") + def _(event): + event.app.exit(result=radio.current_value) + @kb.add("q") + def _(event): + event.app.exit(result=None) + layout = Layout(HSplit([radio])) + + app = Application( + layout=layout, + key_bindings=kb, + full_screen=True + ) + + return app.run() + + +# -------------------------- +# MULTI-LEVEL MENU +# -------------------------- + +def main_menu(): + while True: + choice = menu("Main Menu", [ + "System", + "Network", + "Services", + "Exit" + ]) + + if choice == "System": + system_menu() + + elif choice == "Network": + network_menu() + + elif choice == "Services": + services_menu() + + elif choice == "Exit": + return + + +def system_menu(): + choice = menu("System Menu", [ + "Info", + "Reboot", + "Back" + ]) + print("SYSTEM →", choice) + + +def network_menu(): + choice = menu("Network Menu", [ + "Show IP", + "Restart Networking", + "Back" + ]) + print("NETWORK →", choice) + + +def services_menu(): + choice = menu("Services", [ + "Start Service", + "Stop Service", + "Status", + "Back" + ]) + print("SERVICES →", choice) + + + def load_config(defaults=defaults): '''Load configuration from /myapps/portainer.conf if it exists, else from env vars or defaults.''' if os.path.exists("/myapps/portainer.conf"): @@ -212,7 +302,7 @@ def wl(msg): print(msg) -def prompt_missing_args(args_in, defaults_in, fields, action=None): +def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None): """ fields = [("arg_name", "Prompt text")] """ @@ -227,6 +317,7 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None): value_in = getattr(args_in, field) default = defaults_in.get(f"PORTAINER_{field}".upper()) cur_site = defaults_in.get("PORTAINER_SITE".upper()) + cur_env = defaults_in.get("PORTAINER_ENVIRONMENT_ID".upper()) if value_in is None: if default is not None: prompt_text = f"{text} (default={default}) : " @@ -241,13 +332,19 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None): commands = por.endpoints_names elif field == "stack": if args.action == "create_stack": + # input(json.dumps(stacks, indent=2)) commands = [ 'authentik', 'bitwarden', 'bookstack', 'dockermon', 'fail2ban', 'gitea', 'gitlab', 'grafana', 'home-assistant', 'homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'mailu3', - 'mealie', 'mediacenter', 'mosquitto', 'motioneye', 'n8n', 'nextcloud', 'nginx', + 'mealie', 'mediacenter', 'mosquitto', 'motioneye', 'n8n', 'nebula', 'nextcloud', 'nginx', 'node-red', 'octoprint', 'ollama', 'pihole', 'portainer-ce', 'rancher', 'registry', 'regsync', 'semaphore', 'unifibrowser', 'uptime-kuma', 'watchtower', 'wazuh', 'webhub', 'wud', 'zabbix-server'] + for s in por.all_data['stacks'][defaults_in[f"PORTAINER_ENDPOINT_ID".upper()]]['by_name'].keys(): + + # print(s) + commands.remove(s) + else: commands = [] if por._debug: @@ -280,11 +377,11 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None): ).run() if value_in is None: print("Cancelled.") + sys.exit(0) elif "__ALL__" in value_in: # User selected "Select ALL" value_in = commands # all real commands if "pihole" in value_in: - if action == "delete_stack": value_in.remove("pihole") value_in.append("pihole") @@ -333,7 +430,6 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None): if field == "site" and value_in != cur_site: por.get_site(value_in) - if field == "stack" and value_in != cur_site: os.environ[field] = ",".join(value_in) else: @@ -364,35 +460,52 @@ if __name__ == "__main__": os.system("cls" if os.name == "nt" else "clear") if args.action is None: actions = [ - "create_stack", - "delete_stack", - "stop_stack", - "start_stack", - "list_stacks", - "update_stack", - "secrets", - "print_all_data", - "list_endpoints", - "list_containers", - "stop_containers", - "start_containers", - "refresh_environment", - "refresh_status", - "update_status", - ] - print("Possible actions: \n") - i = 1 - for a in actions: - print(f" > {i:>2}. {a}") - i += 1 - ans = input("\nSelect action to perform: ") - args.action = actions[int(ans) - 1] + ("create_stack","create_stack"), + ("delete_stack","delete_stack"), + ("stop_stack","stop_stack"), + ("start_stack","start_stack"), + ("list_stacks","list_stacks"), + ("update_stack","update_stack"), + ("secrets","secrets"), + ("print_all_data","print_all_data"), + ("list_endpoints","list_endpoints"), + ("list_containers","list_containers"), + ("stop_containers","stop_containers"), + ("start_containers","start_containers"), + ("refresh_environment","refresh_environment"), + ("refresh_status","refresh_status"), + ("update_status","update_status"), + ] + + selected_action = radiolist_dialog( + title="Select one service", + text="Choose a service:", + values=actions + ).run() + + + + print("Selected:", selected_action) + + + + + # print("Possible actions: \n") + # i = 1 + # for a in actions: + # print(f" > {i:>2}. {a}") + # i += 1 + # ans = input("\nSelect action to perform: ") + args.action = selected_action + + os.system("cls" if os.name == "nt" else "clear") # Example: list endpoints por = Portainer(cur_config["PORTAINER_SITE"], timeout=args.timeout) por.set_defaults(cur_config) if args.debug: por._debug = True + if args.action == "secrets": args = prompt_missing_args( args, @@ -402,7 +515,6 @@ if __name__ == "__main__": ("endpoint_id", "Endpoint ID"), ], ) - secrets = { "gitea_runner_registration_token": "8nmKqJhkvYwltmNfF2o9vs0tzo70ufHSQpVg6ymb", "influxdb2-admin-token": "l4c1j4yd33Du5lo", @@ -437,6 +549,7 @@ if __name__ == "__main__": sys.exit() if args.action == "create_stack": + por.action = "create_stack" args = prompt_missing_args( args, cur_config, @@ -447,6 +560,7 @@ if __name__ == "__main__": ("stack_mode", "Stack mode (swarm or compose)"), ("deploy_mode", "Deploy mode (git or upload)"), ], + por, ) por.create_stack( args.endpoint_id,