Compare commits

...

16 Commits

Author SHA1 Message Date
jaydee 5adb2c7c2e build 2026-03-29 20:18:13 +02:00
jaydee 8067ac8561 build 2026-03-28 12:33:16 +01:00
jaydee 79e9e708b4 build 2026-03-28 12:33:00 +01:00
ladislav.dusa 64c3615705 build 2026-03-25 14:56:28 +01:00
jaydee f5c883964a build 2026-03-24 23:19:58 +01:00
jaydee bc984f05d2 build 2026-03-24 12:26:15 +01:00
jaydee a9a4de2038 build 2026-03-24 12:16:29 +01:00
jaydee 9205e0c8f7 build 2026-03-23 16:56:46 +01:00
jaydee 062176a875 build 2026-03-22 15:28:56 +01:00
jaydee db7005b304 build 2026-03-21 22:46:02 +01:00
jaydee 0e8fcaa530 build 2026-03-21 21:53:36 +01:00
jaydee df151c4c7f build 2026-03-21 21:24:03 +01:00
jaydee eba757bf25 build 2026-03-21 21:19:48 +01:00
jaydee 1e1f82e658 build 2026-03-21 21:07:18 +01:00
jaydee 5e36820f88 build 2026-03-21 19:09:41 +01:00
jaydee 546f695a0d build 2026-03-21 11:33:26 +01:00
9 changed files with 109 additions and 40 deletions
Binary file not shown.
+83 -33
View File
@@ -16,6 +16,8 @@ import time
import base64 import base64
import shutil import shutil
import requests import requests
import tempfile
import subprocess
from portainer.api import PortainerApi from portainer.api import PortainerApi
from git import Repo from git import Repo
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@@ -46,7 +48,7 @@ def setup_vault():
# Specify the mount point of your KV engine # Specify the mount point of your KV engine
return vclient return vclient
VERSION = "0.1.75" VERSION = "0.1.77"
defaults = { defaults = {
@@ -140,7 +142,7 @@ parser.add_argument(
default=None, default=None,
help="Service ID to limit service operations", help="Service ID to limit service operations",
) )
parser.add_argument("--stack", "-s", type=str, default=None, nargs="+", help="Stack ID for operations") parser.add_argument("--stack", "-s", type=str, default=None, help="Stack ID for operations")
parser.add_argument("--action", "-a", type=str, default=None, help="Action to perform") parser.add_argument("--action", "-a", type=str, default=None, help="Action to perform")
parser.add_argument( parser.add_argument(
"--autostart", "-Z", action="store_true", help="Auto-start created stacks" "--autostart", "-Z", action="store_true", help="Auto-start created stacks"
@@ -152,6 +154,8 @@ parser.add_argument("--gpu", "-g", action="store_true")
parser.add_argument("--timeout", type=int, default=10, help="Request timeout seconds") parser.add_argument("--timeout", type=int, default=10, help="Request timeout seconds")
parser.add_argument("--deploy-mode", "-m", type=str, default="git", help="Deploy mode") parser.add_argument("--deploy-mode", "-m", type=str, default="git", help="Deploy mode")
parser.add_argument("--stack-mode", "-w", default=None, help="Stack mode") parser.add_argument("--stack-mode", "-w", default=None, help="Stack mode")
parser.add_argument("--print-command", "-P", action="store_true", help="Print quick command from action")
parser.add_argument( parser.add_argument(
"-E", "--excluded", "-E", "--excluded",
nargs="+", nargs="+",
@@ -222,6 +226,41 @@ def wl(msg):
if args.debug: if args.debug:
print(msg) print(msg)
def run(cmd, cwd=None):
result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(result.stderr)
return result.stdout.strip()
def get_compose_files():
#git clone --depth=1 --filter=blob:none --no-checkout https://github.com/user/repo.git
with tempfile.TemporaryDirectory() as tmpdir:
repo_path = os.path.join(tmpdir, "repo")
# Clone with minimal data (no checkout, no blobs)
run([
"git", "clone",
"--depth=1",
"--filter=blob:none",
"--no-checkout",
"git@gitlab.sectorq.eu:/home/docker-compose.git",
repo_path
])
# List files in HEAD
output = run([
"git", "ls-tree",
"-r",
"HEAD",
"--name-only"
], cwd=repo_path)
folders = []
for line in output.splitlines():
if "/" in line and line.split("/")[0] != "__swarm":
folders.append(line.split("/")[0])
return list(dict.fromkeys(folders))
def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None): def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
""" """
@@ -257,13 +296,14 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
elif field == "stack": elif field == "stack":
if args.action == "create_stack": if args.action == "create_stack":
# input(json.dumps(stacks, indent=2)) # input(json.dumps(stacks, indent=2))
commands = [ commands = get_compose_files()
'api_server', 'authentik', 'bitwarden', 'bookstack', 'databasus', 'dockermon', 'duplicati', 'fail2ban', 'filebrowser', 'gitea', 'gitlab', 'grafana', 'grocy', # commands = [
'hashicorp', 'home-assistant', 'homebox','homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'kopia', 'linkding', 'linkwarden', 'mailu3', # 'api_server', 'authentik', 'bitwarden', 'bookstack', 'databasus', 'dockermon', 'duplicati', 'fail2ban', 'filebrowser', 'gitea', 'gitlab', 'grafana', 'grocy',
'mealie', 'mediacenter', 'mosquitto', 'motioneye', 'n8n', 'nebula', 'nextcloud', 'nginx', # 'hashicorp', 'home-assistant', 'homebox','homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'kopia', 'linkding', 'linkwarden', 'mailu3',
'node-red', 'octoprint', 'ollama', 'onlyoffice', 'paperless-ngx', 'pihole', 'portainer-ce','portainerce', 'rancher', 'registry', # 'mealie', 'mediacenter', 'mosquitto', 'motioneye', 'n8n', 'nebula', 'nextcloud', 'nginx',
'regsync', 'repo_mirror', 'searxng','semaphore', 'unifibrowser', 'uptime-kuma', 'watchtower', 'wazuh', 'webhub', 'wordpress', # 'node-red', 'octoprint', 'ollama', 'onlyoffice', 'paperless-ngx', 'pihole', 'portainer-ce','portainerce', 'puppet', 'puppet-agent', 'rancher', 'registry',
'wud', 'zabbix-server'] # 'regsync', 'repo_mirror', 'searxng','semaphore', 'unifibrowser', 'uptime-kuma', 'watchtower', 'wazuh', 'webhub', 'wordpress',
# 'wud', 'zabbix-server']
try: try:
print(por.all_data['stacks'][defaults_in[f"PORTAINER_ENDPOINT_ID".upper()]]['by_name'].keys()) print(por.all_data['stacks'][defaults_in[f"PORTAINER_ENDPOINT_ID".upper()]]['by_name'].keys())
for s in por.all_data['stacks'][defaults_in[f"PORTAINER_ENDPOINT_ID".upper()]]['by_name'].keys(): for s in por.all_data['stacks'][defaults_in[f"PORTAINER_ENDPOINT_ID".upper()]]['by_name'].keys():
@@ -320,9 +360,9 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
value_in = ['pihole', 'nginx', 'authentik', 'hashicorp', 'mosquitto','homepage', 'mailu3', 'home-assistant', 'mediacenter' ] # all real commands value_in = ['pihole', 'nginx', 'authentik', 'hashicorp', 'mosquitto','homepage', 'mailu3', 'home-assistant', 'mediacenter' ] # all real commands
value_in.sort() value_in.sort()
if "pihole" in value_in: if "pihole" in value_in:
if args.action == "delete_stack": if args.action in ["delete_stack","stop_stack"]:
value_in.remove("pihole") value_in.remove("pihole")
value_in.append("pihole") value_in.append("pihole")
else: else:
@@ -469,7 +509,6 @@ if __name__ == "__main__":
for key, value in secrets.items(): for key, value in secrets.items():
res = por.create_secret(key, value, args.endpoint_id, args.timeout) res = por.create_secret(key, value, args.endpoint_id, args.timeout)
print(res) print(res)
sys.exit()
if args.action == "delete_stack": if args.action == "delete_stack":
args = prompt_missing_args( args = prompt_missing_args(
@@ -490,7 +529,6 @@ if __name__ == "__main__":
args.endpoint_id, args.endpoint_id,
args.stack, args.stack,
) )
sys.exit()
if args.action == "create_stack": if args.action == "create_stack":
por.action = "create_stack" por.action = "create_stack"
@@ -515,7 +553,7 @@ if __name__ == "__main__":
args.autostart, args.autostart,
args.stack_mode, args.stack_mode,
) )
sys.exit()
if args.action == "stop_stack": if args.action == "stop_stack":
args = prompt_missing_args( args = prompt_missing_args(
@@ -529,7 +567,6 @@ if __name__ == "__main__":
) )
por.stop_stack(args.stack, args.endpoint_id) por.stop_stack(args.stack, args.endpoint_id)
sys.exit()
if args.action == "start_stack": if args.action == "start_stack":
args = prompt_missing_args( args = prompt_missing_args(
@@ -542,7 +579,7 @@ if __name__ == "__main__":
], ],
) )
por.start_stack(args.stack, args.endpoint_id) por.start_stack(args.stack, args.endpoint_id)
sys.exit()
if args.action == "restart_service": if args.action == "restart_service":
args = prompt_missing_args( args = prompt_missing_args(
@@ -554,7 +591,7 @@ if __name__ == "__main__":
], ],
) )
por.restart_service(args.endpoint_id, "lala") por.restart_service(args.endpoint_id, "lala")
sys.exit()
if args.action == "update_service": if args.action == "update_service":
args = prompt_missing_args( args = prompt_missing_args(
@@ -568,7 +605,7 @@ if __name__ == "__main__":
por.update_service() por.update_service()
if args.launcher: if args.launcher:
input("\nPress ENTER to continue...") input("\nPress ENTER to continue...")
sys.exit()
if args.action == "update_containers": if args.action == "update_containers":
@@ -583,7 +620,6 @@ if __name__ == "__main__":
], ],
) )
por.update_containers() por.update_containers()
sys.exit()
if args.action == "list_stacks": if args.action == "list_stacks":
args = prompt_missing_args( args = prompt_missing_args(
@@ -598,21 +634,19 @@ if __name__ == "__main__":
if args.launcher: if args.launcher:
input("Press ENTER to continue...") input("Press ENTER to continue...")
# print(json.dumps(por.all_data, indent=2)) # print(json.dumps(por.all_data, indent=2))
sys.exit()
if args.action == "list_all_stacks": if args.action == "list_all_stacks":
por.get_stacks_failed() por.get_stacks_failed()
if args.launcher: if args.launcher:
input("Press ENTER to continue...") input("Press ENTER to continue...")
# print(json.dumps(por.all_data, indent=2)) # print(json.dumps(por.all_data, indent=2))
sys.exit()
if args.action == "delete_ophaned_stacks": if args.action == "delete_ophaned_stacks":
por.delete_failed_stack() por.delete_failed_stack()
if args.launcher: if args.launcher:
input("Press ENTER to continue...") input("Press ENTER to continue...")
# print(json.dumps(por.all_data, indent=2)) # print(json.dumps(por.all_data, indent=2))
sys.exit()
if args.action == "list_containers": if args.action == "list_containers":
@@ -628,7 +662,6 @@ if __name__ == "__main__":
print("\n".join(por.get_containers())) print("\n".join(por.get_containers()))
if args.launcher: if args.launcher:
input("\nPress ENTER to continue...") input("\nPress ENTER to continue...")
sys.exit()
if args.action == "update_stack": if args.action == "update_stack":
args = prompt_missing_args( args = prompt_missing_args(
@@ -643,17 +676,16 @@ if __name__ == "__main__":
por.update_stack(args) por.update_stack(args)
if args.launcher: if args.launcher:
input("\nPress ENTER to continue...") input("\nPress ENTER to continue...")
sys.exit()
if args.action == "print_all_data": if args.action == "print_all_data":
print(json.dumps(por.all_data, indent=2)) print(json.dumps(por.all_data, indent=2))
if args.launcher: if args.launcher:
input("\nPress ENTER to continue...") input("\nPress ENTER to continue...")
sys.exit()
if args.action == "update_status": 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": if args.action == "list_endpoints":
eps = por.get_endpoints(args) eps = por.get_endpoints(args)
@@ -664,7 +696,7 @@ if __name__ == "__main__":
print(tabulate(export_data, headers=headers, tablefmt="github")) print(tabulate(export_data, headers=headers, tablefmt="github"))
if args.launcher: if args.launcher:
input("\nPress ENTER to continue...") input("\nPress ENTER to continue...")
sys.exit()
if args.action == "stop_containers": if args.action == "stop_containers":
# TODO: does not work # TODO: does not work
@@ -678,14 +710,14 @@ if __name__ == "__main__":
) )
if por.all_data["endpoints_status"][args.endpoint_id] != 1: if por.all_data["endpoints_status"][args.endpoint_id] != 1:
print(f"Endpoint {por.get_endpoint_name(args.endpoint_id)} is offline") print(f"Endpoint {por.get_endpoint_name(args.endpoint_id)} is offline")
sys.exit()
print(f"Stopping containers on {por.get_endpoint_name(args.endpoint_id)}") print(f"Stopping containers on {por.get_endpoint_name(args.endpoint_id)}")
cont = [] cont = []
for c in por.all_data["containers"][args.endpoint_id]: for c in por.all_data["containers"][args.endpoint_id]:
if args.stack in (c, "all"): if args.stack in (c, "all"):
cont += por.all_data["containers"][args.endpoint_id][c] cont += por.all_data["containers"][args.endpoint_id][c]
por.stop_containers(args.endpoint_id, cont) por.stop_containers(args.endpoint_id, cont)
sys.exit()
if args.action == "start_containers": if args.action == "start_containers":
print("Starting containers") print("Starting containers")
@@ -695,7 +727,7 @@ if __name__ == "__main__":
if args.stack in (c, "all"): if args.stack in (c, "all"):
cont += por.all_data["containers"][args.endpoint_id][c] cont += por.all_data["containers"][args.endpoint_id][c]
por.start_containers(args.endpoint_id, cont) por.start_containers(args.endpoint_id, cont)
sys.exit()
if args.action == "start_containers": if args.action == "start_containers":
print("Starting containers") print("Starting containers")
cont = [] cont = []
@@ -704,10 +736,28 @@ if __name__ == "__main__":
if args.stack in (c, "all"): if args.stack in (c, "all"):
cont += por.all_data["containers"][args.endpoint_id][c] cont += por.all_data["containers"][args.endpoint_id][c]
por.start_containers(args.endpoint_id, cont) por.start_containers(args.endpoint_id, cont)
sys.exit()
if args.action == "refresh_environment": if args.action == "refresh_environment":
cont = por.refresh() cont = por.refresh()
sys.exit()
if args.action == "refresh_status": if args.action == "refresh_status":
por.refresh_status(args) por.refresh_status(args)
if args.print_command:
one_time_command = "portainer"
if args.action:
one_time_command += f" --action={args.action}"
if por.endpoint_name:
one_time_command += f" --endpoint-id={por.endpoint_name}"
if por.site:
one_time_command += f" --site={por.site}"
if args.stack:
if type(args.stack) == list:
args.stack = ",".join(args.stack)
one_time_command += f" --stack={args.stack}"
width = shutil.get_terminal_size().columns
input(width)
print("#"*width)
print(f"COMMAND : {one_time_command}")
print("#"*width)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+26 -7
View File
@@ -13,6 +13,8 @@ import tabulate
from git import Repo from git import Repo
import requests import requests
import hvac import hvac
import subprocess
import tempfile
from prompt_toolkit import prompt from prompt_toolkit import prompt
from prompt_toolkit.completion import WordCompleter from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.shortcuts import checkboxlist_dialog from prompt_toolkit.shortcuts import checkboxlist_dialog
@@ -37,6 +39,7 @@ class PortainerApi:
self.timeout = timeout self.timeout = timeout
self.git_url = "git@gitlab.sectorq.eu:home/docker-compose.git" self.git_url = "git@gitlab.sectorq.eu:home/docker-compose.git"
self.stack_name = None self.stack_name = None
self.stack_names = []
self.stacks_all = {} self.stacks_all = {}
self.stack_id = None self.stack_id = None
self.stack_ids = [] self.stack_ids = []
@@ -122,6 +125,7 @@ class PortainerApi:
self.cur_config = config self.cur_config = config
def get_site(self, site): def get_site(self, site):
self.site = site
if site == "portainer": if site == "portainer":
self.base_url = os.getenv( self.base_url = os.getenv(
"PORTAINER_URL", "https://portainer.sectorq.eu/api" "PORTAINER_URL", "https://portainer.sectorq.eu/api"
@@ -176,7 +180,7 @@ class PortainerApi:
def _api_get(self, path, timeout=120): def _api_get(self, path, timeout=120):
url = f"{self.base_url.rstrip('/')}{path}" url = f"{self.base_url.rstrip('/')}{path}"
headers = {"X-API-Key": f"{self.token}"} headers = {"X-API-Key": f"{self.token}"}
resp = requests.get(url, headers=headers, timeout=timeout) resp = requests.get(url, headers=headers, timeout=120)
if resp.status_code != 200: if resp.status_code != 200:
return resp.status_code return resp.status_code
print(f"Error: {resp.status_code} - {resp.text}") print(f"Error: {resp.status_code} - {resp.text}")
@@ -273,7 +277,7 @@ class PortainerApi:
stcks = [] stcks = []
stacks = self._api_get(path, timeout=timeout) stacks = self._api_get(path, timeout=timeout)
self.stacks_all = {} self.stacks_all = {}
fail_endponts = [20, 39, 41, 32, 43] fail_endponts = [20, 39, 41, 32, 43, 44]
# print(json.dumps(stacks,indent=2)) # print(json.dumps(stacks,indent=2))
webhooks = {} webhooks = {}
for s in stacks: for s in stacks:
@@ -656,12 +660,18 @@ class PortainerApi:
autostart=False, autostart=False,
stack_mode="swarm", stack_mode="swarm",
): ):
diff_stacks = ['mediacenter']
for stack in stacks: for stack in stacks:
if self.endpoint_name == "nas": server = ""
server = "_nas" print("Stack:", stack)
else: print("Endpoint:", endpoint)
server = "" if stack in diff_stacks:
if endpoint == "nas":
server = "_nas"
elif endpoint == "m-s":
server = "_m-server"
if stack_mode == "swarm": if stack_mode == "swarm":
swarm_id = self.get_swarm_id(endpoint) swarm_id = self.get_swarm_id(endpoint)
p = "swarm" p = "swarm"
@@ -1221,6 +1231,8 @@ class PortainerApi:
self.get_endpoint(endpoint_id) self.get_endpoint(endpoint_id)
size = 0 size = 0
if stack is not None: if stack is not None:
if type(stack) == str:
stack = stack.split(",")
for s in stack: for s in stack:
if len(s) > size: if len(s) > size:
size = len(s) size = len(s)
@@ -1254,6 +1266,7 @@ class PortainerApi:
"""Stop one stack or all stacks on an endpoint.""" """Stop one stack or all stacks on an endpoint."""
# print(f"Stopping stack {stack}") # print(f"Stopping stack {stack}")
protected_stack = ['hashicorp','nginx','pihole',]
ok = "\033[92m✔\033[0m" ok = "\033[92m✔\033[0m"
ok2 = "\033[93m✔\033[0m" ok2 = "\033[93m✔\033[0m"
err = "\033[91m✖\033[0m" err = "\033[91m✖\033[0m"
@@ -1261,6 +1274,8 @@ class PortainerApi:
self.get_endpoint(endpoint_id) self.get_endpoint(endpoint_id)
size = 0 size = 0
if stack is not None: if stack is not None:
if type(stack) == str:
stack = stack.split(",")
for s in stack: for s in stack:
if size < len(s): if size < len(s):
size = len(s) size = len(s)
@@ -1268,6 +1283,10 @@ class PortainerApi:
size = size + 5 size = size + 5
self.stack_ids = list(dict.fromkeys(self.stack_ids)) self.stack_ids = list(dict.fromkeys(self.stack_ids))
for stck in self.stack_ids: for stck in self.stack_ids:
if self.stacks_all[self.endpoint_id]['by_id'] in protected_stack:
ans = input(f"Really stop {self.stacks_all[self.endpoint_id]['by_id'][stck]} ? ") or "n"
if ans != "y":
continue
print( print(
f"Stopping stack {self.stacks_all[self.endpoint_id]['by_id'][stck][:size].ljust(size)}", f"Stopping stack {self.stacks_all[self.endpoint_id]['by_id'][stck][:size].ljust(size)}",
end="", end="",