Compare commits

..

145 Commits

Author SHA1 Message Date
8ba22f79b9 build 2026-01-24 21:07:26 +01:00
ddeb67750f build 2026-01-24 21:05:09 +01:00
12ff88f8e8 build 2026-01-24 21:03:32 +01:00
9d27e804a5 build 2026-01-24 21:02:34 +01:00
1f0a19b7b1 build 2026-01-24 21:00:19 +01:00
5adfbbcf3d build 2026-01-24 20:59:37 +01:00
0dda82be87 build 2026-01-24 20:57:21 +01:00
1a54c1e341 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2026-01-24 20:55:55 +01:00
8d4bd382ee build 2026-01-24 20:55:51 +01:00
ladislav.dusa
96068d4fb3 Merge branch 'main' of https://gitlab.sectorq.eu/jaydee/portainer 2026-01-13 13:22:24 +01:00
ladislav.dusa
de37276ab6 build 2026-01-13 13:22:20 +01:00
2dc800f7f9 build 2026-01-12 23:30:30 +01:00
ae387a794c build 2026-01-09 17:34:31 +01:00
a3518ec0bb build 2026-01-09 14:07:31 +01:00
ladislav.dusa
3e86a75502 build 2026-01-08 09:06:23 +01:00
ladislav.dusa
11cd76215a build 2026-01-08 08:52:35 +01:00
4bbe283211 build 2026-01-06 22:44:38 +01:00
fc3fe7b837 build 2026-01-06 22:02:06 +01:00
3152014ca3 build 2026-01-05 17:45:29 +01:00
e411c81224 build 2026-01-05 17:43:44 +01:00
8ae696a96a build 2026-01-05 17:35:54 +01:00
abd989a0db build 2026-01-05 14:55:37 +01:00
bb8ef3bdb8 build 2025-12-30 22:08:02 +01:00
99aa451620 build 2025-12-30 21:51:26 +01:00
fd1fcf90a4 build 2025-12-27 19:17:53 +01:00
135447d7aa build 2025-12-27 19:04:45 +01:00
164252534e build 2025-12-27 18:48:26 +01:00
807437c47e build 2025-12-23 22:33:08 +01:00
08a15f3bb9 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 22:32:41 +01:00
1f4319f4dd build 2025-12-23 22:32:36 +01:00
9800b01ea2 Update .gitlab-ci.yml file 2025-12-23 22:32:18 +01:00
4cd9cfce20 build 2025-12-23 22:30:57 +01:00
12a095e169 build 2025-12-23 22:23:00 +01:00
162c270c02 build 2025-12-23 22:19:02 +01:00
0c4a91d7ae build 2025-12-23 22:16:52 +01:00
36cb83694c Update .gitlab-ci.yml file 2025-12-23 22:16:34 +01:00
f97cd105ba build 2025-12-23 22:14:56 +01:00
954d5b2dd7 Update .gitlab-ci.yml file 2025-12-23 22:14:35 +01:00
73a68a0f1b build 2025-12-23 22:12:50 +01:00
4598caca89 Update .gitlab-ci.yml file 2025-12-23 22:12:23 +01:00
4d22e77689 build 2025-12-23 22:10:23 +01:00
c2a1a7d115 build 2025-12-23 22:08:48 +01:00
9a79910428 Update .gitlab-ci.yml file 2025-12-23 22:08:27 +01:00
bc69ff6223 build 2025-12-23 22:06:24 +01:00
4e610eea32 build 2025-12-23 22:00:09 +01:00
4e8c0ab3a0 build 2025-12-23 15:08:18 +01:00
b057dfcce4 build 2025-12-23 15:06:21 +01:00
ab15e7c8ea Update .gitlab-ci.yml file 2025-12-23 15:05:56 +01:00
74269b0368 build 2025-12-23 15:01:24 +01:00
45c97d1791 Update .gitlab-ci.yml file 2025-12-23 14:59:37 +01:00
c341c2332f build 2025-12-23 14:58:01 +01:00
d0f2cfc75f Update .gitlab-ci.yml file 2025-12-23 14:57:46 +01:00
c22287f53b build 2025-12-23 13:44:28 +01:00
f618476534 Update .gitlab-ci.yml file 2025-12-23 13:43:58 +01:00
5865df9abc Update .gitlab-ci.yml file 2025-12-23 13:43:42 +01:00
f83ee560c1 Update .gitlab-ci.yml file 2025-12-23 13:32:45 +01:00
5ce8573013 build 2025-12-23 13:19:41 +01:00
e8191802b1 Update .gitlab-ci.yml file 2025-12-23 13:19:07 +01:00
e546d0cf3f build 2025-12-23 13:16:48 +01:00
daf219329a Update .gitlab-ci.yml file 2025-12-23 13:15:47 +01:00
b601ecc0c3 build 2025-12-23 13:13:44 +01:00
1a8e532a02 build 2025-12-23 12:52:18 +01:00
d6e4db6dd4 Update .gitlab-ci.yml file 2025-12-23 12:52:02 +01:00
78012cec65 build 2025-12-23 12:46:55 +01:00
ba098499f5 Update .gitlab-ci.yml file 2025-12-23 12:45:58 +01:00
ce24b5c00d build 2025-12-23 12:45:09 +01:00
46143a7c12 build 2025-12-23 12:39:42 +01:00
c731fbe0de Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 12:39:36 +01:00
d878a2baa0 build 2025-12-23 12:39:32 +01:00
e280ea67f7 Update .gitlab-ci.yml file 2025-12-23 12:39:03 +01:00
260eb63262 build 2025-12-23 12:37:57 +01:00
41d6ec9914 Update .gitlab-ci.yml file 2025-12-23 12:37:44 +01:00
15e442d49b build 2025-12-23 12:35:33 +01:00
14c31575af build 2025-12-23 12:30:03 +01:00
406513b4b8 Update .gitlab-ci.yml file 2025-12-23 12:29:03 +01:00
0c1b624972 Update .gitlab-ci.yml file 2025-12-23 12:25:05 +01:00
db5209e3fb build 2025-12-23 12:17:24 +01:00
111c70ef00 Update .gitlab-ci.yml file 2025-12-23 12:17:07 +01:00
8bba4d1d18 build 2025-12-23 12:10:06 +01:00
db45a48106 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 12:10:00 +01:00
53438a3fb0 build 2025-12-23 12:09:56 +01:00
5646e0692d Update .gitlab-ci.yml file 2025-12-23 12:08:17 +01:00
6634cc20fa build 2025-12-23 12:05:41 +01:00
fc9f25a203 Update .gitlab-ci.yml file 2025-12-23 12:05:25 +01:00
8351e9f1b1 Update .gitlab-ci.yml file 2025-12-23 12:05:13 +01:00
d007510704 build 2025-12-23 12:03:42 +01:00
3e202c9fd8 build 2025-12-23 11:55:22 +01:00
d6e5c4087d build 2025-12-23 11:53:06 +01:00
f70ebecc49 build 2025-12-23 11:41:50 +01:00
b8264994c5 Update .gitlab-ci.yml file 2025-12-23 11:41:37 +01:00
6081c44d4c build 2025-12-23 11:40:49 +01:00
abb5fc7708 Update .gitlab-ci.yml file 2025-12-23 11:40:31 +01:00
a9eecac96d build 2025-12-23 11:35:19 +01:00
e7bcee762d build 2025-12-23 11:33:51 +01:00
e3fed3304a Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 11:33:46 +01:00
338aa66565 build 2025-12-23 11:33:42 +01:00
058554a0ea Update .gitlab-ci.yml file 2025-12-23 11:33:19 +01:00
b28a7c8273 build 2025-12-23 11:27:57 +01:00
e464c498ff build 2025-12-23 11:22:46 +01:00
b028a48fc0 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 11:22:40 +01:00
b2373f7016 build 2025-12-23 11:22:36 +01:00
1b50b3337c Update .gitlab-ci.yml file 2025-12-23 11:22:09 +01:00
ac68b5be6f build 2025-12-23 11:20:43 +01:00
035abffeab Update .gitlab-ci.yml file 2025-12-23 11:20:29 +01:00
6097d2b442 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-23 11:19:44 +01:00
55bdff1745 build 2025-12-23 11:19:30 +01:00
b385f3db12 Update .gitlab-ci.yml file 2025-12-23 11:18:49 +01:00
94a3ccfd23 Update .gitlab-ci.yml file 2025-12-23 11:18:03 +01:00
b0c570d7ba build 2025-12-23 11:16:56 +01:00
4339a7d769 build 2025-12-23 10:59:28 +01:00
039078191f build 2025-12-23 10:56:22 +01:00
f5d76d87e0 build 2025-12-23 09:55:31 +01:00
f561508d2e build 2025-12-21 00:52:21 +01:00
974966fdd8 build 2025-12-21 00:49:24 +01:00
63e158899e build 2025-12-20 15:22:59 +01:00
9336b56f96 build 2025-12-20 15:22:40 +01:00
66fba7b994 build 2025-12-20 15:20:29 +01:00
7804dbb117 build 2025-12-20 15:19:00 +01:00
fb1763e14d build 2025-12-20 15:16:40 +01:00
829891d1ba build 2025-12-20 15:13:14 +01:00
174aab4faa build 2025-12-19 17:23:20 +01:00
9c6445ee03 build 2025-12-19 17:08:15 +01:00
d215dd2615 build 2025-12-17 00:23:14 +01:00
0cbbb443f2 build 2025-12-17 00:10:12 +01:00
91702d8f4a build 2025-12-17 00:08:22 +01:00
4884ba41c4 build 2025-12-17 00:07:18 +01:00
ec47d331d7 build 2025-12-17 00:03:46 +01:00
cc6dc31409 build 2025-12-17 00:01:37 +01:00
609da77e77 build 2025-12-16 23:57:44 +01:00
17daad941e build 2025-12-16 23:55:58 +01:00
cd32ad1d3e build 2025-12-16 23:51:41 +01:00
45a0b0030c build 2025-12-16 23:49:36 +01:00
5d0b488b87 build 2025-12-16 23:47:04 +01:00
5df457bdcb build 2025-12-16 23:43:23 +01:00
8f207f2fad build 2025-12-16 23:41:19 +01:00
c0682d478d build 2025-12-16 23:35:45 +01:00
db1710e065 build 2025-12-16 23:34:22 +01:00
7bac5e84c8 build 2025-12-16 23:31:42 +01:00
3a0117c2a5 build 2025-12-16 23:29:18 +01:00
8b916572cb build 2025-12-16 23:28:03 +01:00
498b88c7ee build 2025-12-16 23:26:43 +01:00
497a1f7947 build 2025-12-16 23:23:58 +01:00
928e4daae6 build 2025-12-16 23:20:52 +01:00
926248fda7 build 2025-12-16 22:44:49 +01:00
ab9e93effe build 2025-12-16 21:28:23 +01:00
6 changed files with 183 additions and 113 deletions

