mirror of
https://gitlab.sectorq.eu/jaydee/portainer.git
synced 2026-05-04 18:49:50 +02:00
Compare commits
19 Commits
c5c5a5fafd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5adb2c7c2e | |||
| 8067ac8561 | |||
| 79e9e708b4 | |||
| 64c3615705 | |||
| f5c883964a | |||
| bc984f05d2 | |||
| a9a4de2038 | |||
| 9205e0c8f7 | |||
| 062176a875 | |||
| db7005b304 | |||
| 0e8fcaa530 | |||
| df151c4c7f | |||
| eba757bf25 | |||
| 1e1f82e658 | |||
| 5e36820f88 | |||
| 546f695a0d | |||
| 4a3609ef27 | |||
| 9986c1bc03 | |||
| 18afddee86 |
Binary file not shown.
@@ -16,6 +16,8 @@ import time
|
||||
import base64
|
||||
import shutil
|
||||
import requests
|
||||
import tempfile
|
||||
import subprocess
|
||||
from portainer.api import PortainerApi
|
||||
from git import Repo
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
@@ -46,7 +48,7 @@ def setup_vault():
|
||||
# Specify the mount point of your KV engine
|
||||
return vclient
|
||||
|
||||
VERSION = "0.1.75"
|
||||
VERSION = "0.1.77"
|
||||
|
||||
|
||||
defaults = {
|
||||
@@ -140,7 +142,7 @@ parser.add_argument(
|
||||
default=None,
|
||||
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(
|
||||
"--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("--deploy-mode", "-m", type=str, default="git", help="Deploy 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(
|
||||
"-E", "--excluded",
|
||||
nargs="+",
|
||||
@@ -222,6 +226,41 @@ def wl(msg):
|
||||
if args.debug:
|
||||
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):
|
||||
"""
|
||||
@@ -257,13 +296,14 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
|
||||
elif field == "stack":
|
||||
if args.action == "create_stack":
|
||||
# input(json.dumps(stacks, indent=2))
|
||||
commands = [
|
||||
'api_server', 'authentik', 'bitwarden', 'bookstack', 'databasus', 'dockermon', 'duplicati', 'fail2ban', 'filebrowser', 'gitea', 'gitlab', 'grafana', 'grocy',
|
||||
'hashicorp', 'home-assistant', 'homebox','homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'kopia', 'linkding', 'linkwarden', 'mailu3',
|
||||
'mealie', 'mediacenter', 'mosquitto', 'motioneye', 'n8n', 'nebula', 'nextcloud', 'nginx',
|
||||
'node-red', 'octoprint', 'ollama', 'onlyoffice', 'paperless-ngx', 'pihole', 'portainer-ce','portainerce', 'rancher', 'registry',
|
||||
'regsync', 'repo_mirror', 'searxng','semaphore', 'unifibrowser', 'uptime-kuma', 'watchtower', 'wazuh', 'webhub', 'wordpress',
|
||||
'wud', 'zabbix-server']
|
||||
commands = get_compose_files()
|
||||
# commands = [
|
||||
# 'api_server', 'authentik', 'bitwarden', 'bookstack', 'databasus', 'dockermon', 'duplicati', 'fail2ban', 'filebrowser', 'gitea', 'gitlab', 'grafana', 'grocy',
|
||||
# 'hashicorp', 'home-assistant', 'homebox','homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'kopia', 'linkding', 'linkwarden', 'mailu3',
|
||||
# 'mealie', 'mediacenter', 'mosquitto', 'motioneye', 'n8n', 'nebula', 'nextcloud', 'nginx',
|
||||
# 'node-red', 'octoprint', 'ollama', 'onlyoffice', 'paperless-ngx', 'pihole', 'portainer-ce','portainerce', 'puppet', 'puppet-agent', 'rancher', 'registry',
|
||||
# 'regsync', 'repo_mirror', 'searxng','semaphore', 'unifibrowser', 'uptime-kuma', 'watchtower', 'wazuh', 'webhub', 'wordpress',
|
||||
# 'wud', 'zabbix-server']
|
||||
try:
|
||||
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():
|
||||
@@ -302,6 +342,7 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
|
||||
commands.sort()
|
||||
commands_tuples = [(cmd, cmd) for cmd in commands]
|
||||
commands_tuples.insert(0, ("__ALL__", "[Select ALL]"))
|
||||
commands_tuples.insert(0, ("mandatory", "[Mandatory]"))
|
||||
value_in = checkboxlist_dialog(
|
||||
title="Select Services",
|
||||
text="Choose one or more services:",
|
||||
@@ -314,12 +355,14 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
|
||||
elif "__ALL__" in value_in:
|
||||
# User selected "Select ALL"
|
||||
value_in = commands # all real commands
|
||||
|
||||
elif "mandatory" in value_in:
|
||||
# User selected "Select ALL"
|
||||
value_in = ['pihole', 'nginx', 'authentik', 'hashicorp', 'mosquitto','homepage', 'mailu3', 'home-assistant', 'mediacenter' ] # all real commands
|
||||
|
||||
value_in.sort()
|
||||
|
||||
|
||||
if "pihole" in value_in:
|
||||
if action == "delete_stack":
|
||||
if args.action in ["delete_stack","stop_stack"]:
|
||||
value_in.remove("pihole")
|
||||
value_in.append("pihole")
|
||||
else:
|
||||
@@ -466,7 +509,6 @@ if __name__ == "__main__":
|
||||
for key, value in secrets.items():
|
||||
res = por.create_secret(key, value, args.endpoint_id, args.timeout)
|
||||
print(res)
|
||||
sys.exit()
|
||||
|
||||
if args.action == "delete_stack":
|
||||
args = prompt_missing_args(
|
||||
@@ -487,7 +529,6 @@ if __name__ == "__main__":
|
||||
args.endpoint_id,
|
||||
args.stack,
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
if args.action == "create_stack":
|
||||
por.action = "create_stack"
|
||||
@@ -512,7 +553,7 @@ if __name__ == "__main__":
|
||||
args.autostart,
|
||||
args.stack_mode,
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
|
||||
if args.action == "stop_stack":
|
||||
args = prompt_missing_args(
|
||||
@@ -526,7 +567,6 @@ if __name__ == "__main__":
|
||||
)
|
||||
|
||||
por.stop_stack(args.stack, args.endpoint_id)
|
||||
sys.exit()
|
||||
|
||||
if args.action == "start_stack":
|
||||
args = prompt_missing_args(
|
||||
@@ -539,7 +579,7 @@ if __name__ == "__main__":
|
||||
],
|
||||
)
|
||||
por.start_stack(args.stack, args.endpoint_id)
|
||||
sys.exit()
|
||||
|
||||
|
||||
if args.action == "restart_service":
|
||||
args = prompt_missing_args(
|
||||
@@ -551,7 +591,7 @@ if __name__ == "__main__":
|
||||
],
|
||||
)
|
||||
por.restart_service(args.endpoint_id, "lala")
|
||||
sys.exit()
|
||||
|
||||
|
||||
if args.action == "update_service":
|
||||
args = prompt_missing_args(
|
||||
@@ -565,7 +605,7 @@ if __name__ == "__main__":
|
||||
por.update_service()
|
||||
if args.launcher:
|
||||
input("\nPress ENTER to continue...")
|
||||
sys.exit()
|
||||
|
||||
|
||||
if args.action == "update_containers":
|
||||
|
||||
@@ -580,7 +620,6 @@ if __name__ == "__main__":
|
||||
],
|
||||
)
|
||||
por.update_containers()
|
||||
sys.exit()
|
||||
|
||||
if args.action == "list_stacks":
|
||||
args = prompt_missing_args(
|
||||
@@ -595,21 +634,19 @@ if __name__ == "__main__":
|
||||
if args.launcher:
|
||||
input("Press ENTER to continue...")
|
||||
# print(json.dumps(por.all_data, indent=2))
|
||||
sys.exit()
|
||||
|
||||
if args.action == "list_all_stacks":
|
||||
por.get_stacks_failed()
|
||||
if args.launcher:
|
||||
input("Press ENTER to continue...")
|
||||
# print(json.dumps(por.all_data, indent=2))
|
||||
sys.exit()
|
||||
|
||||
|
||||
if args.action == "delete_ophaned_stacks":
|
||||
por.delete_failed_stack()
|
||||
if args.launcher:
|
||||
input("Press ENTER to continue...")
|
||||
# print(json.dumps(por.all_data, indent=2))
|
||||
sys.exit()
|
||||
|
||||
|
||||
if args.action == "list_containers":
|
||||
@@ -625,7 +662,6 @@ if __name__ == "__main__":
|
||||
print("\n".join(por.get_containers()))
|
||||
if args.launcher:
|
||||
input("\nPress ENTER to continue...")
|
||||
sys.exit()
|
||||
|
||||
if args.action == "update_stack":
|
||||
args = prompt_missing_args(
|
||||
@@ -640,17 +676,16 @@ if __name__ == "__main__":
|
||||
por.update_stack(args)
|
||||
if args.launcher:
|
||||
input("\nPress ENTER to continue...")
|
||||
sys.exit()
|
||||
|
||||
|
||||
if args.action == "print_all_data":
|
||||
print(json.dumps(por.all_data, indent=2))
|
||||
if args.launcher:
|
||||
input("\nPress ENTER to continue...")
|
||||
sys.exit()
|
||||
|
||||
|
||||
if args.action == "update_status":
|
||||
por.update_status(args.endpoint_id, args.stack)
|
||||
sys.exit()
|
||||
|
||||
if args.action == "list_endpoints":
|
||||
eps = por.get_endpoints(args)
|
||||
@@ -661,7 +696,7 @@ if __name__ == "__main__":
|
||||
print(tabulate(export_data, headers=headers, tablefmt="github"))
|
||||
if args.launcher:
|
||||
input("\nPress ENTER to continue...")
|
||||
sys.exit()
|
||||
|
||||
|
||||
if args.action == "stop_containers":
|
||||
# TODO: does not work
|
||||
@@ -675,14 +710,14 @@ if __name__ == "__main__":
|
||||
)
|
||||
if por.all_data["endpoints_status"][args.endpoint_id] != 1:
|
||||
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)}")
|
||||
cont = []
|
||||
for c in por.all_data["containers"][args.endpoint_id]:
|
||||
if args.stack in (c, "all"):
|
||||
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")
|
||||
@@ -692,7 +727,7 @@ if __name__ == "__main__":
|
||||
if args.stack in (c, "all"):
|
||||
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 = []
|
||||
@@ -701,10 +736,28 @@ if __name__ == "__main__":
|
||||
if args.stack in (c, "all"):
|
||||
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":
|
||||
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.
+64
-18
@@ -13,6 +13,8 @@ import tabulate
|
||||
from git import Repo
|
||||
import requests
|
||||
import hvac
|
||||
import subprocess
|
||||
import tempfile
|
||||
from prompt_toolkit import prompt
|
||||
from prompt_toolkit.completion import WordCompleter
|
||||
from prompt_toolkit.shortcuts import checkboxlist_dialog
|
||||
@@ -37,6 +39,7 @@ class PortainerApi:
|
||||
self.timeout = timeout
|
||||
self.git_url = "git@gitlab.sectorq.eu:home/docker-compose.git"
|
||||
self.stack_name = None
|
||||
self.stack_names = []
|
||||
self.stacks_all = {}
|
||||
self.stack_id = None
|
||||
self.stack_ids = []
|
||||
@@ -122,6 +125,7 @@ class PortainerApi:
|
||||
self.cur_config = config
|
||||
|
||||
def get_site(self, site):
|
||||
self.site = site
|
||||
if site == "portainer":
|
||||
self.base_url = os.getenv(
|
||||
"PORTAINER_URL", "https://portainer.sectorq.eu/api"
|
||||
@@ -176,7 +180,7 @@ class PortainerApi:
|
||||
def _api_get(self, path, timeout=120):
|
||||
url = f"{self.base_url.rstrip('/')}{path}"
|
||||
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:
|
||||
return resp.status_code
|
||||
print(f"Error: {resp.status_code} - {resp.text}")
|
||||
@@ -273,7 +277,7 @@ class PortainerApi:
|
||||
stcks = []
|
||||
stacks = self._api_get(path, timeout=timeout)
|
||||
self.stacks_all = {}
|
||||
fail_endponts = [20, 39, 41, 32, 43]
|
||||
fail_endponts = [20, 39, 41, 32, 43, 44]
|
||||
# print(json.dumps(stacks,indent=2))
|
||||
webhooks = {}
|
||||
for s in stacks:
|
||||
@@ -656,7 +660,18 @@ class PortainerApi:
|
||||
autostart=False,
|
||||
stack_mode="swarm",
|
||||
):
|
||||
diff_stacks = ['mediacenter']
|
||||
for stack in stacks:
|
||||
server = ""
|
||||
print("Stack:", stack)
|
||||
print("Endpoint:", endpoint)
|
||||
if stack in diff_stacks:
|
||||
if endpoint == "nas":
|
||||
server = "_nas"
|
||||
elif endpoint == "m-s":
|
||||
server = "_m-server"
|
||||
|
||||
|
||||
if stack_mode == "swarm":
|
||||
swarm_id = self.get_swarm_id(endpoint)
|
||||
p = "swarm"
|
||||
@@ -748,8 +763,8 @@ class PortainerApi:
|
||||
},
|
||||
"repositoryURL": "https://gitlab.sectorq.eu/home/docker-compose.git",
|
||||
"ReferenceName": "refs/heads/main",
|
||||
"composeFile": f"{stack}/docker-compose.yml",
|
||||
"ConfigFilePath": f"{stack}/docker-compose.yml",
|
||||
"composeFile": f"{stack}/docker-compose{server}.yml",
|
||||
"ConfigFilePath": f"{stack}/docker-compose{server}.yml",
|
||||
"repositoryAuthentication": True,
|
||||
"repositoryUsername": "jaydee",
|
||||
"repositoryPassword": "glpat-uj-n-eEfTY398PE4vKSS",
|
||||
@@ -1208,44 +1223,75 @@ class PortainerApi:
|
||||
|
||||
def start_stack(self, stack=None, endpoint_id=None):
|
||||
"""Start one stack or all stacks on an endpoint."""
|
||||
ok = "\033[92m✔\033[0m"
|
||||
ok2 = "\033[93m✔\033[0m"
|
||||
err = "\033[91m✖\033[0m"
|
||||
if endpoint_id is not None:
|
||||
print("Getting endpoint")
|
||||
self.get_endpoint(endpoint_id)
|
||||
size = 0
|
||||
if stack is not None:
|
||||
if type(stack) == str:
|
||||
stack = stack.split(",")
|
||||
for s in stack:
|
||||
if len(s) > size:
|
||||
size = len(s)
|
||||
self.stack_ids.append(self._resolve_stack_id(s, endpoint_id))
|
||||
size = size + 5
|
||||
for stck in self.stack_ids:
|
||||
print(
|
||||
f"Starting stack {self.stacks_all[self.endpoint_id]['by_id'][stck][:size].ljust(size)}",
|
||||
end="", flush=True
|
||||
)
|
||||
path = f"/stacks/{stck}/start"
|
||||
if self.endpoint_id is not None:
|
||||
path += f"?endpointId={self.endpoint_id}"
|
||||
try:
|
||||
resp = self._api_post_no_body(path, timeout=20)
|
||||
resp = self._api_post_no_body(path, timeout=120)
|
||||
except ValueError as e:
|
||||
print(f"Error stoping stack: {e}")
|
||||
print(f"Error starting stack: {e}")
|
||||
return []
|
||||
if "Id" in json.loads(resp):
|
||||
print(
|
||||
f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stck]} : started"
|
||||
)
|
||||
print(ok)
|
||||
elif "already running" in json.loads(resp)['message']:
|
||||
print(ok2)
|
||||
else:
|
||||
print(
|
||||
f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stck]} : {json.loads(resp)['message']}"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def stop_stack(self, stack, endpoint_id):
|
||||
|
||||
"""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"
|
||||
ok2 = "\033[93m✔\033[0m"
|
||||
err = "\033[91m✖\033[0m"
|
||||
if endpoint_id is not None:
|
||||
self.get_endpoint(endpoint_id)
|
||||
|
||||
size = 0
|
||||
if stack is not None:
|
||||
if type(stack) == str:
|
||||
stack = stack.split(",")
|
||||
for s in stack:
|
||||
if size < len(s):
|
||||
size = len(s)
|
||||
self.stack_ids.append(self._resolve_stack_id(s, endpoint_id))
|
||||
# print(self.stack_ids)
|
||||
size = size + 5
|
||||
self.stack_ids = list(dict.fromkeys(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(
|
||||
f"Stopping stack {self.stacks_all[self.endpoint_id]['by_id'][stck][:size].ljust(size)}",
|
||||
end="",
|
||||
flush=True
|
||||
)
|
||||
path = f"/stacks/{stck}/stop"
|
||||
# print(path)
|
||||
if self.endpoint_id is not None:
|
||||
@@ -1256,9 +1302,9 @@ class PortainerApi:
|
||||
print(f"Error stopping stack: {e}")
|
||||
return []
|
||||
if "Id" in json.loads(resp):
|
||||
print(
|
||||
f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stck]} : stopped"
|
||||
)
|
||||
print(ok)
|
||||
elif "already inactive" in json.loads(resp)['message']:
|
||||
print(ok2)
|
||||
else:
|
||||
print(
|
||||
f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stck]} : {json.loads(resp)['message']}"
|
||||
@@ -1283,8 +1329,8 @@ class PortainerApi:
|
||||
return "all"
|
||||
|
||||
if not self._is_number(stack):
|
||||
result = self.get_stack(stack, endpoint_id)
|
||||
return result["Id"]
|
||||
result = self.stacks_all[endpoint_id]['by_name'][stack]
|
||||
return result
|
||||
|
||||
return int(stack)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user