mirror of
https://gitlab.sectorq.eu/jaydee/portainer.git
synced 2026-01-29 12:59:44 +01:00
Compare commits
114 Commits
63e158899e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ba22f79b9 | |||
| ddeb67750f | |||
| 12ff88f8e8 | |||
| 9d27e804a5 | |||
| 1f0a19b7b1 | |||
| 5adfbbcf3d | |||
| 0dda82be87 | |||
| 1a54c1e341 | |||
| 8d4bd382ee | |||
|
|
96068d4fb3 | ||
|
|
de37276ab6 | ||
| 2dc800f7f9 | |||
| ae387a794c | |||
| a3518ec0bb | |||
|
|
3e86a75502 | ||
|
|
11cd76215a | ||
| 4bbe283211 | |||
| fc3fe7b837 | |||
| 3152014ca3 | |||
| e411c81224 | |||
| 8ae696a96a | |||
| abd989a0db | |||
| bb8ef3bdb8 | |||
| 99aa451620 | |||
| fd1fcf90a4 | |||
| 135447d7aa | |||
| 164252534e | |||
| 807437c47e | |||
| 08a15f3bb9 | |||
| 1f4319f4dd | |||
| 9800b01ea2 | |||
| 4cd9cfce20 | |||
| 12a095e169 | |||
| 162c270c02 | |||
| 0c4a91d7ae | |||
| 36cb83694c | |||
| f97cd105ba | |||
| 954d5b2dd7 | |||
| 73a68a0f1b | |||
| 4598caca89 | |||
| 4d22e77689 | |||
| c2a1a7d115 | |||
| 9a79910428 | |||
| bc69ff6223 | |||
| 4e610eea32 | |||
| 4e8c0ab3a0 | |||
| b057dfcce4 | |||
| ab15e7c8ea | |||
| 74269b0368 | |||
| 45c97d1791 | |||
| c341c2332f | |||
| d0f2cfc75f | |||
| c22287f53b | |||
| f618476534 | |||
| 5865df9abc | |||
| f83ee560c1 | |||
| 5ce8573013 | |||
| e8191802b1 | |||
| e546d0cf3f | |||
| daf219329a | |||
| b601ecc0c3 | |||
| 1a8e532a02 | |||
| d6e4db6dd4 | |||
| 78012cec65 | |||
| ba098499f5 | |||
| ce24b5c00d | |||
| 46143a7c12 | |||
| c731fbe0de | |||
| d878a2baa0 | |||
| e280ea67f7 | |||
| 260eb63262 | |||
| 41d6ec9914 | |||
| 15e442d49b | |||
| 14c31575af | |||
| 406513b4b8 | |||
| 0c1b624972 | |||
| db5209e3fb | |||
| 111c70ef00 | |||
| 8bba4d1d18 | |||
| db45a48106 | |||
| 53438a3fb0 | |||
| 5646e0692d | |||
| 6634cc20fa | |||
| fc9f25a203 | |||
| 8351e9f1b1 | |||
| d007510704 | |||
| 3e202c9fd8 | |||
| d6e5c4087d | |||
| f70ebecc49 | |||
| b8264994c5 | |||
| 6081c44d4c | |||
| abb5fc7708 | |||
| a9eecac96d | |||
| e7bcee762d | |||
| e3fed3304a | |||
| 338aa66565 | |||
| 058554a0ea | |||
| b28a7c8273 | |||
| e464c498ff | |||
| b028a48fc0 | |||
| b2373f7016 | |||
| 1b50b3337c | |||
| ac68b5be6f | |||
| 035abffeab | |||
| 6097d2b442 | |||
| 55bdff1745 | |||
| b385f3db12 | |||
| 94a3ccfd23 | |||
| b0c570d7ba | |||
| 4339a7d769 | |||
| 039078191f | |||
| f5d76d87e0 | |||
| f561508d2e | |||
| 974966fdd8 |
@@ -1,3 +1,4 @@
|
||||
|
||||
stages: # List of stages for jobs, and their order of execution
|
||||
- lint
|
||||
- build
|
||||
@@ -26,7 +27,10 @@ build-job: # This job runs in the build stage, which runs first.
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.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.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
|
||||
@@ -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
|
||||
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
|
||||
@@ -61,7 +63,6 @@ notify:
|
||||
- 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
|
||||
@@ -72,4 +73,3 @@ notify2:
|
||||
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/portainer.tmp
|
||||
rules:
|
||||
- if: '$CI_COMMIT_MESSAGE =~ /build/'
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.defaultInterpreterPath": "../../venvs/portainer/bin/python"
|
||||
}
|
||||
@@ -5,24 +5,29 @@ 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 logging
|
||||
import signal
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import tty
|
||||
import termios
|
||||
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 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
|
||||
|
||||
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:
|
||||
VAULT_TOKEN = os.environ.get("VAULT_TOKEN")
|
||||
if VAULT_TOKEN is None:
|
||||
@@ -39,7 +44,7 @@ else:
|
||||
raise Exception("Failed to authenticate with Vault")
|
||||
# Specify the mount point of your KV engine
|
||||
|
||||
VERSION = "0.1.17"
|
||||
VERSION = "0.1.55"
|
||||
|
||||
defaults = {
|
||||
"endpoint_id": "vm01",
|
||||
@@ -139,10 +144,12 @@ parser.add_argument(
|
||||
)
|
||||
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("--launcher", "-L", 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("--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)
|
||||
@@ -242,11 +249,11 @@ def prompt_missing_args(args_in, defaults_in, fields, action=None,stacks=None):
|
||||
if args.action == "create_stack":
|
||||
# input(json.dumps(stacks, indent=2))
|
||||
commands = [
|
||||
'authentik', 'bitwarden', 'bookstack', 'dockermon', 'fail2ban', 'gitea', 'gitlab', 'grafana',
|
||||
'hashicorp', 'home-assistant', 'homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'mailu3',
|
||||
'authentik', 'bitwarden', 'bookstack', 'dockermon', 'duplicati', 'fail2ban', 'gitea', 'gitlab', 'grafana', 'grocy',
|
||||
'hashicorp', 'home-assistant', 'homebox','homepage', 'immich', 'influxdb', 'jupyter', 'kestra', 'kopia', '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',
|
||||
'regsync', 'searxng','semaphore', 'unifibrowser', 'uptime-kuma', 'watchtower', 'wazuh', 'webhub', 'wordpress',
|
||||
'wud', 'zabbix-server']
|
||||
try:
|
||||
print(por.all_data['stacks'][defaults_in[f"PORTAINER_ENDPOINT_ID".upper()]]['by_name'].keys())
|
||||
@@ -403,18 +410,13 @@ if __name__ == "__main__":
|
||||
]
|
||||
|
||||
selected_action = radiolist_dialog(
|
||||
title="Select one service",
|
||||
title=f"Select one service - version: {VERSION}",
|
||||
text="Choose a service:",
|
||||
values=actions
|
||||
).run()
|
||||
|
||||
|
||||
|
||||
print("Selected:", selected_action)
|
||||
|
||||
|
||||
|
||||
|
||||
# print("Possible actions: \n")
|
||||
# i = 1
|
||||
# for a in actions:
|
||||
@@ -426,7 +428,7 @@ if __name__ == "__main__":
|
||||
|
||||
os.system("cls" if os.name == "nt" else "clear")
|
||||
# Example: list endpoints
|
||||
por = Portainer(cur_config["PORTAINER_SITE"], args)
|
||||
por = PortainerApi(cur_config["PORTAINER_SITE"], args)
|
||||
por.set_defaults(cur_config)
|
||||
if args.debug:
|
||||
por._debug = True
|
||||
@@ -547,6 +549,8 @@ if __name__ == "__main__":
|
||||
],
|
||||
)
|
||||
por.update_service()
|
||||
if args.launcher:
|
||||
input("\nPress ENTER to continue...")
|
||||
sys.exit()
|
||||
|
||||
if args.action == "update_containers":
|
||||
@@ -574,6 +578,8 @@ if __name__ == "__main__":
|
||||
],
|
||||
)
|
||||
por.print_stacks(args)
|
||||
if args.launcher:
|
||||
input("Press ENTER to continue...")
|
||||
# print(json.dumps(por.all_data, indent=2))
|
||||
sys.exit()
|
||||
|
||||
@@ -588,6 +594,8 @@ if __name__ == "__main__":
|
||||
],
|
||||
)
|
||||
print("\n".join(por.get_containers()))
|
||||
if args.launcher:
|
||||
input("\nPress ENTER to continue...")
|
||||
sys.exit()
|
||||
|
||||
if args.action == "update_stack":
|
||||
@@ -601,11 +609,16 @@ if __name__ == "__main__":
|
||||
)
|
||||
|
||||
por.update_stack(args)
|
||||
if args.launcher:
|
||||
input("\nPress ENTER to continue...")
|
||||
sys.exit()
|
||||
|
||||
if args.action == "print_all_data":
|
||||
print(json.dumps(por.all_data, indent=2))
|
||||
if args.launcher:
|
||||
input("\nPress ENTER to continue...")
|
||||
sys.exit()
|
||||
|
||||
if args.action == "update_status":
|
||||
por.update_status(args.endpoint_id, args.stack)
|
||||
sys.exit()
|
||||
@@ -617,7 +630,8 @@ if __name__ == "__main__":
|
||||
export_data.append([i, eps["by_id"][i]])
|
||||
headers = ["EndpointId", "Name"]
|
||||
print(tabulate(export_data, headers=headers, tablefmt="github"))
|
||||
|
||||
if args.launcher:
|
||||
input("\nPress ENTER to continue...")
|
||||
sys.exit()
|
||||
|
||||
if args.action == "stop_containers":
|
||||
@@ -664,8 +678,4 @@ if __name__ == "__main__":
|
||||
sys.exit()
|
||||
|
||||
if args.action == "refresh_status":
|
||||
if args.stack == "all":
|
||||
print("Stopping all stacks...")
|
||||
stcks = por.get_stacks(endpoint_id=args.endpoint_id)
|
||||
else:
|
||||
por.refresh_status(args.stack_id)
|
||||
por.refresh_status(args)
|
||||
0
portainer/__init__.py
Normal file
0
portainer/__init__.py
Normal file
@@ -21,7 +21,7 @@ from prompt_toolkit.shortcuts import radiolist_dialog
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Portainer:
|
||||
class PortainerApi:
|
||||
"""
|
||||
Simple wrapper around the module-level Portainer helper functions.
|
||||
Instantiate with base_url and optional token/timeout and call methods
|
||||
@@ -141,6 +141,13 @@ class Portainer:
|
||||
self.get_endpoints()
|
||||
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):
|
||||
"""Check if the input string is a number."""
|
||||
try:
|
||||
@@ -159,7 +166,7 @@ class Portainer:
|
||||
response = requests.post(
|
||||
"https://gotify.sectorq.eu/message",
|
||||
data=payload,
|
||||
headers={"X-Gotify-Key": "ASn_fIAd5OVjm8c"}
|
||||
headers={"X-Gotify-Key": "A1krRuo8GIW-fpY"}
|
||||
)
|
||||
logger.debug(response.text)
|
||||
# print("Status:", response.status_code)
|
||||
@@ -343,7 +350,6 @@ class Portainer:
|
||||
# print(stack)
|
||||
cont = []
|
||||
data = {}
|
||||
|
||||
eps = [ep for ep in self.all_data['endpoints']['by_id'].keys()]
|
||||
#input(eps)
|
||||
for endpoint in eps:
|
||||
@@ -362,11 +368,15 @@ class Portainer:
|
||||
print(f"failed to get containers from {path}: {e}")
|
||||
continue
|
||||
contr = []
|
||||
# print(f"Containers: {containers}")
|
||||
try:
|
||||
for c in containers:
|
||||
#input(c)
|
||||
cont.append([c["Names"][0].replace("/", ""),c["Id"], c['Image']])
|
||||
contr.append([c["Names"][0].replace("/", ""), c["Id"], c['Image']])
|
||||
# print(c)
|
||||
try:
|
||||
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:
|
||||
data[self.all_data["endpoints"]["by_id"][endpoint]] = contr
|
||||
data[endpoint] = contr
|
||||
@@ -489,7 +499,7 @@ class Portainer:
|
||||
stacks_tuples.append((s['Webhook'],s['Name']))
|
||||
# print(s['Name'], " : ", s['Webhook'])
|
||||
stacks_dict = dict(stacks_tuples)
|
||||
print(stacks_dict)
|
||||
# print(stacks_dict)
|
||||
#input(stacks_tuples)
|
||||
# stacks_tuples = [(s['AutoUpdate']['Webhook'], s['Name']) for s in stacks if "Webhook" in s['AutoUpdate'] ]
|
||||
|
||||
@@ -514,7 +524,7 @@ class Portainer:
|
||||
values=stacks_tuples
|
||||
).run()
|
||||
stcs = []
|
||||
input(stack_ids)
|
||||
#input(stack_ids)
|
||||
|
||||
if args.stack == "all":
|
||||
for s in stack_dict:
|
||||
@@ -524,18 +534,18 @@ class Portainer:
|
||||
if s in stack_ids:
|
||||
stcs.append([s, stack_dict[s]])
|
||||
|
||||
print(stcs)
|
||||
# print(stcs)
|
||||
with ThreadPoolExecutor(max_workers=10) as exe:
|
||||
list(exe.map(update, stcs))
|
||||
|
||||
input('UPDATED')
|
||||
#input('UPDATED')
|
||||
if not args.autostart:
|
||||
time.sleep(120)
|
||||
cont = []
|
||||
for c in self.all_data["containers"][endpoint]:
|
||||
if stack == c or stack == "all":
|
||||
cont += self.all_data["containers"][endpoint][c]
|
||||
self.stop_containers(endpoint, cont)
|
||||
for c in self.all_data["containers"][args.endpoint_id]:
|
||||
if args.stack == c or args.stack == "all":
|
||||
cont += self.all_data["containers"][args.endpoint_id][c]
|
||||
self.stop_containers(args.endpoint_id, cont)
|
||||
|
||||
def get_endpoints(self, timeout=10):
|
||||
'''Get a list of all endpoints.'''
|
||||
@@ -913,6 +923,7 @@ class Portainer:
|
||||
#print(longest)
|
||||
ok = "\033[92m✔\033[0m"
|
||||
err = "\033[91m✖\033[0m"
|
||||
updates = []
|
||||
for service_id in service_ids:
|
||||
# print(self.all_data["containers"][self.args.endpoint_id])
|
||||
|
||||
@@ -933,7 +944,7 @@ class Portainer:
|
||||
#print("Recreate")
|
||||
self.recreate_container(service_id, pull)
|
||||
#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=" ")
|
||||
for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]:
|
||||
if name.startswith(service_dict[service_id]):
|
||||
@@ -941,7 +952,7 @@ class Portainer:
|
||||
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]}")
|
||||
updates.append(service_dict[service_id])
|
||||
print(err, end=" ")
|
||||
for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]:
|
||||
if name.startswith(service_dict[service_id]):
|
||||
@@ -951,12 +962,24 @@ class Portainer:
|
||||
for name, hash_, image in self.all_data["containers"][self.args.endpoint_id]:
|
||||
if name.startswith(service_dict[service_id]):
|
||||
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="")
|
||||
return True
|
||||
|
||||
def update_service(self):
|
||||
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 = sorted(service_tuples, key=lambda x: x[1])
|
||||
service_dict = dict(service_tuples)
|
||||
@@ -1024,6 +1047,7 @@ class Portainer:
|
||||
print(err)
|
||||
else:
|
||||
print(ok)
|
||||
self.gotify_message(f"Service update process finished")
|
||||
print("\033[?25h", end="")
|
||||
return True
|
||||
|
||||
@@ -1097,7 +1121,7 @@ class Portainer:
|
||||
# print(path)
|
||||
params={"pullImage": pull}
|
||||
try:
|
||||
resp = self._api_post(path, json=params, timeout=20)
|
||||
resp = self._api_post(path, json=params, timeout=120)
|
||||
#print(resp)
|
||||
except ValueError as e:
|
||||
print(f"Error restarting service: {e}")
|
||||
@@ -1351,4 +1375,5 @@ class Portainer:
|
||||
path = f"/endpoints/{endpoint_id}/docker/secrets/create"
|
||||
encoded = base64.b64encode(value.encode()).decode()
|
||||
data = {"Name": name, "Data": encoded}
|
||||
|
||||
return self._api_post(path, data, timeout=timeout)
|
||||
@@ -5,3 +5,6 @@ tabulate
|
||||
flake8
|
||||
pylint
|
||||
black
|
||||
docker
|
||||
hvac
|
||||
prompt_toolkit
|
||||
Reference in New Issue
Block a user