mirror of
https://gitlab.sectorq.eu/jaydee/portainer.git
synced 2025-12-14 02:34:53 +01:00
Compare commits
7 Commits
101bfbc9a4
...
9be4051720
| Author | SHA1 | Date | |
|---|---|---|---|
| 9be4051720 | |||
| 2319a13554 | |||
| b434887ff9 | |||
| 4c8beff0c8 | |||
| d6842eab62 | |||
| 5864085ec3 | |||
| 04248ce279 |
@@ -7,9 +7,9 @@ variables:
|
||||
GIT_SSH_COMMAND: "ssh -i /home/gitlab-runner/.ssh/id_rsa -o IdentitiesOnly=yes"
|
||||
lint:
|
||||
stage: lint
|
||||
# image: python:3.12
|
||||
image: r.sectorq.eu/jaydee/builder-portainer:latest
|
||||
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"
|
||||
# - echo "PATH is now: $PATH"
|
||||
script:
|
||||
|
||||
141
port.py
141
port.py
@@ -12,6 +12,7 @@ import base64
|
||||
import tabulate
|
||||
from git import Repo
|
||||
import requests
|
||||
import hvac
|
||||
from prompt_toolkit import prompt
|
||||
from prompt_toolkit.completion import WordCompleter
|
||||
from prompt_toolkit.shortcuts import checkboxlist_dialog
|
||||
@@ -27,9 +28,10 @@ class Portainer:
|
||||
to perform API operations.
|
||||
"""
|
||||
|
||||
def __init__(self, site, timeout=10):
|
||||
def __init__(self, site, args=None, timeout=120):
|
||||
self.base_url = None
|
||||
self.token = None
|
||||
self.args = args
|
||||
self.action = None
|
||||
self._debug = False
|
||||
self.timeout = timeout
|
||||
@@ -39,7 +41,7 @@ class Portainer:
|
||||
self.stack_id = None
|
||||
self.stack_ids = []
|
||||
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 = "git@gitlab.sectorq.eu:home/docker-compose.git"
|
||||
@@ -125,10 +127,11 @@ class Portainer:
|
||||
self.base_url = os.getenv(
|
||||
"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":
|
||||
self.base_url = os.getenv("PORTAINER_URL", "https://port.sectorq.eu/api")
|
||||
self.token = "ptr_/5RkMCT/j3BTaL32vMSDtXFi76yOXRKVFOrUtzMsl5Y="
|
||||
else:
|
||||
self.base_url = os.getenv(
|
||||
"PORTAINER_URL", "https://portainer.sectorq.eu/api"
|
||||
@@ -145,6 +148,22 @@ class Portainer:
|
||||
except ValueError:
|
||||
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):
|
||||
url = f"{self.base_url.rstrip('/')}{path}"
|
||||
headers = {"X-API-Key": f"{self.token}"}
|
||||
@@ -275,10 +294,10 @@ class Portainer:
|
||||
|
||||
def get_services(self, endpoint, timeout=30):
|
||||
'''Get a list of services for a specific stack on an endpoint.'''
|
||||
print(json.dumps(self.all_data,indent=2))
|
||||
path = f"/endpoints/{endpoint}/docker/services"
|
||||
print(path)
|
||||
#path += f'?filters={{"label": ["com.docker.compose.project={stack}"]}}'
|
||||
# print(json.dumps(self.all_data,indent=2))
|
||||
path = f"/endpoints/{self.get_endpoint_id(endpoint)}/docker/services"
|
||||
# print(path)
|
||||
# path += f'?filters={{"label": ["com.docker.compose.project={stack}"]}}'
|
||||
services = self._api_get(path, timeout=timeout)
|
||||
return services
|
||||
|
||||
@@ -399,7 +418,11 @@ class Portainer:
|
||||
for s in self.all_data["webhooks"][endpoint]:
|
||||
stcs.append([s, self.all_data["webhooks"][endpoint][s]])
|
||||
else:
|
||||
stcs.append([stack, self.all_data["webhooks"][endpoint][stack]])
|
||||
try:
|
||||
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)
|
||||
def update(c):
|
||||
@@ -724,7 +747,6 @@ class Portainer:
|
||||
}
|
||||
self._api_post_file(path, self.endpoint_id, stack, envs, file)
|
||||
|
||||
|
||||
def print_stacks(self, endpoint="all"):
|
||||
"""Print a table of stacks, optionally filtered by endpoint."""
|
||||
stacks = self.get_stacks()
|
||||
@@ -759,57 +781,66 @@ class Portainer:
|
||||
print(f"Total stacks: {count}")
|
||||
# print(sorted(stack_names))
|
||||
|
||||
def update_service(self, endpoint_id=None, service_id=None):
|
||||
all_services = self.get_services(self.get_endpoint_id(endpoint_id))
|
||||
def update_service(self):
|
||||
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 = sorted(service_tuples, key=lambda x: x[1])
|
||||
service_dict = dict(service_tuples)
|
||||
# input(service_tuples)
|
||||
if self.args.service_id is None:
|
||||
#services = [(s["Id"], s["Name"]) for s in self.get_stacks(endpoint_id)]
|
||||
service_tuples.insert(0, ("__ALL__", "[Select ALL]"))
|
||||
service_tuples.insert(0, ("__ONLY_CHECK__", "[Check Only]"))
|
||||
service_ids = checkboxlist_dialog(
|
||||
title="Select one service",
|
||||
text="Choose a service:",
|
||||
values=service_tuples
|
||||
).run()
|
||||
elif self.args.service_id == "all":
|
||||
service_ids = [s[0] for s in service_tuples if s[0] != "__ALL__" and s[0] != "__ONLY_CHECK__"]
|
||||
else:
|
||||
service_ids = [self.args.service_id]
|
||||
if "__ONLY_CHECK__" in service_ids and self.args.update is False:
|
||||
pull = False
|
||||
else:
|
||||
pull = True
|
||||
if "__ALL__" in service_ids:
|
||||
service_ids = [s[0] for s in service_tuples if s[0] != "__ALL__" and s[0] != "__ONLY_CHECK__"]
|
||||
|
||||
input(service_tuples)
|
||||
if service_id == "all":
|
||||
longest = 0
|
||||
for a in service_dict.items():
|
||||
# print(a[1])
|
||||
if len(a[1]) > longest:
|
||||
longest = len(a[1])
|
||||
#print(longest)
|
||||
ok = "\033[92m✔\033[0m"
|
||||
err = "\033[91m✖\033[0m"
|
||||
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"
|
||||
|
||||
service_id = self.all
|
||||
#services = [(s["Id"], s["Name"]) for s in self.get_stacks(endpoint_id)]
|
||||
services.insert(0, ("__ALL__", "[Select ALL]"))
|
||||
service_ids = checkboxlist_dialog(
|
||||
title="Select one service",
|
||||
text="Choose a service:",
|
||||
values=services
|
||||
).run()
|
||||
if service_ids == "__ALL__":
|
||||
pass
|
||||
print(service_ids)
|
||||
service_dict = dict(service_ids)
|
||||
services = self.get_services(self.endpoint_name, stack_id)
|
||||
svc_name = service_dict.get(stack_id)
|
||||
stack_svcs = []
|
||||
svc_menu = []
|
||||
for s in services:
|
||||
try:
|
||||
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)
|
||||
resp = self._api_get(path, timeout=20)
|
||||
except ValueError as e:
|
||||
print(f"Error restarting service: {e}")
|
||||
return []
|
||||
|
||||
|
||||
service_id = radiolist_dialog(
|
||||
title="Select one service",
|
||||
text="Choose a service:",
|
||||
values=svc_menu
|
||||
).run()
|
||||
|
||||
|
||||
"""Restart a service on an endpoint."""
|
||||
path = f"/docker/{self.endpoint_id}/services/{service_id}/image_status?refresh=true"
|
||||
|
||||
try:
|
||||
resp = self._api_get(path, timeout=20)
|
||||
except ValueError as e:
|
||||
print(f"Error restarting service: {e}")
|
||||
return []
|
||||
if resp['Status'] == "outdated":
|
||||
self.restart_srv(service_id, True)
|
||||
print(f"Service {service_id} : restarted")
|
||||
if resp['Status'] == "outdated":
|
||||
if pull:
|
||||
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
|
||||
|
||||
def restart_srv(self,service_id, pool=False):
|
||||
|
||||
75
portainer.py
75
portainer.py
@@ -14,6 +14,7 @@ import json
|
||||
import argparse
|
||||
import tty
|
||||
import termios
|
||||
import hvac
|
||||
from tabulate import tabulate
|
||||
from port import Portainer
|
||||
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 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"
|
||||
|
||||
defaults = {
|
||||
@@ -34,8 +53,6 @@ defaults = {
|
||||
|
||||
cur_config = {}
|
||||
|
||||
|
||||
|
||||
def load_config(defaults=defaults):
|
||||
'''Load configuration from /myapps/portainer.conf if it exists, else from env vars or defaults.'''
|
||||
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 = [
|
||||
# "PORTAINER_URL",
|
||||
@@ -116,53 +133,21 @@ parser.add_argument(
|
||||
default=None,
|
||||
help="Service ID to limit service operations",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--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("--stack", "-s", type=str, nargs="+", 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"
|
||||
)
|
||||
parser.add_argument("--start-stack", "-x", action="store_true")
|
||||
parser.add_argument("--stop-stack", "-o", action="store_true")
|
||||
parser.add_argument("--secrets", "-q", action="store_true")
|
||||
parser.add_argument("--update", "-u", action="store_true", help="Update service if it exists")
|
||||
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("--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("--deploy-mode", "-m", type=str, default="git", help="Deploy mode")
|
||||
parser.add_argument("--stack-mode", "-w", default=None, help="Stack mode")
|
||||
args = parser.parse_args()
|
||||
print("Running version:", VERSION)
|
||||
print("Environment:", args.site)
|
||||
|
||||
args.client = client
|
||||
if args.site is not None:
|
||||
cur_config["PORTAINER_SITE"] = args.site
|
||||
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)
|
||||
|
||||
for field, text in fields:
|
||||
# print(field)
|
||||
value_in = getattr(args_in, field)
|
||||
default = defaults_in.get(f"PORTAINER_{field}".upper())
|
||||
cur_site = defaults_in.get("PORTAINER_SITE".upper())
|
||||
cur_env = defaults_in.get("PORTAINER_ENVIRONMENT_ID".upper())
|
||||
|
||||
# print(value_in)
|
||||
if value_in is None:
|
||||
if default is not None:
|
||||
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))
|
||||
commands = [
|
||||
'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',
|
||||
'node-red', 'octoprint', 'ollama', 'onlyoffice', 'paperless-ngx', 'pihole', 'portainer-ce', 'rancher', 'registry',
|
||||
'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
|
||||
|
||||
|
||||
|
||||
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")
|
||||
def signal_handler(sig, frame):
|
||||
logger.warning("Killed manually %s, %s", sig, frame)
|
||||
print("\nTerminated by user")
|
||||
print("\033[?25h", end="")
|
||||
sys.exit(0)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
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")
|
||||
# Example: list endpoints
|
||||
por = Portainer(cur_config["PORTAINER_SITE"], timeout=args.timeout)
|
||||
por = Portainer(cur_config["PORTAINER_SITE"], args)
|
||||
por.set_defaults(cur_config)
|
||||
if args.debug:
|
||||
por._debug = True
|
||||
@@ -548,6 +536,9 @@ if __name__ == "__main__":
|
||||
sys.exit()
|
||||
|
||||
if args.action == "update_service":
|
||||
|
||||
|
||||
|
||||
args = prompt_missing_args(
|
||||
args,
|
||||
cur_config,
|
||||
@@ -556,7 +547,7 @@ if __name__ == "__main__":
|
||||
("endpoint_id", "Endpoint ID")
|
||||
],
|
||||
)
|
||||
por.update_service(args.endpoint_id, args.service_id)
|
||||
por.update_service()
|
||||
sys.exit()
|
||||
if args.action == "list_stacks":
|
||||
args = prompt_missing_args(
|
||||
|
||||
Reference in New Issue
Block a user