View File

@@ -1,3 +1,4 @@
stages: # List of stages for jobs, and their order of execution stages: # List of stages for jobs, and their order of execution
- lint - lint
- build - build
@@ -26,7 +27,10 @@ build-job: # This job runs in the build stage, which runs first.
- mkdir -p ~/.ssh - mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa - echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa
- pyinstaller --onefile portainer.py - pip install uuid
#- pyinstaller --onefile --add-data "port.py:." portainer.py
- rm -rf build dist *.spec
- pyinstaller --onefile --clean -n portainer main.py
#- scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.80.222:/myapps/bin/ || true #- scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.80.222:/myapps/bin/ || true
- scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.77.12:/myapps/bin/ || true - scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.77.12:/myapps/bin/ || true
- scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.77.101:/myapps/bin/ || true - scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.77.101:/myapps/bin/ || true
@@ -46,13 +50,11 @@ clean-job: # This job runs in the build stage, which runs first.
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp - rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
rules: rules:
- if: '$CI_COMMIT_MESSAGE =~ /build/' - if: '$CI_COMMIT_MESSAGE =~ /build/'
cleanup_on_failure_job: cleanup_on_failure_job:
stage: clean # Should be in a later stage than the job that might fail stage: clean # Should be in a later stage than the job that might fail
when: on_failure # <-- This is the key keyword when: on_failure # <-- This is the key keyword
script: script:
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp - rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
notify: notify:
stage: notify # Should be in a later stage than the job that might fail stage: notify # Should be in a later stage than the job that might fail
when: on_success # <-- This is the key keyword when: on_success # <-- This is the key keyword
@@ -61,7 +63,6 @@ notify:
- echo "${flow_id}" - echo "${flow_id}"
- curl -XPOST http://192.168.77.101:8123/api/webhook/voice-notifications-tC_8YKxMJIAaQRV5riKuC7Zl --data-raw 'message=portainer build job completed' - curl -XPOST http://192.168.77.101:8123/api/webhook/voice-notifications-tC_8YKxMJIAaQRV5riKuC7Zl --data-raw 'message=portainer build job completed'
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp - rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
notify2: notify2:
stage: notify # Should be in a later stage than the job that might fail stage: notify # Should be in a later stage than the job that might fail
when: on_failure # <-- This is the key keyword when: on_failure # <-- This is the key keyword
@@ -72,4 +73,3 @@ notify2:
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp - rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
rules: rules:
- if: '$CI_COMMIT_MESSAGE =~ /build/' - if: '$CI_COMMIT_MESSAGE =~ /build/'

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.defaultInterpreterPath": "../../venvs/portainer/bin/python"
}

