Compare commits

...

7 Commits

Author SHA1 Message Date
9be4051720 build 2025-12-12 17:46:33 +01:00
2319a13554 Update .gitlab-ci.yml file 2025-12-12 17:45:00 +01:00
b434887ff9 build 2025-12-12 17:13:57 +01:00
4c8beff0c8 build 2025-12-12 15:58:03 +01:00
d6842eab62 build 2025-12-12 15:19:53 +01:00
5864085ec3 build 2025-12-12 10:48:09 +01:00
04248ce279 build 2025-12-12 10:39:06 +01:00
3 changed files with 124 additions and 102 deletions

View File

@@ -7,9 +7,9 @@ variables:
GIT_SSH_COMMAND: "ssh -i /home/gitlab-runner/.ssh/id_rsa -o IdentitiesOnly=yes" GIT_SSH_COMMAND: "ssh -i /home/gitlab-runner/.ssh/id_rsa -o IdentitiesOnly=yes"
lint: lint:
stage: lint stage: lint
# image: python:3.12 image: r.sectorq.eu/jaydee/builder-portainer:latest
before_script: before_script:
- python3 -m pip install --break-system-packages flake8 black pylint tabulate prompt_toolkit - python3 -m pip install --break-system-packages flake8 black pylint tabulate prompt_toolkit hvac
- export PATH="$PATH:/home/gitlab-runner/.local/bin" - export PATH="$PATH:/home/gitlab-runner/.local/bin"
# - echo "PATH is now: $PATH" # - echo "PATH is now: $PATH"
script: script:

117
port.py
View File

