Compare commits

..

113 Commits

Author SHA1 Message Date
e271e9bf81 build 2025-12-05 22:14:12 +01:00
15f8da3b64 build 2025-12-05 22:09:21 +01:00
23c5758e3c build 2025-12-05 21:58:49 +01:00
bd185a22ce Update .gitlab-ci.yml file 2025-12-05 21:58:02 +01:00
e92f3b8467 build 2025-12-05 21:54:22 +01:00
cf90c7b7b3 Update .gitlab-ci.yml file 2025-12-05 21:53:57 +01:00
1f19fedbca build 2025-12-05 21:52:44 +01:00
7f043a61ab Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-05 21:52:27 +01:00
47e3a665b8 build 2025-12-05 21:52:25 +01:00
734ab945ab Update .gitlab-ci.yml file 2025-12-05 21:51:43 +01:00
822626c5f3 Update .gitlab-ci.yml file 2025-12-05 21:47:12 +01:00
aecbc7731a build 2025-12-05 21:46:17 +01:00
d52711e1f5 build 2025-12-05 21:41:27 +01:00
24a75740fe Update .gitlab-ci.yml file 2025-12-05 21:41:10 +01:00
8fb39ddfce Update .gitlab-ci.yml file 2025-12-05 21:40:20 +01:00
530be46a61 Update .gitlab-ci.yml file 2025-12-05 21:15:56 +01:00
67638b2c1b build 2025-12-05 21:12:36 +01:00
41e5ff5709 Update .gitlab-ci.yml file 2025-12-05 21:11:40 +01:00
dfe7298bb2 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-05 21:09:29 +01:00
c790349b6b build 2025-12-05 21:09:25 +01:00
bb57a3827d Update .gitlab-ci.yml file 2025-12-05 21:09:08 +01:00
6f9b9c7605 build 2025-12-05 17:12:37 +01:00
276044614d build 2025-12-05 17:10:57 +01:00
45900138fc build 2025-12-05 17:09:05 +01:00
19a91c17c6 build 2025-12-05 12:36:04 +01:00
aba67dc90d build 2025-12-05 12:33:59 +01:00
bc64e5d023 build 2025-12-05 10:39:09 +01:00
269292a8d3 build 2025-12-05 07:36:54 +01:00
cca115b33c build 2025-12-05 07:05:58 +01:00
c96ffc4ee6 build 2025-12-04 23:53:46 +01:00
cb196091c7 build 2025-12-04 23:45:47 +01:00
0ad11f1ae3 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-04 23:45:31 +01:00
94bd230fd1 build 2025-12-04 23:45:28 +01:00
79ea47836b Update .gitlab-ci.yml file 2025-12-04 23:45:06 +01:00
9f8adb8617 build 2025-12-04 23:42:55 +01:00
d42f7dba0c Update .gitlab-ci.yml file 2025-12-04 23:42:26 +01:00
f06abe7abb Update .gitlab-ci.yml file 2025-12-04 23:42:03 +01:00
de9ce2a566 Update .gitlab-ci.yml file 2025-12-04 23:35:17 +01:00
10299b61e6 Update .gitlab-ci.yml file 2025-12-04 23:33:48 +01:00
fb3d0b5998 Update .gitlab-ci.yml file 2025-12-04 23:32:13 +01:00
7fc3aa25c0 Update .gitlab-ci.yml file 2025-12-04 23:31:44 +01:00
78cba2b1e9 Update .gitlab-ci.yml file 2025-12-04 23:28:31 +01:00
9ac6b48062 Update .gitlab-ci.yml file 2025-12-04 23:27:37 +01:00
ea5ae5f3a6 Update .gitlab-ci.yml file 2025-12-04 23:27:23 +01:00
0af3938aee Update .gitlab-ci.yml file 2025-12-04 23:26:06 +01:00
14932c182b build 2025-12-04 23:24:11 +01:00
e1aaa72f13 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-04 23:24:04 +01:00
c937eb9f5f build 2025-12-04 23:23:59 +01:00
74739f459f Update .gitlab-ci.yml file 2025-12-04 23:23:39 +01:00
493f16281c build 2025-12-04 23:13:54 +01:00
f328118f65 build 2025-12-04 22:12:36 +01:00
e9a3cb8dc4 build 2025-12-04 22:10:57 +01:00
6f05b2b93b Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-04 22:10:33 +01:00
91abc72e5c build 2025-12-04 22:10:30 +01:00
cf5e2ce658 Update .gitlab-ci.yml file 2025-12-04 22:10:18 +01:00
2719a91426 build 2025-12-04 22:09:11 +01:00
5b6a504258 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-04 22:08:51 +01:00
6cc5bfb487 build 2025-12-04 22:08:47 +01:00
e981a0dc1c Update .gitlab-ci.yml file 2025-12-04 22:08:26 +01:00
ee01577a6c build 2025-12-04 22:02:58 +01:00
f8b0acdfcc build 2025-12-04 20:06:22 +01:00
cce4c5eb9b build 2025-12-04 16:01:50 +01:00
90712be4b2 build 2025-12-04 15:59:56 +01:00
0ab5e9627b build 2025-12-04 14:23:07 +01:00
ad5b61a44d build 2025-12-04 13:22:22 +01:00
1ae000292b Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-04 13:21:24 +01:00
5fb1e8ade4 build 2025-12-04 13:21:20 +01:00
0022673a7b Update .gitlab-ci.yml file 2025-12-04 13:21:03 +01:00
48e8b36646 build 2025-12-02 21:38:57 +01:00
3f4ccd9d32 build 2025-12-02 21:35:44 +01:00
8e899ce382 Update .gitlab-ci.yml file 2025-12-02 21:35:11 +01:00
39fb6a1366 build 2025-12-02 21:34:17 +01:00
313d6df99b build 2025-12-02 21:33:44 +01:00
8ecdeb95aa Update .gitlab-ci.yml file 2025-12-02 21:33:24 +01:00
05aa7e362d build 2025-12-02 21:31:35 +01:00
b111717e26 build 2025-12-02 21:30:49 +01:00
5be1560be6 build 2025-12-02 21:29:55 +01:00
28b745b2cc Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-02 21:10:57 +01:00
1a3e37ed8b build 2025-12-02 21:10:52 +01:00
92299e1b23 Update .gitlab-ci.yml file 2025-12-02 21:10:14 +01:00
c7d4623c2f build 2025-12-02 21:05:06 +01:00
6925199f87 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-02 21:04:46 +01:00
c722b5f723 build 2025-12-02 21:04:41 +01:00
abefe7ebd7 Update .gitlab-ci.yml file 2025-12-02 20:01:33 +01:00
c1fcb94962 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-02 20:00:27 +01:00
e052fd7038 build 2025-12-02 20:00:22 +01:00
05aa62cbed Update .gitlab-ci.yml file 2025-12-02 20:00:13 +01:00
327ce78259 Update .gitlab-ci.yml file 2025-12-02 19:44:58 +01:00
b38bf30b56 Update .gitlab-ci.yml file 2025-12-02 19:44:14 +01:00
dec2f81b05 build 2025-12-02 19:14:39 +01:00
615af18e12 build 2025-12-02 19:08:37 +01:00
d25595b0db build 2025-12-02 18:59:25 +01:00
3c8b297dd5 build 2025-12-02 18:17:39 +01:00
0f3b6d9d99 build 2025-12-02 18:12:08 +01:00
90f5fe38b3 build 2025-12-02 18:07:51 +01:00
7bff9cc3b4 build 2025-12-02 18:03:58 +01:00
de59da76df build 2025-12-02 18:00:00 +01:00
d80d5298f3 Update .gitlab-ci.yml file 2025-12-02 17:59:45 +01:00
54790febd2 build 2025-12-02 17:58:45 +01:00
f654a483ea Update .gitlab-ci.yml file 2025-12-02 17:58:33 +01:00
60d9b8e1c8 build 2025-12-02 17:57:23 +01:00
d8283aa281 Merge branch 'main' of gitlab.sectorq.eu:jaydee/portainer 2025-12-02 17:57:13 +01:00
f2a8c9ee29 build 2025-12-02 17:57:02 +01:00
cdde724e4b Update .gitlab-ci.yml file 2025-12-02 17:56:15 +01:00
a98599beee build 2025-12-02 00:45:28 +01:00
0b058c955e build 2025-12-01 20:35:10 +01:00
5d31ab363a build 2025-12-01 19:47:53 +01:00
1895eff815 build 2025-12-01 19:46:39 +01:00
0a00c22d67 build 2025-12-01 19:46:03 +01:00
2320ff1b53 build 2025-12-01 18:10:46 +01:00
117da5fc5d build 2025-11-30 23:34:48 +01:00
8c517e66b0 build 2025-11-30 23:01:08 +01:00
3595f832f2 build 2025-11-30 18:01:10 +01:00
6 changed files with 1334 additions and 572 deletions

