diff --git a/port.py b/port.py index 62f3b1d..9d89345 100644 --- a/port.py +++ b/port.py @@ -2,15 +2,15 @@ import os import requests import json import uuid -import argparse import shutil import time import logging from concurrent.futures import ThreadPoolExecutor + + logger = logging.getLogger(__name__) -from tabulate import tabulate -from git import Repo # pip install gitpython -import base64 + + class Portainer: """ Simple wrapper around the module-level Portainer helper functions. @@ -28,22 +28,83 @@ 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 = "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" - self.basic_stacks = ["pihole","nginx", "mosquitto", "webhub", "authentik","bitwarden","mailu3","home-assistant","homepage"] - self.nas_stacks = self.basic_stacks + ["gitlab", "bookstack","dockermon","gitea","grafana","immich","jupyter","kestra","mealie"] - self.m_server_stacks = self.basic_stacks + ['immich', 'zabbix-server', 'gitea', 'unifibrowser', 'mediacenter', 'watchtower', 'wazuh', 'octoprint', 'motioneye', 'kestra', 'bookstack', 'wud', 'uptime-kuma', 'registry', 'regsync', 'dockermon', 'grafana', 'nextcloud', 'semaphore', 'node-red', 'test', 'jupyter', 'paperless', 'mealie', 'n8n', 'ollama', 'rancher'] - self.rpi5_stacks = self.basic_stacks + ["gitlab","bookstack","gitea"] - self.rack_stacks = self.basic_stacks + ["gitlab", "bookstack","dockermon","gitea","grafana","immich","jupyter","kestra","mealie"] + self.basic_stacks = [ + "pihole", + "nginx", + "mosquitto", + "webhub", + "authentik", + "bitwarden", + "mailu3", + "home-assistant", + "homepage" + ] + self.nas_stacks = self.basic_stacks + [ + "gitlab", + "bookstack", + "dockermon", + "gitea", + "grafana", + "immich", + "jupyter", + "kestra", + "mealie" + ] + self.m_server_stacks = self.basic_stacks + [ + 'immich', + 'zabbix-server', + 'gitea', + 'unifibrowser', + 'mediacenter', + 'watchtower', + 'wazuh', + 'octoprint', + 'motioneye', + 'kestra', + 'bookstack', + 'wud', + 'uptime-kuma', + 'registry', + 'regsync', + 'dockermon', + 'grafana', + 'nextcloud', + 'semaphore', + 'node-red', + 'test', + 'jupyter', + 'paperless', + 'mealie', + 'n8n', + 'ollama', + 'rancher' + ] + self.rpi5_stacks = self.basic_stacks + [ + "gitlab", + "bookstack", + "gitea" + ] + self.rack_stacks = self.basic_stacks + [ + "gitlab", + "bookstack", + "dockermon", + "gitea", + "grafana", + "immich", + "jupyter", + "kestra", + "mealie" + ] self.log_mode = False self.hw_mode = False - self.all_data = {"containers":{},"stacks":{},"endpoints":{}} + self.all_data = {"containers": {}, "stacks": {}, "endpoints": {}} self.get_endpoints() self.get_stacks() self.get_containers() - - + def is_number(self, s): """Check if the input string is a number.""" try: @@ -62,11 +123,11 @@ class Portainer: def api_post(self, path, json="", timeout=120): url = f"{self.base_url.rstrip('/')}{path}" headers = {"X-API-Key": f"{self.token}"} - #print(url) - #print(json) + # print(url) + # print(json) resp = requests.post(url, headers=headers, json=json, timeout=timeout) return resp.text - + def api_post_file(self, path, endpoint_id, name, envs, file, timeout=120): #input("API POST2 called. Press Enter to continue.") """Example authenticated GET request to Portainer API.""" @@ -361,18 +422,15 @@ class Portainer: self.endpoint_id = self.get_endpoint_id(endpoint) if os.path.exists(self.repo_dir): shutil.rmtree(self.repo_dir) - print(f"Folder '{self.repo_dir}' has been removed.") else: print(f"Folder '{self.repo_dir}' does not exist.") Repo.clone_from(self.git_url, self.repo_dir) if mode == "git": - print("Creating new stack from git repo...") path = f"/stacks/create/{p}/repository" if self.endpoint_id is not None: path += f"?endpointId={self.endpoint_id}" - - print(path) + if stack == "all": if self.endpoint_name == "rack": diff --git a/portainer.py b/portainer.py index ff1447a..a607173 100755 --- a/portainer.py +++ b/portainer.py @@ -24,8 +24,8 @@ defaults = { parser = argparse.ArgumentParser(description="Portainer helper - use env vars or pass credentials.") -parser.add_argument("--base", "-b", default=os.getenv("PORTAINER_URL", "https://portainer.example.com"), - help="Base URL for Portainer (ENV: PORTAINER_URL)") +parser.add_argument("--base", "-b", default=os.getenv("PORTAINER_URL", \ +"https://portainer.example.com"),help="Base URL for Portainer (ENV: PORTAINER_URL)") parser.add_argument("--site", "-t", type=str, default=None, help="Site") parser.add_argument("--endpoint-id", "-e", type=str, default=None, help="Endpoint ID to limit stack operations") parser.add_argument("--refresh-environment", "-R", action="store_true", help="List endpoints") @@ -114,7 +114,24 @@ def get_portainer_token(base_url, username=None, password=None, timeout=10): if not token: raise ValueError(f"No token found in response: {data}") return token - +def prompt_missing_args(args, defaults, fields): + """ + fields = [("arg_name", "Prompt text")] + """ + for field, text in fields: + value = getattr(args, field) + default = defaults.get(field) + + if value is None: + if default is not None: + prompt = f"{text} (default={default}) : " + value = input(prompt) or default + else: + value = input(f"{text}: ") + + setattr(args, field, value) + + return args if __name__ == "__main__": # Example usage: set PORTAINER_USER and PORTAINER_PASS in env, or pass literals below. #token = get_portainer_token(base,"admin","l4c1j4yd33Du5lo") # or get_portainer_token(base, "admin", "secret") @@ -149,24 +166,24 @@ if __name__ == "__main__": sys.exit() if args.action == "delete_stack": - if args.endpoint_id == None: - args.endpoint_id = input("Endpoint ID is required for deleting stacks : ") - if args.stack == None: - args.stack = input("Stack name or ID is required for deleting stacks : ") + args = prompt_missing_args(args, defaults, [ + ("site", "Site"), + ("endpoint_id", "Endpoint ID"), + ("stack", "Stack name or ID") + ]) por.delete_stack(args.endpoint_id, args.stack,) sys.exit() if args.action == "create_stack": - if args.endpoint_id == None: - args.endpoint_id = input(f"Endpoint ID (default={defaults["endpoint_id"]}) : ") or defaults["endpoint_id"] - if args.stack == None: - args.stack = input(f"Stack name or ID : ") - if args.stack_mode == None: - args.stack_mode = input(f"Stack mode (swarm or compose) (default={defaults["stack_mode"]}) : ") or defaults["stack_mode"] - if args.deploy_mode == None: - args.deploy_mode = input(f"Deploy mode (git or upload) (default={defaults["deploy_mode"]}) : ") or defaults["deploy_mode"] - if args.site == None: - args.site = input(f"Site (default={defaults["site"]}) : ") or defaults["site"] + args = prompt_missing_args(args, defaults, [ + ("site", "Site"), + ("endpoint_id", "Endpoint ID"), + ("stack", "Stack name or ID"), + ("stack_mode", "Stack mode (swarm or compose)"), + ("deploy_mode", "Deploy mode (git or upload)") + ]) + + por.create_stack(args.endpoint_id,args.stack, args.deploy_mode, args.autostart, args.stack_mode) sys.exit() @@ -183,7 +200,7 @@ if __name__ == "__main__": args.endpoint_id = input("Endpoint ID is required for starting stacks : ") if args.stack == None: args.stack = input("Stack name or ID is required for starting stacks : ") - por.start_stack(args.stack,args.endpoint_id) + por.start_stack(args.stack, args.endpoint_id) sys.exit() if args.action == "list_stacks": @@ -202,17 +219,17 @@ if __name__ == "__main__": por.update_stack(args.endpoint_id,args.stack,autostart) sys.exit() if args.action == "print_all_data": - print(json.dumps(por.all_data,indent=2)) - sys.exit() + print(json.dumps(por.all_data, indent=2)) + sys.exit() if args.action == "update_status": - por.update_status(args.endpoint_id,args.stack) + por.update_status(args.endpoint_id, args.stack) sys.exit() if args.action == "list_endpoints": eps = por.get_endpoints() data = [] for i in eps["by_id"]: - data.append([i,eps["by_id"][i]]) + data.append([i, eps["by_id"][i]]) headers = ["EndpointId", "Name"] print(tabulate(data, headers=headers, tablefmt="github")) @@ -226,37 +243,35 @@ if __name__ == "__main__": cont = [] for c in por.all_data["containers"][args.endpoint_id]: if args.stack == c or args.stack == "all": - cont+=por.all_data["containers"][args.endpoint_id][c] - por.stop_containers(args.endpoint_id,cont) + cont += por.all_data["containers"][args.endpoint_id][c] + por.stop_containers(args.endpoint_id, cont) sys.exit() if args.action == "start_containers": print("Starting containers") cont = [] - #input(json.dumps(por.all_data,indent=2)) + # input(json.dumps(por.all_data, indent=2)) for c in por.all_data["containers"][args.endpoint_id]: if args.stack == c or args.stack == "all": - cont+=por.all_data["containers"][args.endpoint_id][c] - por.start_containers(args.endpoint_id,cont) + cont += por.all_data["containers"][args.endpoint_id][c] + por.start_containers(args.endpoint_id, cont) sys.exit() if args.action == "start_containers": print("Starting containers") cont = [] - #input(json.dumps(por.all_data,indent=2)) + # input(json.dumps(por.all_data,indent=2)) for c in por.all_data["containers"][args.endpoint_id]: if args.stack == c or args.stack == "all": - cont+=por.all_data["containers"][args.endpoint_id][c] - por.start_containers(args.endpoint_id,cont) - sys.exit() + cont += por.all_data["containers"][args.endpoint_id][c] + por.start_containers(args.endpoint_id, cont) + sys.exit() if args.action == "refresh_environment": cont = por.refresh() sys.exit() if args.action == "refresh_status": - if args.stack== "all": + if args.stack == "all": print("Stopping all stacks...") stcks = por.get_stacks(base, token, endpoint_id=args.endpoint_id) - # stcks = get_stack(base, sta, token, endpoint_id=install_endpoint_id) else: por.refresh_status(base, args.stack_id, token) -