@@ -12,6 +12,7 @@ import base64
import tabulate import tabulate
from git import Repo from git import Repo
import requests import requests
import hvac
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
@@ -27,9 +28,10 @@ class Portainer:
to perform API operations. to perform API operations.
""" """
def __init__(self, site, timeout=10): def __init__(self, site, args=None, timeout=120):
self.base_url = None self.base_url = None
self.token = None self.token = None
self.args = args
self.action = None self.action = None
self._debug = False self._debug = False
self.timeout = timeout self.timeout = timeout
@@ -39,7 +41,7 @@ class Portainer:
self.stack_id = None self.stack_id = None
self.stack_ids = [] self.stack_ids = []
self.endpoint_name = None self.endpoint_name = None
self.endpoint_id = None self.endpoint_id = args.endpoint_id
# 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.git_url = "git@gitlab.sectorq.eu:home/docker-compose.git"
@@ -125,10 +127,11 @@ class 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"
) )
self.token = "ptr_GCNUoFcTOaXm7k8ZxPdQGmrFIamxZPTydbserYofMHc=" # self.token = "ptr_GCNUoFcTOaXm7k8ZxPdQGmrFIamxZPTydbserYofMHc="
token_path = "portainer/token"
self.token = self.args.client.secrets.kv.v2.read_secret_version(path=token_path)['data']['data']['value']
elif site == "port": elif site == "port":
self.base_url = os.getenv("PORTAINER_URL", "https://port.sectorq.eu/api") self.base_url = os.getenv("PORTAINER_URL", "https://port.sectorq.eu/api")
self.token = "ptr_/5RkMCT/j3BTaL32vMSDtXFi76yOXRKVFOrUtzMsl5Y="
else: else:
self.base_url = os.getenv( self.base_url = os.getenv(
"PORTAINER_URL", "https://portainer.sectorq.eu/api" "PORTAINER_URL", "https://portainer.sectorq.eu/api"
@@ -145,6 +148,22 @@ class Portainer:
except ValueError: except ValueError:
return False return False
def gotify_message(self, message):
payload = {
"title": "Updates in Portainer",
"message": message,
"priority": 5
}
'''Send a notification message via Gotify.'''
response = requests.post(
"https://gotify.sectorq.eu/message",
data=payload,
headers={"X-Gotify-Key": "ASn_fIAd5OVjm8c"}
)
# print("Status:", response.status_code)
# print("Response:", response.text)
pass
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}"}
@@ -275,9 +294,9 @@ class Portainer:
def get_services(self, endpoint, timeout=30): def get_services(self, endpoint, timeout=30):
'''Get a list of services for a specific stack on an endpoint.''' '''Get a list of services for a specific stack on an endpoint.'''
print(json.dumps(self.all_data,indent=2)) # print(json.dumps(self.all_data,indent=2))
path = f"/endpoints/{endpoint}/docker/services" path = f"/endpoints/{self.get_endpoint_id(endpoint)}/docker/services"
print(path) # print(path)
# path += f'?filters={{"label": ["com.docker.compose.project={stack}"]}}' # path += f'?filters={{"label": ["com.docker.compose.project={stack}"]}}'
services = self._api_get(path, timeout=timeout) services = self._api_get(path, timeout=timeout)
return services return services
@@ -399,7 +418,11 @@ class Portainer:
for s in self.all_data["webhooks"][endpoint]: for s in self.all_data["webhooks"][endpoint]:
stcs.append([s, self.all_data["webhooks"][endpoint][s]]) stcs.append([s, self.all_data["webhooks"][endpoint][s]])
else: else:
try:
stcs.append([stack, self.all_data["webhooks"][endpoint][stack]]) stcs.append([stack, self.all_data["webhooks"][endpoint][stack]])
except Exception as e:
print(f"Error: Stack {stack} not found on endpoint {endpoint}: {e}")
# input(stcs) # input(stcs)
def update(c): def update(c):
@@ -724,7 +747,6 @@ class Portainer:
} }
self._api_post_file(path, self.endpoint_id, stack, envs, file) self._api_post_file(path, self.endpoint_id, stack, envs, file)
def print_stacks(self, endpoint="all"): def print_stacks(self, endpoint="all"):
"""Print a table of stacks, optionally filtered by endpoint.""" """Print a table of stacks, optionally filtered by endpoint."""
stacks = self.get_stacks() stacks = self.get_stacks()
@@ -759,47 +781,44 @@ class Portainer:
print(f"Total stacks: {count}") print(f"Total stacks: {count}")
# print(sorted(stack_names)) # print(sorted(stack_names))
def update_service(self, endpoint_id=None, service_id=None): def update_service(self):
all_services = self.get_services(self.get_endpoint_id(endpoint_id)) all_services = self.get_services(self.get_endpoint_id(self.args.endpoint_id))
service_tuples = [(s['ID'], s['Spec']['Name']) for s in all_services] service_tuples = [(s['ID'], s['Spec']['Name']) for s in all_services]
service_tuples = sorted(service_tuples, key=lambda x: x[1])
input(service_tuples) service_dict = dict(service_tuples)
if service_id == "all": # input(service_tuples)
if self.args.service_id is None:
service_id = self.all
#services = [(s["Id"], s["Name"]) for s in self.get_stacks(endpoint_id)] #services = [(s["Id"], s["Name"]) for s in self.get_stacks(endpoint_id)]
services.insert(0, ("__ALL__", "[Select ALL]")) service_tuples.insert(0, ("__ALL__", "[Select ALL]"))
service_tuples.insert(0, ("__ONLY_CHECK__", "[Check Only]"))
service_ids = checkboxlist_dialog( service_ids = checkboxlist_dialog(
title="Select one service", title="Select one service",
text="Choose a service:", text="Choose a service:",
values=services values=service_tuples
).run() ).run()
if service_ids == "__ALL__": elif self.args.service_id == "all":
pass service_ids = [s[0] for s in service_tuples if s[0] != "__ALL__" and s[0] != "__ONLY_CHECK__"]
print(service_ids) else:
service_dict = dict(service_ids) service_ids = [self.args.service_id]
services = self.get_services(self.endpoint_name, stack_id) if "__ONLY_CHECK__" in service_ids and self.args.update is False:
svc_name = service_dict.get(stack_id) pull = False
stack_svcs = [] else:
svc_menu = [] pull = True
for s in services: if "__ALL__" in service_ids:
try: service_ids = [s[0] for s in service_tuples if s[0] != "__ALL__" and s[0] != "__ONLY_CHECK__"]
if svc_name in s['Spec']['Name']:
stack_svcs.append([s['Version']['Index'], s['Spec']['Name']])
svc_menu.append([s['ID'], s['Spec']['Name']])
except KeyError as e:
print(e)
longest = 0
service_id = radiolist_dialog( for a in service_dict.items():
title="Select one service", # print(a[1])
text="Choose a service:", if len(a[1]) > longest:
values=svc_menu longest = len(a[1])
).run() #print(longest)
ok = "\033[92m✔\033[0m"
err = "\033[91m✖\033[0m"
"""Restart a service on an endpoint.""" for service_id in service_ids:
print("\033[?25l", end="")
print(f"{service_dict[service_id]:<{longest}} ", end="", flush=True)
path = f"/docker/{self.endpoint_id}/services/{service_id}/image_status?refresh=true" path = f"/docker/{self.endpoint_id}/services/{service_id}/image_status?refresh=true"
try: try:
@@ -807,9 +826,21 @@ class Portainer:
except ValueError as e: except ValueError as e:
print(f"Error restarting service: {e}") print(f"Error restarting service: {e}")
return [] return []
if resp['Status'] == "outdated": if resp['Status'] == "outdated":
self.restart_srv(service_id, True) if pull:
print(f"Service {service_id} : restarted") self.restart_srv(service_id, pull)
#print(f"Service {service_dict[service_id]:<{longest}} : updated")
self.gotify_message(f"Service {service_dict[service_id]} updated")
print(ok)
else:
print(f"\r\033[4m{service_dict[service_id]:<{longest}}\033[0m ", end="", flush=True)
#print(f"\033[4m{service_dict[service_id]:<{longest}} {err}\033[0m")
self.gotify_message(f"Service update available for {service_dict[service_id]}")
print(err)
else:
print(ok)
print("\033[?25h", end="")
return True return True
def restart_srv(self,service_id, pool=False): def restart_srv(self,service_id, pool=False):