3
.flake8 Normal file
View File

@@ -0,0 +1,3 @@
[flake8]
max-line-length = 120

74
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,74 @@
stages: # List of stages for jobs, and their order of execution
- lint
- build
- clean
- notify
variables:
GIT_SSH_COMMAND: "ssh -i /home/gitlab-runner/.ssh/id_rsa -o IdentitiesOnly=yes"
lint:
stage: lint
# image: python:3.12
before_script:
- python3 -m pip install --break-system-packages flake8 black pylint tabulate prompt_toolkit
- export PATH="$PATH:/home/gitlab-runner/.local/bin"
# - echo "PATH is now: $PATH"
script:
- flake8 .
- black --check .
- pylint portainer.py
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
rules:
- if: '$CI_COMMIT_MESSAGE =~ /lint/'
build-job: # This job runs in the build stage, which runs first.
stage: build
script:
- python3 -m venv venv
- source venv/bin/activate
- pip install pyinstaller requests tabulate gitpython prompt_toolkit
- pyinstaller --onefile portainer.py
- scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.80.222:/myapps/bin/ || true
- scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@morefine.home.lan:/myapps/bin/ || true
- scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@m-server.home.lan:/myapps/bin/ || true
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
artifacts:
paths:
- dist/
expire_in: 1 week
# - column=":"
# - echo "${flow_id}"
# - curl -X POST https://kestra.sectorq.eu/api/v1/executions/webhook/jaydee/ansible-all/${flow_id} -d '{"tag":["proxmox"],"target":["servers"]}' -H "Content-Type${column} application/json"
rules:
- if: '$CI_COMMIT_MESSAGE =~ /build/'
clean-job: # This job runs in the build stage, which runs first.
stage: clean
script:
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
rules:
- if: '$CI_COMMIT_MESSAGE =~ /build/'
cleanup_on_failure_job:
stage: clean # Should be in a later stage than the job that might fail
when: on_failure # <-- This is the key keyword
script:
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
notify:
stage: notify # Should be in a later stage than the job that might fail
when: on_success # <-- This is the key keyword
script:
- column=':'
- echo "${flow_id}"
- 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
notify2:
stage: notify # Should be in a later stage than the job that might fail
when: on_failure # <-- This is the key keyword
script:
- column=':'
- echo "${flow_id}"
- curl -XPOST http://192.168.77.101:8123/api/webhook/voice-notifications-tC_8YKxMJIAaQRV5riKuC7Zl --data-raw 'message=portainer build job failed'
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
rules:
- if: '$CI_COMMIT_MESSAGE =~ /build/'

