This commit is contained in:
2025-12-02 20:00:22 +01:00
parent dec2f81b05
commit e052fd7038
2 changed files with 127 additions and 54 deletions

92
port.py
View File

@@ -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,8 +123,8 @@ 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
@@ -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":

View File

@@ -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))
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)
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)