View File

@@ -14,6 +14,7 @@ import json
import argparse import argparse
import tty import tty
import termios import termios
import hvac
from tabulate import tabulate from tabulate import tabulate
from port import Portainer from port import Portainer
from prompt_toolkit import prompt from prompt_toolkit import prompt
@@ -21,6 +22,24 @@ from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.shortcuts import checkboxlist_dialog from prompt_toolkit.shortcuts import checkboxlist_dialog
from prompt_toolkit.shortcuts import radiolist_dialog from prompt_toolkit.shortcuts import radiolist_dialog
VAULT_ADDR = os.environ.get("VAULT_ADDR", "http://192.168.77.101:8200")
try:
VAULT_TOKEN = os.environ.get("VAULT_TOKEN")
if VAULT_TOKEN is None:
raise KeyError
except KeyError:
VAULT_TOKEN = input("Valult root token : ")
os.environ["VAULT_TOKEN"] = VAULT_TOKEN
input(VAULT_TOKEN)
client = hvac.Client(url=VAULT_ADDR, token=VAULT_TOKEN)
# Check if connected
if client.is_authenticated():
print("Connected to Vault")
else:
raise Exception("Failed to authenticate with Vault")
# Specify the mount point of your KV engine
VERSION = "0.1.13" VERSION = "0.1.13"
defaults = { defaults = {
@@ -34,8 +53,6 @@ defaults = {
cur_config = {} cur_config = {}
def load_config(defaults=defaults): def load_config(defaults=defaults):
'''Load configuration from /myapps/portainer.conf if it exists, else from env vars or defaults.''' '''Load configuration from /myapps/portainer.conf if it exists, else from env vars or defaults.'''
if os.path.exists("/myapps/portainer.conf"): if os.path.exists("/myapps/portainer.conf"):
@@ -71,7 +88,7 @@ def load_config(defaults=defaults):
cur_config = load_config(defaults) a = load_config(defaults)
# ENV_VARS = [ # ENV_VARS = [
# "PORTAINER_URL", # "PORTAINER_URL",
@@ -116,53 +133,21 @@ parser.add_argument(
default=None, default=None,
help="Service ID to limit service operations", help="Service ID to limit service operations",
) )
parser.add_argument( parser.add_argument("--stack", "-s", type=str, nargs="+", help="Stack ID for operations")
"--refresh-environment", "-R", action="store_true", help="List endpoints"
)
parser.add_argument(
"--list-endpoints", "-E", action="store_true", help="List endpoints"
)
parser.add_argument("--list-stacks", "-l", action="store_true", help="List stacks")
parser.add_argument("--print-all-data", "-A", action="store_true", help="List stacks")
parser.add_argument(
"--list-containers", "-c", action="store_true", help="List containers"
)
parser.add_argument("--update-stack", "-U", action="store_true", help="Update stacks")
parser.add_argument(
"--stop-containers", "-O", action="store_true", help="Stop containers"
)
parser.add_argument(
"--start-containers", "-X", action="store_true", help="Start containers"
)
parser.add_argument("--update-status", "-S", action="store_true", help="Update status")
parser.add_argument(
"--get-stack", metavar="NAME_OR_ID", help="Get stack by name or numeric id"
)
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"
) )
parser.add_argument("--start-stack", "-x", action="store_true") parser.add_argument("--update", "-u", action="store_true", help="Update service if it exists")
parser.add_argument("--stop-stack", "-o", action="store_true")
parser.add_argument("--secrets", "-q", action="store_true")
parser.add_argument("--debug", "-D", action="store_true") parser.add_argument("--debug", "-D", action="store_true")
parser.add_argument("--create-stack", "-n", action="store_true")
parser.add_argument("--create-stack_new2", "-N", action="store_true")
parser.add_argument("--gpu", "-g", action="store_true") parser.add_argument("--gpu", "-g", action="store_true")
parser.add_argument("--create-stacks", "-C", action="store_true")
parser.add_argument("--refresh-status", "-r", action="store_true")
parser.add_argument("--stack", "-s", type=str, nargs="+", help="Stack ID for operations")
parser.add_argument(
"--token-only", action="store_true", help="Print auth token and exit"
)
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")
args = parser.parse_args() args = parser.parse_args()
print("Running version:", VERSION) print("Running version:", VERSION)
print("Environment:", args.site) print("Environment:", args.site)
args.client = client
if args.site is not None: if args.site is not None:
cur_config["PORTAINER_SITE"] = args.site cur_config["PORTAINER_SITE"] = args.site
if args.endpoint_id is not None: if args.endpoint_id is not None:
@@ -235,10 +220,13 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
longest = len(a) longest = len(a)
for field, text in fields: for field, text in fields:
# print(field)
value_in = getattr(args_in, field) value_in = getattr(args_in, field)
default = defaults_in.get(f"PORTAINER_{field}".upper()) default = defaults_in.get(f"PORTAINER_{field}".upper())
cur_site = defaults_in.get("PORTAINER_SITE".upper()) cur_site = defaults_in.get("PORTAINER_SITE".upper())
cur_env = defaults_in.get("PORTAINER_ENVIRONMENT_ID".upper()) cur_env = defaults_in.get("PORTAINER_ENVIRONMENT_ID".upper())
# print(value_in)
if value_in is None: if value_in is None:
if default is not None: if default is not None:
prompt_text = f"{text} (default={default}) : " prompt_text = f"{text} (default={default}) : "
@@ -256,7 +244,7 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
# input(json.dumps(stacks, indent=2)) # input(json.dumps(stacks, indent=2))
commands = [ commands = [
'authentik', 'bitwarden', 'bookstack', 'dockermon', 'fail2ban', 'gitea', 'gitlab', 'grafana', 'authentik', 'bitwarden', 'bookstack', 'dockermon', 'fail2ban', 'gitea', 'gitlab', 'grafana',
'home-assistant', 'homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'mailu3', 'hashicorp', 'home-assistant', 'homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'mailu3',
'mealie', 'mediacenter', 'mosquitto', 'motioneye', 'n8n', 'nebula', 'nextcloud', 'nginx', 'mealie', 'mediacenter', 'mosquitto', 'motioneye', 'n8n', 'nebula', 'nextcloud', 'nginx',
'node-red', 'octoprint', 'ollama', 'onlyoffice', 'paperless-ngx', 'pihole', 'portainer-ce', 'rancher', 'registry', 'node-red', 'octoprint', 'ollama', 'onlyoffice', 'paperless-ngx', 'pihole', 'portainer-ce', 'rancher', 'registry',
'regsync', 'semaphore', 'unifibrowser', 'uptime-kuma', 'watchtower', 'wazuh', 'webhub', 'wordpress', 'regsync', 'semaphore', 'unifibrowser', 'uptime-kuma', 'watchtower', 'wazuh', 'webhub', 'wordpress',
@@ -383,13 +371,13 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
return args return args
if __name__ == "__main__": if __name__ == "__main__":
# Example usage: set PORTAINER_USER and PORTAINER_PASS in env, or pass literals below. # 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") # token = get_portainer_token(base,"admin","l4c1j4yd33Du5lo") # or get_portainer_token(base, "admin", "secret")
def signal_handler(sig, frame): def signal_handler(sig, frame):
logger.warning("Killed manually %s, %s", sig, frame) logger.warning("Killed manually %s, %s", sig, frame)
print("\nTerminated by user") print("\nTerminated by user")
print("\033[?25h", end="")
sys.exit(0) sys.exit(0)
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
os.system("cls" if os.name == "nt" else "clear") os.system("cls" if os.name == "nt" else "clear")
@@ -438,7 +426,7 @@ if __name__ == "__main__":
os.system("cls" if os.name == "nt" else "clear") os.system("cls" if os.name == "nt" else "clear")
# Example: list endpoints # Example: list endpoints
por = Portainer(cur_config["PORTAINER_SITE"], timeout=args.timeout) por = Portainer(cur_config["PORTAINER_SITE"], args)
por.set_defaults(cur_config) por.set_defaults(cur_config)
if args.debug: if args.debug:
por._debug = True por._debug = True
@@ -548,6 +536,9 @@ if __name__ == "__main__":
sys.exit() sys.exit()
if args.action == "update_service": if args.action == "update_service":
args = prompt_missing_args( args = prompt_missing_args(
args, args,
cur_config, cur_config,
@@ -556,7 +547,7 @@ if __name__ == "__main__":
("endpoint_id", "Endpoint ID") ("endpoint_id", "Endpoint ID")
], ],
) )
por.update_service(args.endpoint_id, args.service_id) por.update_service()
sys.exit() sys.exit()
if args.action == "list_stacks": if args.action == "list_stacks":
args = prompt_missing_args( args = prompt_missing_args(