4
.pylintrc Normal file
View File

@@ -0,0 +1,4 @@
# In your .pylintrc or setup.cfg file
[FORMAT]
# The number of characters that a line should not exceed
max-line-length=120

1148
port.py

File diff suppressed because it is too large Load Diff

671
portainer.py Normal file → Executable file
View File

@@ -1,189 +1,598 @@
"""
Portainer API Client module.
This module provides a wrapper for interacting with the Portainer API
to manage endpoints, stacks, and containers.
"""
# !/myapps/venvs/portainer/bin/python3
import os
import sys
import requests
import json
import uuid
import argparse
import shutil
import time
from tabulate import tabulate
from git import Repo # pip install gitpython
from port import Portainer
import logging
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("--endpoint-id", "-e", type=str, default="all", help="Endpoint ID to limit stack 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")
import signal
import sys
import json
import argparse
import tty
import termios
from tabulate import tabulate
from port import Portainer
from prompt_toolkit import prompt
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.shortcuts import checkboxlist_dialog
from prompt_toolkit.shortcuts import radiolist_dialog
VERSION = "0.1.10"
defaults = {
"endpoint_id": "vm01",
"stack": "my_stack",
"deploy_mode": "git",
"autostart": "True",
"stack_mode": "swarm",
"site": "portainer",
}
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"):
with open("/myapps/portainer.conf", "r") as f:
conf_data = f.read()
for line in conf_data.split("\n"):
if line.startswith("#") or line.strip() == "":
continue
key, value = line.split("=", 1)
os.environ[key.strip()] = value.strip()
cur_config[key.strip()] = value.strip()
else:
print("No /myapps/portainer.conf file found, proceeding with env vars.")
os.makedirs("/myapps", exist_ok=True)
for field in defaults.keys():
value_in = os.getenv(f"PORTAINER_{field.upper()}")
if value_in is not None:
os.environ[f"PORTAINER_{field.upper()}"] = value_in
cur_config[f"PORTAINER_{field.upper()}"] = value_in
else:
os.environ[f"PORTAINER_{field.upper()}"] = defaults[field]
cur_config[f"PORTAINER_{field.upper()}"] = defaults[field]
conf_data = "\n".join(f"{k.upper()}={v}" for k, v in cur_config.items())
# print("Using the following configuration:")
with open("/myapps/portainer.conf", "w") as f:
f.write(conf_data)
print("Configuration written to /myapps/portainer.conf")
return cur_config
cur_config = load_config(defaults)
# ENV_VARS = [
# "PORTAINER_URL",
# "PORTAINER_SITE",
# "PORTAINER_ENDPOINT_ID",
# "PORTAINER_STACK",
# "PORTAINER_DEPLOY_MODE",
# "PORTAINER_STACK_MODE",
# ]
def update_configs(cur_config):
'''Update defaults from environment variables if set.'''
conf_data = "\n".join(f"{k.upper()}={v}" for k, v in cur_config.items())
# print("Using the following configuration:")
# print(conf_data)
with open("/myapps/portainer.conf", "w") as f:
f.write(conf_data)
print("Configuration written to /myapps/portainer.conf")
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("--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"
)
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 stacls")
parser.add_argument("--stop-containers", "-O", action="store_true", help="Stop containers")
parser.add_argument("--start-containers", "-X", action="store_true", help="Stop containers")
parser.add_argument("--delete-stack", "-d", action="store_true", help="Delete stack")
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("--autostart", "-a", 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("--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, help="Stack ID for operations")
parser.add_argument("--token-only", action="store_true", help="Print auth token and exit")
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(
"--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("--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("--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)
if args.site is not None:
cur_config["PORTAINER_SITE"] = args.site
if args.endpoint_id is not None:
cur_config["PORTAINER_ENDPOINT_ID"] = args.endpoint_id
if args.stack is not None:
cur_config["PORTAINER_STACK"] = args.stack
if args.deploy_mode is not None:
cur_config["PORTAINER_DEPLOY_MODE"] = args.deploy_mode
if args.stack_mode is not None:
cur_config["PORTAINER_STACK_MODE"] = args.stack_mode
update_configs(cur_config)
if args.debug:
input(cur_config)
_LOG_LEVEL = "INFO"
LOG_FILE = "/tmp/portainer.log"
if _LOG_LEVEL == "DEBUG":
logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG, format='%(asctime)s : %(levelname)s : %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
logging.debug('using debug loging')
logging.basicConfig(
filename=LOG_FILE,
level=logging.DEBUG,
format="%(asctime)s : %(levelname)s : %(message)s",
datefmt="%m/%d/%Y %I:%M:%S %p",
)
logging.debug("using debug logging")
elif _LOG_LEVEL == "ERROR":
logging.basicConfig(filename=LOG_FILE, level=logging.ERROR, format='%(asctime)s : %(levelname)s : %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
logging.info('using error loging')
logging.basicConfig(
filename=LOG_FILE,
level=logging.ERROR,
format="%(asctime)s : %(levelname)s : %(message)s",
datefmt="%m/%d/%Y %I:%M:%S %p",
)
logging.info("using error logging")
elif _LOG_LEVEL == "SCAN":
logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG, format='%(asctime)s : %(levelname)s : %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
logging.info('using error loging')
logging.basicConfig(
filename=LOG_FILE,
level=logging.DEBUG,
format="%(asctime)s : %(levelname)s : %(message)s",
datefmt="%m/%d/%Y %I:%M:%S %p",
)
logging.info("using scan logging")
else:
logging.basicConfig(filename=LOG_FILE, level=logging.INFO, format='%(asctime)s : %(levelname)s : %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
logging.info("script started")
logger = logging.getLogger(__name__)
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format="%(asctime)s : %(levelname)s : %(message)s",
datefmt="%m/%d/%Y %I:%M:%S %p",
)
logging.info("script started")
logger = logging.getLogger(__name__)
portainer_api_key = "ptr_GCNUoFcTOaXm7k8ZxPdQGmrFIamxZPTydbserYofMHc="
def wl(msg):
"""Write log message if debug is enabled."""
if args.debug:
print(msg)
def is_number(s):
"""Check if the input string is a number."""
try:
float(s)
return True
except ValueError:
return False
def get_portainer_token(base_url, username=None, password=None, timeout=10):
def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
"""
Authenticate to Portainer and return a JWT token.
Reads PORTAINER_USER / PORTAINER_PASS from environment if username/password are not provided.
fields = [("arg_name", "Prompt text")]
"""
username = username or os.getenv("PORTAINER_USER")
password = password or os.getenv("PORTAINER_PASS")
if not username or not password:
raise ValueError("Username and password must be provided (or set PORTAINER_USER / PORTAINER_PASS).")
url = f"{base_url.rstrip('/')}/api/auth"
resp = requests.post(url, json={"Username": username, "Password": password}, timeout=timeout)
resp.raise_for_status()
data = resp.json()
token = data.get("jwt") or data.get("JWT") or data.get("token")
if not token:
raise ValueError(f"No token found in response: {data}")
return token
longest = 0
for field, text in fields:
a = text + " (default= " + cur_config["PORTAINER_" + field.upper()] + ")"
if len(a) > longest:
longest = len(a)
for field, text in fields:
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())
if value_in is None:
if default is not None:
prompt_text = f"{text} (default={default}) : "
# value_in = input(prompt) or default
if field == "site":
commands = ["portainer", "port"]
elif field == "deploy_mode":
commands = ["git", "upload"]
elif field == "stack_mode":
commands = ["swarm", "compose"]
elif field == "endpoint_id":
commands = por.endpoints_names
elif field == "stack":
if args.action == "create_stack":
# input(json.dumps(stacks, indent=2))
commands = [
'authentik', 'bitwarden', 'bookstack', 'dockermon', 'fail2ban', 'gitea', 'gitlab', 'grafana',
'home-assistant', 'homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'mailu3',
'mealie', 'mediacenter', 'mosquitto', 'motioneye', 'n8n', 'nebula', 'nextcloud', 'nginx',
'node-red', 'octoprint', 'ollama', 'pihole', 'portainer-ce', 'rancher', 'registry',
'regsync', 'semaphore', 'unifibrowser', 'uptime-kuma', 'watchtower', 'wazuh', 'webhub',
'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():
# print(s)
commands.remove(s)
except KeyError:
print("No stacks found for endpoint", defaults_in[f"PORTAINER_ENDPOINT_ID".upper()])
else:
commands = []
if por._debug:
input(por.stacks_all)
# print(defaults_in[f"PORTAINER_ENDPOINT_ID".upper()])
try:
for s in por.stacks_all[
defaults_in[f"PORTAINER_ENDPOINT_ID".upper()]
]["by_name"].keys():
commands.append(s)
except KeyError:
print(
"No stacks found for endpoint",
defaults_in[f"PORTAINER_ENDPOINT_ID".upper()],
)
sys.exit(1)
else:
commands = []
completer = WordCompleter(
commands, ignore_case=True, match_middle=False
)
try:
if field == "stack":
commands_tuples = [(cmd, cmd) for cmd in commands]
commands_tuples.insert(0, ("__ALL__", "[Select ALL]"))
value_in = checkboxlist_dialog(
title="Select Services",
text="Choose one or more services:",
values=commands_tuples,
).run()
if value_in is None:
print("Cancelled.")
sys.exit(0)
elif "__ALL__" in value_in:
# User selected "Select ALL"
value_in = commands # all real commands
value_in = value_in.sort()
if "pihole" in value_in:
if action == "delete_stack":
value_in.remove("pihole")
value_in.append("pihole")
else:
value_in.remove("pihole")
value_in.insert(0, "pihole")
print(" >> Stacks :", ",".join(value_in))
else:
value_in = (
prompt(
f" >> {prompt_text}",
completer=completer,
placeholder=default,
)
or default
)
except KeyboardInterrupt:
print("\n^C received — exiting cleanly.")
sys.exit(0)
# value_in = input_with_default(prompt_text, default, longest+2)
else:
# value_in = input(f"{text}: ")
commands = ["start", "stop", "status", "restart", "reload", "exit"]
completer = WordCompleter(commands, ignore_case=True)
try:
value_in = (
prompt(
f" >> {text} {default}",
completer=completer,
placeholder=default,
)
or default
)
except KeyboardInterrupt:
print("\n^C received — exiting cleanly.")
sys.exit(0)
# value_in = input_with_default(text, default, longest+2)
if por._debug:
print("Value entered:", value_in)
defaults_in[f"PORTAINER_{field}".upper()] = value_in
setattr(args, field, value_in)
if field == "site" and value_in != cur_site:
por.get_site(value_in)
if field == "stack" and value_in != cur_site:
os.environ[field] = ",".join(value_in)
else:
os.environ[field] = value_in
if por._debug:
print(f"{defaults_in} {field} {value_in}")
if field == "endpoint_id" and value_in != defaults_in.get(
"PORTAINER_ENDPOINT_ID".upper()
):
print("refreshing environment")
por.get_endpoints()
with open("/myapps/portainer.conf", "w") as f:
for k in defaults_in.keys():
f.write(f"{k}={defaults_in[k]}\n")
return args
if __name__ == "__main__":
# Example usage: set PORTAINER_USER and PORTAINER_PASS in env, or pass literals below.
base = os.getenv("PORTAINER_URL", "https://portainer.sectorq.eu/api")
#token = get_portainer_token(base,"admin","l4c1j4yd33Du5lo") # or get_portainer_token(base, "admin", "secret")
token = portainer_api_key
# 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")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
os.system("cls" if os.name == "nt" else "clear")
if args.action is None:
actions = [
("create_stack","create_stack"),
("delete_stack","delete_stack"),
("stop_stack","stop_stack"),
("start_stack","start_stack"),
("list_stacks","list_stacks"),
("update_stack","update_stack"),
("secrets","secrets"),
("print_all_data","print_all_data"),
("list_endpoints","list_endpoints"),
("list_containers","list_containers"),
("stop_containers","stop_containers"),
("start_containers","start_containers"),
("refresh_environment","refresh_environment"),
("refresh_status","refresh_status"),
("update_status","update_status"),
]
selected_action = radiolist_dialog(
title="Select one service",
text="Choose a service:",
values=actions
).run()
print("Selected:", selected_action)
# print("Possible actions: \n")
# i = 1
# for a in actions:
# print(f" > {i:>2}. {a}")
# i += 1
# ans = input("\nSelect action to perform: ")
args.action = selected_action
os.system("cls" if os.name == "nt" else "clear")
# Example: list endpoints
por = Portainer(base, token)
if args.delete_stack:
por.delete_stack(args.endpoint_id,args.stack,)
sys.exit()
por = Portainer(cur_config["PORTAINER_SITE"], timeout=args.timeout)
por.set_defaults(cur_config)
if args.debug:
por._debug = True
if args.create_stack:
por.create_stack(args.endpoint_id,args.stack, args.deploy_mode, args.autostart)
if args.action == "secrets":
args = prompt_missing_args(
args,
cur_config,
[
("site", "Site"),
("endpoint_id", "Endpoint ID"),
],
)
secrets = {
"gitea_runner_registration_token": "8nmKqJhkvYwltmNfF2o9vs0tzo70ufHSQpVg6ymb",
"influxdb2-admin-token": "l4c1j4yd33Du5lo",
"ha_influxdb2_admin_token": "l4c1j4yd33Du5lo",
"wordpress_db_password": "wordpress",
"wordpress_root_db_password": "wordpress",
}
for key, value in secrets.items():
res = por.create_secret(key, value, args.endpoint_id, args.timeout)
print(res)
sys.exit()
if args.stop_stack:
por.stop_stack(args.stack,args.endpoint_id)
if args.action == "delete_stack":
args = prompt_missing_args(
args,
cur_config,
[
("site", "Site"),
("endpoint_id", "Endpoint ID"),
("stack", "Stack name or ID"),
],
action="delete_stack",
)
input(
f"\nDelete stack {','.join(args.stack)} on endpoint {args.endpoint_id}. Press ENTER to continue..."
)
por.delete_stack(
args.endpoint_id,
args.stack,
)
sys.exit()
if args.start_stack:
por.start_stack(args.stack,args.endpoint_id)
if args.action == "create_stack":
por.action = "create_stack"
args = prompt_missing_args(
args,
cur_config,
[
("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,
)
por.create_stack(
args.endpoint_id,
args.stack,
args.deploy_mode,
args.autostart,
args.stack_mode,
)
sys.exit()
if args.list_stacks:
if args.action == "stop_stack":
args = prompt_missing_args(
args,
cur_config,
[
("site", "Site"),
("endpoint_id", "Endpoint ID"),
("stack", "Stack name or ID"),
],
)
por.stop_stack(args.stack, args.endpoint_id)
sys.exit()
if args.action == "start_stack":
args = prompt_missing_args(
args,
cur_config,
[
("site", "Site"),
("endpoint_id", "Endpoint ID"),
("stack", "Stack name or ID"),
],
)
por.start_stack(args.stack, args.endpoint_id)
sys.exit()
if args.action == "list_stacks":
args = prompt_missing_args(
args,
cur_config,
[
("site", "Site"),
("endpoint_id", "Endpoint ID"),
],
)
por.print_stacks(args.endpoint_id)
print(json.dumps(por.all_data,indent=2))
# print(json.dumps(por.all_data, indent=2))
sys.exit()
if args.list_containers:
if args.action == "list_containers":
print("Getting containers")
por.get_containers(args.endpoint_id,args.stack)
por.get_containers(args.endpoint_id, args.stack)
sys.exit()
if args.update_stack:
if args.action == "update_stack":
print("Updating stacks")
autostart=True if args.autostart else False
por.update_stack(args.endpoint_id,args.stack,autostart)
sys.exit()
if args.print_all_data:
print(json.dumps(por.all_data,indent=2))
sys.exit()
if args.update_status:
por.update_status(args.endpoint_id,args.stack)
sys.exit()
if args.list_endpoints:
por.update_stack(args.endpoint_id, args.stack, args.autostart)
sys.exit()
if args.action == "print_all_data":
print(json.dumps(por.all_data, indent=2))
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()
data = []
export_data = []
for i in eps["by_id"]:
data.append([i,eps["by_id"][i]])
export_data.append([i, eps["by_id"][i]])
headers = ["EndpointId", "Name"]
print(tabulate(data, headers=headers, tablefmt="github"))
print(tabulate(export_data, headers=headers, tablefmt="github"))
sys.exit()
if args.stop_containers:
if args.action == "stop_containers":
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 == c or args.stack == "all":
cont+=por.all_data["containers"][args.endpoint_id][c]
por.stop_containers(args.endpoint_id,cont)
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.start_containers:
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)
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.start_containers:
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)
sys.exit()
if args.refresh_environment:
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.refresh_status:
if args.stack== "all":
if args.action == "refresh_status":
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)
stcks = por.get_stacks(endpoint_id=args.endpoint_id)
else:
por.refresh_status(base, args.stack_id, token)
por.refresh_status(args.stack_id)

View File

@@ -1,3 +1,7 @@
requests
gitpython
tabulate
tabulate
# Other dev tools
flake8
pylint
black