View File

@@ -5,24 +5,29 @@ This module provides a wrapper for interacting with the Portainer API
to manage endpoints, stacks, and containers. to manage endpoints, stacks, and containers.
""" """
# !/myapps/venvs/portainer/bin/python3
import os import os
import logging import logging
import signal import signal
import sys import sys
import json import json
import argparse import argparse
import tty
import termios
import hvac import hvac
import time
import base64
import shutil
import requests
from portainer.api import PortainerApi
from git import Repo
from concurrent.futures import ThreadPoolExecutor
from tabulate import tabulate from tabulate import tabulate
from port import Portainer
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
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")
# VAULT_ADDR = os.environ.get("VAULT_ADDR", "http://192.168.77.101:8200")
VAULT_ADDR = os.environ.get("VAULT_ADDR", "https://vault.sectorq.eu")
try: try:
VAULT_TOKEN = os.environ.get("VAULT_TOKEN") VAULT_TOKEN = os.environ.get("VAULT_TOKEN")
if VAULT_TOKEN is None: if VAULT_TOKEN is None:
@@ -39,7 +44,7 @@ else:
raise Exception("Failed to authenticate with Vault") raise Exception("Failed to authenticate with Vault")
# Specify the mount point of your KV engine # Specify the mount point of your KV engine
VERSION = "0.1.14" VERSION = "0.1.55"
defaults = { defaults = {
"endpoint_id": "vm01", "endpoint_id": "vm01",
@@ -83,10 +88,6 @@ def load_config(defaults=defaults):
print("Configuration written to /myapps/portainer.conf") print("Configuration written to /myapps/portainer.conf")
return cur_config return cur_config
a = load_config(defaults) a = load_config(defaults)
# ENV_VARS = [ # ENV_VARS = [
@@ -109,7 +110,11 @@ def update_configs(cur_config):
print("Configuration written to /myapps/portainer.conf") print("Configuration written to /myapps/portainer.conf")
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Portainer helper - use env vars or pass credentials." description=f"""\
Portainer helper - use env vars or pass credentials."
version: {VERSION}
""",
formatter_class=argparse.RawTextHelpFormatter,
) )
parser.add_argument( parser.add_argument(
"--base", "--base",
@@ -139,10 +144,12 @@ parser.add_argument(
) )
parser.add_argument("--update", "-u", action="store_true", help="Update service if it exists") 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("--debug", "-D", action="store_true")
parser.add_argument("--launcher", "-L", action="store_true")
parser.add_argument("--gpu", "-g", action="store_true") 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")
args = parser.parse_args() args = parser.parse_args()
print("Running version:", VERSION) print("Running version:", VERSION)
print("Environment:", args.site) print("Environment:", args.site)
@@ -163,7 +170,7 @@ update_configs(cur_config)
if args.debug: if args.debug:
input(cur_config) input(cur_config)
_LOG_LEVEL = "INFO" _LOG_LEVEL = "DEBUG"
LOG_FILE = "/tmp/portainer.log" LOG_FILE = "/tmp/portainer.log"
if _LOG_LEVEL == "DEBUG": if _LOG_LEVEL == "DEBUG":
logging.basicConfig( logging.basicConfig(
@@ -242,11 +249,11 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
if args.action == "create_stack": if args.action == "create_stack":
# 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', 'duplicati', 'fail2ban', 'gitea', 'gitlab', 'grafana', 'grocy',
'hashicorp', 'home-assistant', 'homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'mailu3', 'hashicorp', 'home-assistant', 'homebox','homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'kopia', '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', 'searxng','semaphore', 'unifibrowser', 'uptime-kuma', 'watchtower', 'wazuh', 'webhub', 'wordpress',
'wud', 'zabbix-server'] '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())
@@ -372,7 +379,7 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
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")
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")
@@ -403,18 +410,13 @@ if __name__ == "__main__":
] ]
selected_action = radiolist_dialog( selected_action = radiolist_dialog(
title="Select one service", title=f"Select one service - version: {VERSION}",
text="Choose a service:", text="Choose a service:",
values=actions values=actions
).run() ).run()
print("Selected:", selected_action) print("Selected:", selected_action)
# print("Possible actions: \n") # print("Possible actions: \n")
# i = 1 # i = 1
# for a in actions: # for a in actions:
@@ -426,7 +428,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"], args) por = PortainerApi(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
@@ -547,6 +549,8 @@ if __name__ == "__main__":
], ],
) )
por.update_service() por.update_service()
if args.launcher:
input("\nPress ENTER to continue...")
sys.exit() sys.exit()
if args.action == "update_containers": if args.action == "update_containers":
@@ -574,12 +578,24 @@ if __name__ == "__main__":
], ],
) )
por.print_stacks(args) por.print_stacks(args)
if args.launcher:
input("Press ENTER to continue...")
# print(json.dumps(por.all_data, indent=2)) # print(json.dumps(por.all_data, indent=2))
sys.exit() sys.exit()
if args.action == "list_containers": if args.action == "list_containers":
print("Getting containers") print("Getting containers")
print(por.get_containers()) args = prompt_missing_args(
args,
cur_config,
[
("site", "Site"),
("endpoint_id", "Endpoint ID"),
],
)
print("\n".join(por.get_containers()))
if args.launcher:
input("\nPress ENTER to continue...")
sys.exit() sys.exit()
if args.action == "update_stack": if args.action == "update_stack":
@@ -593,11 +609,16 @@ if __name__ == "__main__":
) )
por.update_stack(args) por.update_stack(args)
if args.launcher:
input("\nPress ENTER to continue...")
sys.exit() 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:
input("\nPress ENTER to continue...")
sys.exit() 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() sys.exit()
@@ -609,10 +630,20 @@ if __name__ == "__main__":
export_data.append([i, eps["by_id"][i]]) export_data.append([i, eps["by_id"][i]])
headers = ["EndpointId", "Name"] headers = ["EndpointId", "Name"]
print(tabulate(export_data, headers=headers, tablefmt="github")) print(tabulate(export_data, headers=headers, tablefmt="github"))
if args.launcher:
input("\nPress ENTER to continue...")
sys.exit() sys.exit()
if args.action == "stop_containers": if args.action == "stop_containers":
# TODO: does not work
args = prompt_missing_args(
args,
cur_config,
[
("site", "Site"),
("endpoint_id", "Endpoint ID"),
],
)
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() sys.exit()
@@ -647,8 +678,4 @@ if __name__ == "__main__":
sys.exit() sys.exit()
if args.action == "refresh_status": if args.action == "refresh_status":
if args.stack == "all": por.refresh_status(args)
print("Stopping all stacks...")
stcks = por.get_stacks(endpoint_id=args.endpoint_id)
else:
por.refresh_status(args.stack_id)

0
portainer/__init__.py Normal file
View File

View File

@@ -21,7 +21,7 @@ from prompt_toolkit.shortcuts import radiolist_dialog
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Portainer: class PortainerApi:
""" """
Simple wrapper around the module-level Portainer helper functions. Simple wrapper around the module-level Portainer helper functions.
Instantiate with base_url and optional token/timeout and call methods Instantiate with base_url and optional token/timeout and call methods
@@ -141,6 +141,13 @@ class Portainer:
self.get_endpoints() self.get_endpoints()
self.get_stacks() self.get_stacks()
def refresh_status(self, args):
for s in self.all_data['stacks']['m-s']['by_id']:
path = f'/stacks/{s}/images_status?refresh=true'
print(path)
res = self._api_get(path, timeout=args.timeout)
def _is_number(self, s): def _is_number(self, s):
"""Check if the input string is a number.""" """Check if the input string is a number."""
try: try:
@@ -159,8 +166,9 @@ class Portainer:
response = requests.post( response = requests.post(
"https://gotify.sectorq.eu/message", "https://gotify.sectorq.eu/message",
data=payload, data=payload,
headers={"X-Gotify-Key": "ASn_fIAd5OVjm8c"} headers={"X-Gotify-Key": "A1krRuo8GIW-fpY"}
) )
logger.debug(response.text)
# print("Status:", response.status_code) # print("Status:", response.status_code)
# print("Response:", response.text) # print("Response:", response.text)
pass pass
@@ -314,6 +322,7 @@ class Portainer:
def get_endpoint_id(self): def get_endpoint_id(self):
'''Get endpoint ID from either ID or name input.''' '''Get endpoint ID from either ID or name input.'''
# input(self.args.endpoint_id)
if self._is_number(self.args.endpoint_id): if self._is_number(self.args.endpoint_id):
self.endpoint_id = self.args.endpoint_id self.endpoint_id = self.args.endpoint_id
self.endpoint_name = self.endpoints["by_id"][self.args.endpoint_id] self.endpoint_name = self.endpoints["by_id"][self.args.endpoint_id]
@@ -341,7 +350,6 @@ class Portainer:
# print(stack) # print(stack)
cont = [] cont = []
data = {} data = {}
eps = [ep for ep in self.all_data['endpoints']['by_id'].keys()] eps = [ep for ep in self.all_data['endpoints']['by_id'].keys()]
#input(eps) #input(eps)
for endpoint in eps: for endpoint in eps:
@@ -360,11 +368,15 @@ class Portainer:
print(f"failed to get containers from {path}: {e}") print(f"failed to get containers from {path}: {e}")
continue continue
contr = [] contr = []
# print(f"Containers: {containers}")
try: try:
for c in containers: for c in containers:
#input(c) # print(c)
cont.append([c["Names"][0].replace("/", ""),c["Id"], c['Image']]) try:
contr.append([c["Names"][0].replace("/", ""), c["Id"], c['Image']]) cont.append([c["Names"][0].replace("/", ""),c["Id"], c['Image']])
contr.append([c["Names"][0].replace("/", ""), c["Id"], c['Image']])
except:
print("Unable to parse container info")
if self.all_data["endpoints"]["by_id"][endpoint] in data: if self.all_data["endpoints"]["by_id"][endpoint] in data:
data[self.all_data["endpoints"]["by_id"][endpoint]] = contr data[self.all_data["endpoints"]["by_id"][endpoint]] = contr
data[endpoint] = contr data[endpoint] = contr
@@ -393,14 +405,13 @@ class Portainer:
else: else:
eps = [self.get_endpoint_id()] eps = [self.get_endpoint_id()]
#input(eps)
for endpoint in eps:
# print(s) for endpoint in eps:
#print(self.args.stack) #print(self.args.stack)
if self.args.stack in ["all", None]: if self.args.stack in ["all", None]:
# input([id for id in self.all_data["stacks"][endpoint]['by_id'].keys()]) # input([id for id in self.all_data["stacks"][endpoint]['by_id'].keys()])
for s in [id for id in self.all_data["stacks"][endpoint]['by_id'].keys()]: for e in [id for id in self.all_data["stacks"][endpoint]['by_name'].keys()]:
#input(e)
# if s not in self.all_data["stacks"]: # if s not in self.all_data["stacks"]:
# continue # continue
#input(self.all_data) #input(self.all_data)
@@ -408,36 +419,36 @@ class Portainer:
# print(f"Endpoint {self.all_data["endpoints"]["by_id"][s]} is offline") # print(f"Endpoint {self.all_data["endpoints"]["by_id"][s]} is offline")
continue continue
# input(self.all_data["stacks"][endpoint]["by_name"]) # input(self.all_data["stacks"][endpoint]["by_name"])
for e in self.all_data["stacks"][endpoint]["by_name"]:
#input(e) #input(e)
path = ( path = (
f"/endpoints/{endpoint}/docker/containers/json" f"/endpoints/{endpoint}/docker/containers/json"
f'?all=1&filters={{"label": ["com.docker.compose.project={e}"]}}' f'?all=1&filters={{"label": ["com.docker.compose.project={e}"]}}'
)
logging.info(f"request : {path}")
try:
containers = self._api_get(path)
#input(containers)
except Exception as e:
print(f"failed to get containers from {path}: {e}")
continue
contr = []
try:
for c in containers:
# input(c)
cont.append(c["Names"][0].replace("/", ""))
contr.append(c["Names"][0].replace("/", ""))
if self.all_data["endpoints"]["by_id"][endpoint] in data:
data[self.all_data["endpoints"]["by_id"][endpoint]][e] = contr
else:
data[self.all_data["endpoints"]["by_id"][endpoint]] = {
e: contr
}
except Exception as e:
logger.debug(
f"Exception while getting containers for stack {e} ",
f"on endpoint {self.all_data['endpoints']['by_id'][endpoint]}: {e}",
) )
logging.info(f"request : {path}")
try:
containers = self._api_get(path)
#input(containers)
except Exception as e:
print(f"failed to get containers from {path}: {e}")
continue
contr = []
try:
for c in containers:
# input(c)
cont.append(c["Names"][0].replace("/", ""))
contr.append(c["Names"][0].replace("/", ""))
if self.all_data["endpoints"]["by_id"][endpoint] in data:
data[self.all_data["endpoints"]["by_id"][endpoint]][e] = contr
else:
data[self.all_data["endpoints"]["by_id"][endpoint]] = {
e: contr
}
except Exception as e:
logger.debug(
f"Exception while getting containers for stack {e} ",
f"on endpoint {self.all_data['endpoints']['by_id'][endpoint]}: {e}",
)
self.all_data["containers"] = data self.all_data["containers"] = data
@@ -488,7 +499,7 @@ class Portainer:
stacks_tuples.append((s['Webhook'],s['Name'])) stacks_tuples.append((s['Webhook'],s['Name']))
# print(s['Name'], " : ", s['Webhook']) # print(s['Name'], " : ", s['Webhook'])
stacks_dict = dict(stacks_tuples) stacks_dict = dict(stacks_tuples)
print(stacks_dict) # print(stacks_dict)
#input(stacks_tuples) #input(stacks_tuples)
# stacks_tuples = [(s['AutoUpdate']['Webhook'], s['Name']) for s in stacks if "Webhook" in s['AutoUpdate'] ] # stacks_tuples = [(s['AutoUpdate']['Webhook'], s['Name']) for s in stacks if "Webhook" in s['AutoUpdate'] ]
@@ -513,7 +524,7 @@ class Portainer:
values=stacks_tuples values=stacks_tuples
).run() ).run()
stcs = [] stcs = []
input(stack_ids) #input(stack_ids)
if args.stack == "all": if args.stack == "all":
for s in stack_dict: for s in stack_dict:
@@ -523,18 +534,18 @@ class Portainer:
if s in stack_ids: if s in stack_ids:
stcs.append([s, stack_dict[s]]) stcs.append([s, stack_dict[s]])
print(stcs) # print(stcs)
with ThreadPoolExecutor(max_workers=10) as exe: with ThreadPoolExecutor(max_workers=10) as exe:
list(exe.map(update, stcs)) list(exe.map(update, stcs))
input('UPDATED') #input('UPDATED')
if not args.autostart: if not args.autostart:
time.sleep(120) time.sleep(120)
cont = [] cont = []
for c in self.all_data["containers"][endpoint]: for c in self.all_data["containers"][args.endpoint_id]:
if stack == c or stack == "all": if args.stack == c or args.stack == "all":
cont += self.all_data["containers"][endpoint][c] cont += self.all_data["containers"][args.endpoint_id][c]
self.stop_containers(endpoint, cont) self.stop_containers(args.endpoint_id, cont)
def get_endpoints(self, timeout=10): def get_endpoints(self, timeout=10):
'''Get a list of all endpoints.''' '''Get a list of all endpoints.'''
@@ -833,7 +844,7 @@ 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, args):
"""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()
count = 0 count = 0
@@ -841,11 +852,11 @@ class Portainer:
stack_names = [] stack_names = []
for stack in stacks: for stack in stacks:
# print(stack) # print(stack)
if endpoint is not None: if args.endpoint_id is not None:
if not stack["EndpointId"] in self.endpoints["by_id"]: if not stack["EndpointId"] in self.endpoints["by_id"]:
continue continue
if endpoint != "all": if args.endpoint_id != "all":
if self.endpoints["by_name"][endpoint] != stack["EndpointId"]: if self.endpoints["by_name"][args.endpoint_id] != stack["EndpointId"]:
continue continue
try: try:
stack_names.append(stack["Name"]) stack_names.append(stack["Name"])
@@ -872,7 +883,7 @@ class Portainer:
def update_containers(self): def update_containers(self):
all_containers = self.all_data["containers"][self.args.endpoint_id] all_containers = self.all_data["containers"][self.args.endpoint_id]
#input(all_containers) #input(all_containers)
service_tuples = [(s[1], s[0]) for s in all_containers if "." not in s[0]] service_tuples = [(s[1], s[0]) for s in all_containers if "." not in s[0] and not s[0].startswith("runner-")]
service_tuples = sorted(service_tuples, key=lambda x: x[1]) service_tuples = sorted(service_tuples, key=lambda x: x[1])
service_dict = dict(service_tuples) service_dict = dict(service_tuples)
# input(service_tuples) # input(service_tuples)
@@ -886,15 +897,21 @@ class Portainer:
values=service_tuples values=service_tuples
).run() ).run()
elif self.args.service_id == "all": elif self.args.service_id == "all":
service_ids = [s[0] for s in service_tuples if s[0] != "__ALL__" and s[0] != "__ONLY_CHECK__"] service_ids = [s[0] for s in service_tuples if s[0] != "__ALL__" ]
else: else:
service_ids = [self.args.service_id] service_ids = [self.args.service_id]
if "__ONLY_CHECK__" in service_ids or self.args.update is False:
pull = False if self.args.update is False:
print("Checking for updates only...") if "__ONLY_CHECK__" in service_ids:
service_ids.remove("__ONLY_CHECK__")
pull = False
print("Checking for updates only...")
else:
pull = True
print("Checking for updates and pulling updates...")
else: else:
print("Checking for updates and pulling updates...")
pull = True pull = True
print("Checking for updates and pulling updates...")
if "__ALL__" in service_ids: if "__ALL__" in service_ids:
service_ids = [s[0] for s in service_tuples if s[0] != "__ALL__" and s[0] != "__ONLY_CHECK__"] service_ids = [s[0] for s in service_tuples if s[0] != "__ALL__" and s[0] != "__ONLY_CHECK__"]
@@ -906,6 +923,7 @@ class Portainer:
#print(longest) #print(longest)
ok = "\033[92m✔\033[0m" ok = "\033[92m✔\033[0m"
err = "\033[91m✖\033[0m" err = "\033[91m✖\033[0m"
updates = []
for service_id in service_ids: for service_id in service_ids:
# print(self.all_data["containers"][self.args.endpoint_id]) # print(self.all_data["containers"][self.args.endpoint_id])
@@ -923,10 +941,10 @@ class Portainer:
print("?") print("?")
elif resp['Status'] == "outdated": elif resp['Status'] == "outdated":
if pull: if pull:
#print("Recreate")
self.recreate_container(service_id, pull) self.recreate_container(service_id, pull)
#print(f"Service {service_dict[service_id]:<{longest}} : updated") #print(f"Service {service_dict[service_id]:<{longest}} : updated")
self.gotify_message(f"Service {service_dict[service_id]} updated") updates.append(service_dict[service_id])
print(ok, end=" ") print(ok, end=" ")
for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]: for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]:
if name.startswith(service_dict[service_id]): if name.startswith(service_dict[service_id]):
@@ -934,7 +952,7 @@ class Portainer:
else: else:
print(f"\r\033[4m{service_dict[service_id]:<{longest}}\033[0m ", end="", flush=True) 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") #print(f"\033[4m{service_dict[service_id]:<{longest}} {err}\033[0m")
self.gotify_message(f"Service update available for {service_dict[service_id]}") updates.append(service_dict[service_id])
print(err, end=" ") print(err, end=" ")
for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]: for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]:
if name.startswith(service_dict[service_id]): if name.startswith(service_dict[service_id]):
@@ -944,12 +962,24 @@ class Portainer:
for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]: for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]:
if name.startswith(service_dict[service_id]): if name.startswith(service_dict[service_id]):
print(image) print(image)
if len(updates) > 0:
if pull:
self.gotify_message(f"Services updated: {', '.join(updates)}")
else:
self.gotify_message(f"Services updates available: {', '.join(updates)}")
print("\033[?25h", end="") print("\033[?25h", end="")
return True return True
def update_service(self): def update_service(self):
all_services = self.get_services(self.get_endpoint_id()) all_services = self.get_services(self.get_endpoint_id())
#input(all_services) if self.args.debug:
print(all_services)
if all_services == 503:
print("No services found on this endpoint.")
return False
if len(all_services) == 0:
print("No services found on this endpoint.")
return False
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]) service_tuples = sorted(service_tuples, key=lambda x: x[1])
service_dict = dict(service_tuples) service_dict = dict(service_tuples)
@@ -963,25 +993,30 @@ class Portainer:
text="Choose a service:", text="Choose a service:",
values=service_tuples values=service_tuples
).run() ).run()
if "__ONLY_CHECK__" in service_ids:
self.args.update = False
else:
self.args.update = True
if "__ALL__" in service_ids:
service_ids = [s[0] for s in service_tuples if s[0] != "__ALL__" and s[0] != "__ONLY_CHECK__"]
elif self.args.service_id == "all": elif self.args.service_id == "all":
service_ids = [s[0] for s in service_tuples if s[0] != "__ALL__" and s[0] != "__ONLY_CHECK__"] service_ids = [s[0] for s in service_tuples if s[0] != "__ALL__" and s[0] != "__ONLY_CHECK__"]
else: else:
service_ids = [self.args.service_id] service_ids = [self.args.service_id]
if self.args.update is False:
if "__ONLY_CHECK__" in service_ids: if self.args.update:
pull = False
print("Checking for updates only...")
else:
pull = True
print("Checking for updates and pulling updates...")
else:
pull = True pull = True
print("Checking for updates and pulling updates...") print("Checking for updates and pulling updates...")
if "__ALL__" in service_ids: else:
service_ids = [s[0] for s in service_tuples if s[0] != "__ALL__" and s[0] != "__ONLY_CHECK__"] pull = False
print("Checking for updates only...")
longest = 0 longest = 0
for a in service_dict.items(): for a in service_dict.items():
if a[0] == "__ONLY_CHECK__":
continue
# print(a[1]) # print(a[1])
if len(a[1]) > longest: if len(a[1]) > longest:
longest = len(a[1]) longest = len(a[1])
@@ -1004,7 +1039,7 @@ class Portainer:
self.restart_srv(service_id, pull) self.restart_srv(service_id, pull)
#print(f"Service {service_dict[service_id]:<{longest}} : updated") #print(f"Service {service_dict[service_id]:<{longest}} : updated")
self.gotify_message(f"Service {service_dict[service_id]} updated") self.gotify_message(f"Service {service_dict[service_id]} updated")
print(ok) print(f"{ok} updated")
else: else:
print(f"\r\033[4m{service_dict[service_id]:<{longest}}\033[0m ", end="", flush=True) 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") #print(f"\033[4m{service_dict[service_id]:<{longest}} {err}\033[0m")
@@ -1012,6 +1047,7 @@ class Portainer:
print(err) print(err)
else: else:
print(ok) print(ok)
self.gotify_message(f"Service update process finished")
print("\033[?25h", end="") print("\033[?25h", end="")
return True return True
@@ -1082,10 +1118,10 @@ class Portainer:
def recreate_container(self,service_id, pull=False): def recreate_container(self,service_id, pull=False):
"""Restart a service on an endpoint.""" """Restart a service on an endpoint."""
path = f"/docker/{self.endpoint_id}/containers/{service_id}/recreate" path = f"/docker/{self.endpoint_id}/containers/{service_id}/recreate"
#print(path) # print(path)
params={"pullImage": pull} params={"pullImage": pull}
try: try:
resp = self._api_post(path, json=params, timeout=20) resp = self._api_post(path, json=params, timeout=120)
#print(resp) #print(resp)
except ValueError as e: except ValueError as e:
print(f"Error restarting service: {e}") print(f"Error restarting service: {e}")
@@ -1097,7 +1133,7 @@ class Portainer:
params={"serviceID": service_id, "pullImage": pool} params={"serviceID": service_id, "pullImage": pool}
try: try:
resp = self._api_put(path, json=params, timeout=20) resp = self._api_put(path, json=params, timeout=20)
print(resp) # print(resp)
except ValueError as e: except ValueError as e:
print(f"Error restarting service: {e}") print(f"Error restarting service: {e}")
return [] return []
@@ -1339,4 +1375,5 @@ class Portainer:
path = f"/endpoints/{endpoint_id}/docker/secrets/create" path = f"/endpoints/{endpoint_id}/docker/secrets/create"
encoded = base64.b64encode(value.encode()).decode() encoded = base64.b64encode(value.encode()).decode()
data = {"Name": name, "Data": encoded} data = {"Name": name, "Data": encoded}
return self._api_post(path, data, timeout=timeout) return self._api_post(path, data, timeout=timeout)

View File

@@ -5,3 +5,6 @@ tabulate
flake8 flake8
pylint pylint
black black
docker
hvac
prompt_toolkit