This commit is contained in:
2025-12-02 21:04:41 +01:00
parent c1fcb94962
commit c722b5f723
2 changed files with 296 additions and 237 deletions

274
port.py
View File

@@ -5,6 +5,9 @@ import uuid
import shutil import shutil
import time import time
import logging import logging
import base64
import tabulate
from git import Repo
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
@@ -129,7 +132,7 @@ class Portainer:
return resp.text return resp.text
def api_post_file(self, path, endpoint_id, name, envs, file, timeout=120): def api_post_file(self, path, endpoint_id, name, envs, file, timeout=120):
#input("API POST2 called. Press Enter to continue.") # input("API POST2 called. Press Enter to continue.")
"""Example authenticated GET request to Portainer API.""" """Example authenticated GET request to Portainer API."""
url = f"{self.base_url.rstrip('/')}{path}" url = f"{self.base_url.rstrip('/')}{path}"
headers = {"X-API-Key": f"{self.token}"} headers = {"X-API-Key": f"{self.token}"}
@@ -138,7 +141,7 @@ class Portainer:
"Name": name, "Name": name,
"Env": json.dumps(envs) "Env": json.dumps(envs)
} }
#print(data) # print(data)
resp = requests.post(url, headers=headers, files=file, data=data, timeout=timeout) resp = requests.post(url, headers=headers, files=file, data=data, timeout=timeout)
resp.raise_for_status() resp.raise_for_status()
return resp.json() return resp.json()
@@ -146,7 +149,7 @@ class Portainer:
def api_post_no_body(self, path, timeout=120): def api_post_no_body(self, path, timeout=120):
"""Example authenticated GET request to Portainer API.""" """Example authenticated GET request to Portainer API."""
url = f"{self.base_url.rstrip('/')}{path}" url = f"{self.base_url.rstrip('/')}{path}"
#print(url) # print(url)
headers = {"X-API-Key": f"{self.token}"} headers = {"X-API-Key": f"{self.token}"}
resp = requests.post(url, headers=headers, timeout=timeout) resp = requests.post(url, headers=headers, timeout=timeout)
return resp.text return resp.text
@@ -156,9 +159,9 @@ class Portainer:
url = f"{self.base_url.rstrip('/')}{path}" url = f"{self.base_url.rstrip('/')}{path}"
headers = {"X-API-Key": f"{self.token}"} headers = {"X-API-Key": f"{self.token}"}
resp = requests.delete(url, headers=headers, timeout=timeout) resp = requests.delete(url, headers=headers, timeout=timeout)
#print(resp) # print(resp)
resp.raise_for_status() resp.raise_for_status()
#print(resp.status_code) # print(resp.status_code)
return resp.status_code return resp.status_code
def refresh(self): def refresh(self):
@@ -173,62 +176,62 @@ class Portainer:
stcks = [] stcks = []
stacks = self.api_get(path, timeout=timeout) stacks = self.api_get(path, timeout=timeout)
self.stacks_all = {} self.stacks_all = {}
fail_endponts = [20,39,41] fail_endponts = [20, 39, 41]
#print(json.dumps(stacks,indent=2)) # print(json.dumps(stacks,indent=2))
webhooks = {} webhooks = {}
for s in stacks: for s in stacks:
#print(type(s["AutoUpdate"]) ) # print(type(s["AutoUpdate"]) )
#input(s) # input(s)
if s['EndpointId'] in fail_endponts: if s['EndpointId'] in fail_endponts:
continue continue
if not s['EndpointId'] in webhooks: if not s['EndpointId'] in webhooks:
try: try:
webhooks[s['EndpointId']] = {"webhook":{}} webhooks[s['EndpointId']] = {"webhook": {}}
webhooks[self.endpoints["by_id"][s['EndpointId']]] = {"webhook":{}} webhooks[self.endpoints["by_id"][s['EndpointId']]] = {"webhook": {}}
except: except Exception as e:
pass logger.debug(f"Exception while getting webhooks for endpoint {s['EndpointId']}: {e}")
if not s['EndpointId'] in self.stacks_all: if not s['EndpointId'] in self.stacks_all:
self.stacks_all[s['EndpointId']] = {"by_id":{},"by_name":{}} self.stacks_all[s['EndpointId']] = {"by_id": {}, "by_name": {}}
self.stacks_all[self.endpoints["by_id"][s['EndpointId']]] = {"by_id":{},"by_name":{}} self.stacks_all[self.endpoints["by_id"][s['EndpointId']]] = {"by_id": {}, "by_name": {}}
self.stacks_all[s['EndpointId']]["by_id"][s['Id']] = s['Name'] self.stacks_all[s['EndpointId']]["by_id"][s['Id']] = s['Name']
self.stacks_all[self.endpoints["by_id"][s['EndpointId']]]["by_id"][s['Id']] = s['Name'] self.stacks_all[self.endpoints["by_id"][s['EndpointId']]]["by_id"][s['Id']] = s['Name']
self.stacks_all[s['EndpointId']]["by_name"][s['Name']] = s['Id'] self.stacks_all[s['EndpointId']]["by_name"][s['Name']] = s['Id']
self.stacks_all[self.endpoints["by_id"][s['EndpointId']]]["by_name"][s['Name']] = s['Id'] self.stacks_all[self.endpoints["by_id"][s['EndpointId']]]["by_name"][s['Name']] = s['Id']
#print(s) # print(s)
if "AutoUpdate" in s and s["AutoUpdate"] is not None: if "AutoUpdate" in s and s["AutoUpdate"] is not None:
if type(s["AutoUpdate"]) == dict and "Webhook" in s["AutoUpdate"]: if type(s["AutoUpdate"]) is dict and "Webhook" in s["AutoUpdate"]:
#print(self.endpoints["by_id"][s['EndpointId']], s['Name'], s["AutoUpdate"]['Webhook']) # print(self.endpoints["by_id"][s['EndpointId']], s['Name'], s["AutoUpdate"]['Webhook'])
#print("WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW") # print("WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW")
webhooks[s['EndpointId']][s['Name']] = s['AutoUpdate']['Webhook'] webhooks[s['EndpointId']][s['Name']] = s['AutoUpdate']['Webhook']
webhooks[self.endpoints["by_id"][s['EndpointId']]][s['Name']] = s['AutoUpdate']['Webhook'] webhooks[self.endpoints["by_id"][s['EndpointId']]][s['Name']] = s['AutoUpdate']['Webhook']
elif s["AutoUpdate"]["Webhook"] != "": elif s["AutoUpdate"]["Webhook"] != "":
webhooks[s['EndpointId']][s['Name']] = s['Webhook'] webhooks[s['EndpointId']][s['Name']] = s['Webhook']
webhooks[self.endpoints["by_id"][s['EndpointId']]][s['Name']] = s['Webhook'] webhooks[self.endpoints["by_id"][s['EndpointId']]][s['Name']] = s['Webhook']
#print(self.stacks_all) # print(self.stacks_all)
if s['EndpointId'] == endpoint_id or endpoint_id == "all": if s['EndpointId'] == endpoint_id or endpoint_id == "all":
stcks.append(s) stcks.append(s)
#print(stcks) # print(stcks)
if stcks is None: if stcks is None:
return [] return []
self.stacks = stacks self.stacks = stacks
self.all_data["stacks"] = self.stacks_all self.all_data["stacks"] = self.stacks_all
self.all_data["webhooks"] = webhooks self.all_data["webhooks"] = webhooks
#input(json.dumps(self.stacks_all,indent=2)) # input(json.dumps(self.stacks_all,indent=2))
return stcks return stcks
def get_stack_id(self,endpoint,stack): def get_stack_id(self, endpoint, stack):
pass pass
def update_status(self,endpoint,stack): def update_status(self, endpoint, stack):
path = f"/stacks/{self.all_data['stacks'][endpoint]['by_name'][stack]}/images_status?refresh=true" path = f"/stacks/{self.all_data['stacks'][endpoint]['by_name'][stack]}/images_status?refresh=true"
#input(path) # input(path)
stats = self.api_get(path) stats = self.api_get(path)
print(stats) print(stats)
def get_endpoint_id(self,endpoint): def get_endpoint_id(self, endpoint):
if self.is_number(endpoint): if self.is_number(endpoint):
self.endpoint_id = endpoint self.endpoint_id = endpoint
self.endpoint_name = self.endpoints["by_id"][endpoint] self.endpoint_name = self.endpoints["by_id"][endpoint]
@@ -238,7 +241,7 @@ class Portainer:
self.endpoint_id = self.endpoints["by_name"][endpoint] self.endpoint_id = self.endpoints["by_name"][endpoint]
return self.endpoints["by_name"][endpoint] return self.endpoints["by_name"][endpoint]
def get_endpoint_name(self,endpoint): def get_endpoint_name(self, endpoint):
if self.is_number(endpoint): if self.is_number(endpoint):
self.endpoint_id = endpoint self.endpoint_id = endpoint
self.endpoint_name = self.endpoints["by_id"][endpoint] self.endpoint_name = self.endpoints["by_id"][endpoint]
@@ -249,72 +252,75 @@ class Portainer:
return endpoint return endpoint
def get_containers(self, endpoint="all", stack="all", timeout=30): def get_containers(self, endpoint="all", stack="all", timeout=30):
#print(json.dumps(self.all_data,indent=2)) # print(json.dumps(self.all_data,indent=2))
#print(endpoint) # print(endpoint)
#print(stack) # print(stack)
cont = [] cont = []
data = {} data = {}
if endpoint == "all": if endpoint == "all":
for s in self.all_data["endpoints"]["by_id"]: for s in self.all_data["endpoints"]["by_id"]:
#print(s) # print(s)
if stack == "all": if stack == "all":
if not s in self.all_data["stacks"]: if s not in self.all_data["stacks"]:
continue continue
if self.all_data["endpoints_status"][s] != 1: if self.all_data["endpoints_status"][s] != 1:
#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
for e in self.all_data["stacks"][s]["by_name"]: for e in self.all_data["stacks"][s]["by_name"]:
path = f"/endpoints/{s}/docker/containers/json?all=1&filters={{\"label\": [\"com.docker.compose.project={e}\"]}}" path = (
f"/endpoints/{s}/docker/containers/json"
f"?all=1&filters={{\"label\": [\"com.docker.compose.project={e}\"]}}"
)
logging.info(f"request : {path}") logging.info(f"request : {path}")
try: try:
containers = self.api_get(path) containers = self.api_get(path)
except: except Exception as e:
print(f"failed to get containers from {path}") print(f"failed to get containers from {path}: {e}")
continue continue
contr = [] contr = []
try: try:
for c in containers: for c in containers:
cont.append(c["Names"][0].replace("/","")) cont.append(c["Names"][0].replace("/", ""))
contr.append(c["Names"][0].replace("/","")) contr.append(c["Names"][0].replace("/", ""))
if self.all_data["endpoints"]["by_id"][s] in data: if self.all_data["endpoints"]["by_id"][s] in data:
data[self.all_data["endpoints"]["by_id"][s]][e] = contr data[self.all_data["endpoints"]["by_id"][s]][e] = contr
else: else:
data[self.all_data["endpoints"]["by_id"][s]] = {e:contr} data[self.all_data["endpoints"]["by_id"][s]] = {e: contr}
except: except Exception as e:
print("lalalal") logger.debug(
#print(data) f"Exception while getting containers for stack {e} ",
f"on endpoint {self.all_data['endpoints']['by_id'][s]}: {e}"
)
# print(data)
self.all_data["containers"] = data self.all_data["containers"] = data
else: else:
self.get_containers() self.get_containers()
for i in self.all_data["containers"][endpoint][stack]: for i in self.all_data["containers"][endpoint][stack]:
cont.append(i) cont.append(i)
return cont return cont
def stop_containers(self, endpoint, containers, timeout=130): def stop_containers(self, endpoint, containers, timeout=130):
if self.all_data["endpoints_status"][endpoint] != 1: if self.all_data["endpoints_status"][endpoint] != 1:
print(f"Endpoint {self.get_endpoint_name(endpoint)} is offline") print(f"Endpoint {self.get_endpoint_name(endpoint)} is offline")
ep_id = self.endpoints["by_name"][endpoint] ep_id = self.endpoints["by_name"][endpoint]
def stop(c): def stop(c):
print(f" > Stopping {c}") print(f" > Stopping {c}")
self.api_post_no_body( self.api_post_no_body(
f"/endpoints/{ep_id}/docker/containers/{c}/stop" f"/endpoints/{ep_id}/docker/containers/{c}/stop"
) )
#print(f"✔") # print(f"✔")
with ThreadPoolExecutor(max_workers=10) as exe: with ThreadPoolExecutor(max_workers=10) as exe:
exe.map(stop, containers) exe.map(stop, containers)
# for c in containers: # for c in containers:
# print(f" > Stopping {c}") # print(f" > Stopping {c}")
# self.api_post_no_body(f"/endpoints/{self.endpoints["by_name"][endpoint]}/docker/containers/{c}/stop") # self.api_post_no_body(f"/endpoints/{self.endpoints["by_name"][endpoint]}/docker/containers/{c}/stop")
# return 0 # return 0
def start_containers(self, endpoint, containers, timeout=130): def start_containers(self, endpoint, containers, timeout=130):
ep_id = self.endpoints["by_name"][endpoint] ep_id = self.endpoints["by_name"][endpoint]
def stop(c): def stop(c):
print(f" > Starting {c}") print(f" > Starting {c}")
self.api_post_no_body( self.api_post_no_body(
@@ -327,22 +333,24 @@ class Portainer:
stcs = [] stcs = []
if stack == "all": if stack == "all":
for s in self.all_data["webhooks"][endpoint]: for s in self.all_data["webhooks"][endpoint]:
stcs.append([s,self.all_data["webhooks"][endpoint][s]]) stcs.append([s, self.all_data["webhooks"][endpoint][s]])
else: else:
stcs.append([stack, self.all_data["webhooks"][endpoint][stack]]) stcs.append([stack, self.all_data["webhooks"][endpoint][stack]])
#input(stcs) # input(stcs)
def update(c): def update(c):
print(f" > Updating {c[0]} on {endpoint}") print(f" > Updating {c[0]} on {endpoint}")
ans = self.api_post_no_body( ans = self.api_post_no_body(
f"/stacks/webhooks/{c[1]}" f"/stacks/webhooks/{c[1]}"
) )
logger.debug(f"Update response for stack {c[0]} on endpoint {endpoint}: {ans}")
def stop(): def stop():
cont = [] cont = []
for c in self.all_data["containers"][endpoint]: for c in self.all_data["containers"][endpoint]:
if stack == c or stack== "all": if stack == c or stack == "all":
cont+=self.all_data["containers"][endpoint][c] cont += self.all_data["containers"][endpoint][c]
self.stop_containers(endpoint,cont) self.stop_containers(endpoint, cont)
with ThreadPoolExecutor(max_workers=10) as exe: with ThreadPoolExecutor(max_workers=10) as exe:
exe.map(update, stcs) exe.map(update, stcs)
@@ -351,13 +359,13 @@ class Portainer:
time.sleep(120) time.sleep(120)
cont = [] cont = []
for c in self.all_data["containers"][endpoint]: for c in self.all_data["containers"][endpoint]:
if stack == c or stack== "all": if stack == c or stack == "all":
cont+=self.all_data["containers"][endpoint][c] cont += self.all_data["containers"][endpoint][c]
self.stop_containers(endpoint,cont) self.stop_containers(endpoint, cont)
def get_endpoints(self, timeout=10): def get_endpoints(self, timeout=10):
endpoints = self.api_get("/endpoints") endpoints = self.api_get("/endpoints")
eps = {"by_id":{}, "by_name":{}} eps = {"by_id": {}, "by_name": {}}
eps_stats = {} eps_stats = {}
for ep in endpoints: for ep in endpoints:
eps['by_id'][ep['Id']] = ep['Name'] eps['by_id'][ep['Id']] = ep['Name']
@@ -367,13 +375,13 @@ class Portainer:
self.endpoints = eps self.endpoints = eps
self.all_data["endpoints"] = eps self.all_data["endpoints"] = eps
self.all_data["endpoints_status"] = eps_stats self.all_data["endpoints_status"] = eps_stats
#input(eps_stats) # input(eps_stats)
#input(eps) # input(eps)
return eps return eps
def get_endpoint(self, endpoint_id=None, timeout=30): def get_endpoint(self, endpoint_id=None, timeout=30):
self.get_endpoints() self.get_endpoints()
#print(self.endpoints) # print(self.endpoints)
if self.is_number(endpoint_id): if self.is_number(endpoint_id):
self.endpoint_name = self.endpoints["by_id"][endpoint_id] self.endpoint_name = self.endpoints["by_id"][endpoint_id]
self.endpoint_id = endpoint_id self.endpoint_id = endpoint_id
@@ -390,24 +398,37 @@ class Portainer:
def get_stack(self, stack=None, endpoint_id=None, timeout=None): def get_stack(self, stack=None, endpoint_id=None, timeout=None):
self.get_stacks(endpoint_id) self.get_stacks(endpoint_id)
if not self.is_number(endpoint_id): if not self.is_number(endpoint_id):
endpoint_id = int(self.endpoints["by_name"][endpoint_id]) endpoint_id = int(self.endpoints["by_name"][endpoint_id])
self.stack_id = [] self.stack_id = []
if stack == "all": if stack == "all":
for s in self.stacks: for s in self.stacks:
#print(s) # print(s)
if (endpoint_id == s.get("EndpointId")): if (endpoint_id == s.get("EndpointId")):
self.stack_ids.append(s.get("Id")) self.stack_ids.append(s.get("Id"))
return self.stack_ids return self.stack_ids
else: else:
for s in self.stacks: for s in self.stacks:
#print(s) # print(s)
if (stack is not None and s.get("Id") == stack and endpoint_id == s.get("EndpointId")) or str(s.get("Name")) == str(stack) and endpoint_id == int(s.get("EndpointId")): match_by_id = (
stack is not None
and s.get("Id") == stack
and endpoint_id == s.get("EndpointId")
)
match_by_name = (
str(s.get("Name")) == str(stack)
and endpoint_id == int(s.get("EndpointId")) # Ensure types match for comparison
)
if match_by_id or match_by_name:
# if (stack is not None and s.get("Id") == stack and endpoint_id == s.get("EndpointId"))
# or str(s.get("Name")) == str(stack) and endpoint_id == int(s.get("EndpointId")):
self.stack_id = s.get("Id") self.stack_id = s.get("Id")
self.stack_name = s.get("Name") self.stack_name = s.get("Name")
self.stack_ids.append(s.get("Id")) self.stack_ids.append(s.get("Id"))
return s return s
raise ValueError(f"Stack not found: {stack}") raise ValueError(f"Stack not found: {stack}")
def create_stack(self, endpoint, stack=None, mode="git", autostart=False, swarm=False, timeout=None): def create_stack(self, endpoint, stack=None, mode="git", autostart=False, swarm=False, timeout=None):
@@ -418,7 +439,7 @@ class Portainer:
else: else:
p = "standalone" p = "standalone"
env_path = f"{self.repo_dir}/{stack}/.env" env_path = f"{self.repo_dir}/{stack}/.env"
#input(swarm_id) # input(swarm_id)
self.endpoint_id = self.get_endpoint_id(endpoint) self.endpoint_id = self.get_endpoint_id(endpoint)
if os.path.exists(self.repo_dir): if os.path.exists(self.repo_dir):
shutil.rmtree(self.repo_dir) shutil.rmtree(self.repo_dir)
@@ -427,11 +448,9 @@ class Portainer:
Repo.clone_from(self.git_url, self.repo_dir) Repo.clone_from(self.git_url, self.repo_dir)
if mode == "git": if mode == "git":
path = f"/stacks/create/{p}/repository" path = f"/stacks/create/{p}/repository"
if self.endpoint_id is not None: if self.endpoint_id is not None:
path += f"?endpointId={self.endpoint_id}" path += f"?endpointId={self.endpoint_id}"
if stack == "all": if stack == "all":
if self.endpoint_name == "rack": if self.endpoint_name == "rack":
stacks = self.rack_stacks stacks = self.rack_stacks
@@ -443,52 +462,53 @@ class Portainer:
stacks = self.nas_stacks stacks = self.nas_stacks
else: else:
stacks = [stack] stacks = [stack]
#print(json.dumps(self.stacks_all, indent=2)) # print(json.dumps(self.stacks_all, indent=2))
#input(json.dumps(self.stacks_all,indent=2)) # input(json.dumps(self.stacks_all, indent=2))
for stack in stacks: for stack in stacks:
if self.endpoint_id in self.stacks_all: if self.endpoint_id in self.stacks_all:
if stack in self.stacks_all[self.endpoint_id]['by_id'] or stack in self.stacks_all[self.endpoint_id]['by_name']:
# Check if the stack exists by ID or name
stack_check = (
stack in self.stacks_all[self.endpoint_id]['by_id']
or stack in self.stacks_all[self.endpoint_id]['by_name']
)
if stack_check:
print(f"Stack {stack} already exist") print(f"Stack {stack} already exist")
continue continue
print(f"Working on {stack}") print(f"Working on {stack}")
envs = [] envs = []
if os.path.exists(f"{env_path}"): if os.path.exists(f"{env_path}"):
f = open(f"{env_path}","r") f = open(f"{env_path}", "r")
env_vars = f.read().splitlines() env_vars = f.read().splitlines()
for ev in env_vars: for ev in env_vars:
if ev.startswith("#") or ev.strip() == "": if ev.startswith("#") or ev.strip() == "":
continue continue
if "=" in ev: if "=" in ev:
name, value = ev.split("=",1) name, value = ev.split("=", 1)
envs.append({"name": name, "value": value}) envs.append({"name": name, "value": value})
f.close() f.close()
#wl(envs) # wl(envs)
for e in envs: for e in envs:
#print(f"Env: {e['name']} = {e['value']}") # print(f"Env: {e['name']} = {e['value']}")
HWS = ["HW_MODE","HW_MODE1","HW_MODE2"] HWS = ["HW_MODE", "HW_MODE1", "HW_MODE2"]
if e['name'] == "RESTART" and self.endpoint_name == "m-server": if e['name'] == "RESTART" and self.endpoint_name == "m-server":
e['value'] = "always" e['value'] = "always"
if e['name'] in HWS: if e['name'] in HWS:
#print("Found HW_MODE env var.") # print("Found HW_MODE env var.")
if self.hw_mode: if self.hw_mode:
e['value'] = "hw" e['value'] = "hw"
else: else:
e['value'] = "cpu" e['value'] = "cpu"
if e['name'] == "LOGGING": if e['name'] == "LOGGING":
#print("Found LOGGING env var.") # print("Found LOGGING env var.")
if self.log_mode: if self.log_mode:
e['value'] = "journald" e['value'] = "journald"
else: else:
e['value'] = "syslog" e['value'] = "syslog"
uid = uuid.uuid4() uid = uuid.uuid4()
#print(uid) # print(uid)
req = { req = {
"Name": stack, "Name": stack,
"Env": envs, "Env": envs,
@@ -510,7 +530,7 @@ class Portainer:
"supportRelativePath": True, "supportRelativePath": True,
"repositoryAuthentication": True, "repositoryAuthentication": True,
"fromAppTemplate": False, "fromAppTemplate": False,
"registries": [6,3], "registries": [6, 3],
"FromAppTemplate": False, "FromAppTemplate": False,
"Namespace": "", "Namespace": "",
"CreatedByUserId": "", "CreatedByUserId": "",
@@ -518,7 +538,7 @@ class Portainer:
"filesystemPath": "/share/docker_data/portainer/portainer-data/", "filesystemPath": "/share/docker_data/portainer/portainer-data/",
"RegistryID": 4, "RegistryID": 4,
"isDetachedFromGit": True, "isDetachedFromGit": True,
"method":"repository", "method": "repository",
"swarmID": None "swarmID": None
} }
if swarm: if swarm:
@@ -528,9 +548,9 @@ class Portainer:
req["ConfigFilePath"] = f"__swarm/{stack}/{stack}-swarm.yml" req["ConfigFilePath"] = f"__swarm/{stack}/{stack}-swarm.yml"
print(json.dumps(req)) print(json.dumps(req))
res = self.api_post(path,req) res = self.api_post(path, req)
if "Id" in res: if "Id" in res:
#print("Deploy request OK") # print("Deploy request OK")
pass pass
else: else:
print(res) print(res)
@@ -538,9 +558,9 @@ class Portainer:
created = False created = False
while True: while True:
try: try:
#print(self.endpoint_id) # print(self.endpoint_id)
#print(stack) # print(stack)
stck2 = self.get_stack(stack, self.endpoint_id) self.get_stack(stack, self.endpoint_id)
created = True created = True
break break
except Exception as e: except Exception as e:
@@ -550,16 +570,17 @@ class Portainer:
if tries > 50: if tries > 50:
print(f"Error retrieving stack {stack} after creation: {self.endpoint_name}") print(f"Error retrieving stack {stack} after creation: {self.endpoint_name}")
break break
logger.debug(f"Exception while getting stack {stack}: {e}")
if created: if created:
if stack != "pihole": if stack != "pihole":
#print(autostart) # print(autostart)
if not autostart: if not autostart:
#self.get_stacks() # self.get_stacks()
#self.stop_stack(stack,self.endpoint_id) # self.stop_stack(stack,self.endpoint_id)
conts = self.get_containers(self.endpoint_name,stack) conts = self.get_containers(self.endpoint_name, stack)
#print(conts) # print(conts)
self.stop_containers(self.endpoint_name,conts) self.stop_containers(self.endpoint_name, conts)
if mode == "file": if mode == "file":
print("Creating new stack from file...") print("Creating new stack from file...")
@@ -579,7 +600,7 @@ class Portainer:
for stack in stacks: for stack in stacks:
print(f"Working on {stack}") print(f"Working on {stack}")
if os.path.exists(f"{self.repo_dir}/{stack}/.env"): if os.path.exists(f"{self.repo_dir}/{stack}/.env"):
f = open(f"{self.repo_dir}/{stack}/.env","r") f = open(f"{self.repo_dir}/{stack}/.env", "r")
env_vars = f.read().splitlines() env_vars = f.read().splitlines()
envs = [] envs = []
@@ -587,13 +608,13 @@ class Portainer:
if ev.startswith("#") or ev.strip() == "": if ev.startswith("#") or ev.strip() == "":
continue continue
if "=" in ev: if "=" in ev:
name, value = ev.split("=",1) name, value = ev.split("=", 1)
envs.append({"name": name, "value": value}) envs.append({"name": name, "value": value})
f.close() f.close()
#wl(envs) # wl(envs)
for e in envs: for e in envs:
#print(f"Env: {e['name']} = {e['value']}") # print(f"Env: {e['name']} = {e['value']}")
HWS = ["HW_MODE","HW_MODE1","HW_MODE2"] HWS = ["HW_MODE", "HW_MODE1", "HW_MODE2"]
if e['name'] == "RESTART" and self.endpoint_name == "m-server": if e['name'] == "RESTART" and self.endpoint_name == "m-server":
e['value'] = "always" e['value'] = "always"
if e['name'] in HWS: if e['name'] in HWS:
@@ -613,15 +634,14 @@ class Portainer:
# ("filename", file_object) # ("filename", file_object)
"file": ("docker-compose.yml", open(f"/tmp/docker-compose/{stack}/docker-compose.yml", "rb")), "file": ("docker-compose.yml", open(f"/tmp/docker-compose/{stack}/docker-compose.yml", "rb")),
} }
self.api_post_file(path,self.endpoint_id,stack,envs,file) self.api_post_file(path, self.endpoint_id, stack, envs, file)
def print_stacks(self,endpoint="all"): def print_stacks(self, endpoint="all"):
stacks = self.get_stacks() stacks = self.get_stacks()
count = 0 count = 0
lst = []
data = [] data = []
for stack in stacks: for stack in stacks:
if endpoint != None: if endpoint 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 endpoint != "all":
@@ -631,18 +651,19 @@ class Portainer:
data.append([stack['Id'], stack['Name'], self.endpoints['by_id'][stack['EndpointId']]]) data.append([stack['Id'], stack['Name'], self.endpoints['by_id'][stack['EndpointId']]])
except KeyError as e: except KeyError as e:
data.append([stack['Id'], stack['Name'], "?"]) data.append([stack['Id'], stack['Name'], "?"])
logger.debug(f"KeyError getting endpoint name for stack {stack['Name']}: {e}")
count += 1 count += 1
headers = ["StackID", "Name", "Endpoint"] headers = ["StackID", "Name", "Endpoint"]
print(tabulate(data, headers=headers, tablefmt="github")) print(tabulate(data, headers=headers, tablefmt="github"))
print(f"Total stacks: {count}") print(f"Total stacks: {count}")
def start_stack(self,stack=None,endpoint_id=None): def start_stack(self, stack=None, endpoint_id=None):
if endpoint_id != None: if endpoint_id is not None:
print("Getting endpoint") print("Getting endpoint")
self.get_endpoint(endpoint_id) self.get_endpoint(endpoint_id)
if stack != None: if stack is not None:
self.get_stack(stack,endpoint_id) self.get_stack(stack, endpoint_id)
for stack in self.stack_ids: for stack in self.stack_ids:
path = f"/stacks/{stack}/start" path = f"/stacks/{stack}/start"
if self.endpoint_id is not None: if self.endpoint_id is not None:
@@ -658,15 +679,15 @@ class Portainer:
print(f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stack]} : {json.loads(resp)['message']}") print(f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stack]} : {json.loads(resp)['message']}")
return True return True
def stop_stack(self,stack,endpoint_id): def stop_stack(self, stack, endpoint_id):
print(f"Stopping stack {stack}") print(f"Stopping stack {stack}")
if endpoint_id != None: if endpoint_id is not None:
self.get_endpoint(endpoint_id) self.get_endpoint(endpoint_id)
if stack == "all": if stack == "all":
self.get_stack(stack,endpoint_id) self.get_stack(stack, endpoint_id)
else: else:
if stack != None: if stack is not None:
self.stack_ids = [self.get_stack(stack,endpoint_id)["Id"]] self.stack_ids = [self.get_stack(stack, endpoint_id)["Id"]]
for stack in self.stack_ids: for stack in self.stack_ids:
path = f"/stacks/{stack}/stop" path = f"/stacks/{stack}/stop"
if self.endpoint_id is not None: if self.endpoint_id is not None:
@@ -674,7 +695,7 @@ class Portainer:
try: try:
resp = self.api_post_no_body(path, timeout=120) resp = self.api_post_no_body(path, timeout=120)
except NameError as e: except NameError as e:
print(f"Error stoping stack: {e}") print(f"Error stopping stack: {e}")
return [] return []
if "Id" in json.loads(resp): if "Id" in json.loads(resp):
print(f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stack]} : stopped") print(f"Stack {self.stacks_all[self.endpoint_id]['by_id'][stack]} : stopped")
@@ -694,31 +715,32 @@ class Portainer:
self.endpoint_name = endpoint_id self.endpoint_name = endpoint_id
self.endpoint_id = self.endpoints["by_name"][endpoint_id] self.endpoint_id = self.endpoints["by_name"][endpoint_id]
if not self.is_number(endpoint_id): if not self.is_number(endpoint_id):
endpoint_id = int(self.endpoints["by_name"][endpoint_id]) endpoint_id = int(self.endpoints["by_name"][endpoint_id])
if not self.is_number(stack) and stack != "all": if not self.is_number(stack) and stack != "all":
#print(stack) # print(stack)
#print(self.endpoint_id) # print(self.endpoint_id)
stack = self.get_stack(stack,self.endpoint_id)["Id"] stack = self.get_stack(stack, self.endpoint_id)["Id"]
if stack == "all": if stack == "all":
stacks = self.get_stacks(self.endpoint_id) stacks = self.get_stacks(self.endpoint_id)
paths = [] paths = []
for s in stacks: for s in stacks:
#print(f"Delete stack {s['Name']}") # print(f"Delete stack {s['Name']}")
#print(s['EndpointId'], endpoint_id) # print(s['EndpointId'], endpoint_id)
if int(s['EndpointId']) != int(endpoint_id): if int(s['EndpointId']) != int(endpoint_id):
continue continue
#print("Deleting stack:", s['Name']) # print("Deleting stack:", s['Name'])
path = f"/stacks/{s['Id']}" path = f"/stacks/{s['Id']}"
if endpoint_id is not None: if endpoint_id is not None:
path += f"?endpointId={endpoint_id}&removeVolumes=true" path += f"?endpointId={endpoint_id}&removeVolumes=true"
paths.append([self.get_endpoint_name(endpoint_id),s['Name'],path]) paths.append([self.get_endpoint_name(endpoint_id), s['Name'], path])
#input(paths) # input(paths)
def delete(c): def delete(c):
print(f"Delete stack {c[1]} from {c[0]} ") print(f"Delete stack {c[1]} from {c[0]} ")
out = self.api_delete(c[2]) out = self.api_delete(c[2])
logger.debug(f"Deleted stack {c[1]} from {c[0]}: {out}")
with ThreadPoolExecutor(max_workers=10) as exe: with ThreadPoolExecutor(max_workers=10) as exe:
exe.map(delete, paths) exe.map(delete, paths)
@@ -735,12 +757,12 @@ class Portainer:
# print(token) # print(token)
stacks = self.api_delete(path) stacks = self.api_delete(path)
except Exception as e: except Exception as e:
#print(f"Error creating stack: {e}") # print(f"Error creating stack: {e}")
if "Conflict for url" in str(e): if "Conflict for url" in str(e):
print("Stack with this name may already exist.") print("Stack with this name may already exist.")
else: else:
print(f"Error deleting stack: {e}") print(f"Error deleting stack: {e}")
#print(stacks) # print(stacks)
return [] return []
if stacks is None: if stacks is None:
return [] return []

View File

@@ -3,15 +3,11 @@ import os
import sys import sys
import requests import requests
import json import json
import uuid
import argparse import argparse
import shutil
import time
from tabulate import tabulate from tabulate import tabulate
from git import Repo # pip install gitpython
from port import Portainer from port import Portainer
import logging import logging
VERSION="0.0.1" VERSION = "0.0.1"
defaults = { defaults = {
"endpoint_id": "vm01", "endpoint_id": "vm01",
@@ -24,18 +20,21 @@ defaults = {
parser = argparse.ArgumentParser(description="Portainer helper - use env vars or pass credentials.") parser = argparse.ArgumentParser(description="Portainer helper - use env vars or pass credentials.")
parser.add_argument("--base", "-b", default=os.getenv("PORTAINER_URL", \ parser.add_argument("--base",
"https://portainer.example.com"),help="Base URL for Portainer (ENV: PORTAINER_URL)") "-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("--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("--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("--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-endpoints", "-E", action="store_true", help="List endpoints")
parser.add_argument("--list-stacks", "-l", action="store_true", help="List stacks") 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("--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("--list-containers", "-c", action="store_true", help="List containers")
parser.add_argument("--update-stack", "-U", action="store_true", help="Update stacls") 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("--stop-containers", "-O", action="store_true", help="Stop containers")
parser.add_argument("--start-containers", "-X", 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("--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("--get-stack", metavar="NAME_OR_ID", help="Get stack by name or numeric id")
parser.add_argument("--action", "-a", type=str, default=None, help="Action to perform") parser.add_argument("--action", "-a", type=str, default=None, help="Action to perform")
@@ -44,17 +43,17 @@ parser.add_argument("--start-stack", "-x", action='store_true')
parser.add_argument("--stop-stack", "-o", action='store_true') parser.add_argument("--stop-stack", "-o", action='store_true')
parser.add_argument("--secrets", "-q", action='store_true') parser.add_argument("--secrets", "-q", action='store_true')
parser.add_argument("--debug", "-D", action='store_true') parser.add_argument("--debug", "-D", action='store_true')
parser.add_argument("--create-stack","-n", action='store_true') parser.add_argument("--create-stack", "-n", action='store_true')
parser.add_argument("--create-stack_new2","-N", action='store_true') parser.add_argument("--create-stack_new2", "-N", action='store_true')
parser.add_argument("--gpu","-g", action='store_true') parser.add_argument("--gpu", "-g", action='store_true')
parser.add_argument("--create-stacks","-C", action='store_true') parser.add_argument("--create-stacks", "-C", action='store_true')
parser.add_argument("--refresh-status","-r", 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("--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("--token-only", action="store_true", help="Print auth token and exit")
parser.add_argument("--timeout", type=int, default=10, help="Request timeout seconds") parser.add_argument("--timeout", type=int, default=10, help="Request timeout seconds")
parser.add_argument("--deploy-mode","-m", type=str, default="git", help="Deploy mode") parser.add_argument("--deploy-mode", "-m", type=str, default="git", help="Deploy mode")
parser.add_argument("--stack-mode","-w", default=None, help="Stack mode") parser.add_argument("--stack-mode", "-w", default=None, help="Stack mode")
args = parser.parse_args() args = parser.parse_args()
print("Running version:", VERSION) print("Running version:", VERSION)
print("Environment:", args.site) print("Environment:", args.site)
@@ -62,16 +61,36 @@ print("Environment:", args.site)
_LOG_LEVEL = "INFO" _LOG_LEVEL = "INFO"
LOG_FILE = "/tmp/portainer.log" LOG_FILE = "/tmp/portainer.log"
if _LOG_LEVEL == "DEBUG": 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.basicConfig(
logging.debug('using debug loging') 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": 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.basicConfig(
logging.info('using error loging') 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": 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.basicConfig(
logging.info('using error loging') 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: 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.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") logging.info("script started")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -83,9 +102,12 @@ else:
base = os.getenv("PORTAINER_URL", "https://port.sectorq.eu/api") base = os.getenv("PORTAINER_URL", "https://port.sectorq.eu/api")
portainer_api_key = "ptr_/5RkMCT/j3BTaL32vMSDtXFi76yOXRKVFOrUtzMsl5Y=" portainer_api_key = "ptr_/5RkMCT/j3BTaL32vMSDtXFi76yOXRKVFOrUtzMsl5Y="
def wl(msg): def wl(msg):
if args.debug: if args.debug:
print(msg) print(msg)
def is_number(s): def is_number(s):
"""Check if the input string is a number.""" """Check if the input string is a number."""
try: try:
@@ -95,7 +117,6 @@ def is_number(s):
return False return False
def get_portainer_token(base_url, username=None, password=None, timeout=10): def get_portainer_token(base_url, username=None, password=None, timeout=10):
""" """
Authenticate to Portainer and return a JWT token. Authenticate to Portainer and return a JWT token.
@@ -114,6 +135,8 @@ def get_portainer_token(base_url, username=None, password=None, timeout=10):
if not token: if not token:
raise ValueError(f"No token found in response: {data}") raise ValueError(f"No token found in response: {data}")
return token return token
def prompt_missing_args(args, defaults, fields): def prompt_missing_args(args, defaults, fields):
""" """
fields = [("arg_name", "Prompt text")] fields = [("arg_name", "Prompt text")]
@@ -132,11 +155,27 @@ def prompt_missing_args(args, defaults, fields):
setattr(args, field, value) setattr(args, field, value)
return args return args
if __name__ == "__main__": if __name__ == "__main__":
# Example usage: set PORTAINER_USER and PORTAINER_PASS in env, or pass literals below. # Example usage: set PORTAINER_USER and PORTAINER_PASS in env, or pass literals below.
#token = get_portainer_token(base,"admin","l4c1j4yd33Du5lo") # or get_portainer_token(base, "admin", "secret") # token = get_portainer_token(base,"admin","l4c1j4yd33Du5lo") # or get_portainer_token(base, "admin", "secret")
if args.action == None: if args.action is None:
actions = ["delete_stack","create_stack","stop_stack","start_stack","list_stacks","update_stack","secrets","print_all_data","list_endpoints","list_containers","stop_containers","start_containers","refresh_environment","refresh_status","update_status"] actions = ["delete_stack",
"create_stack",
"stop_stack",
"start_stack",
"list_stacks",
"update_stack",
"secrets",
"print_all_data",
"list_endpoints",
"list_containers",
"stop_containers",
"start_containers",
"refresh_environment",
"refresh_status",
"update_status"]
print("Possible actions: ") print("Possible actions: ")
i = 1 i = 1
for a in actions: for a in actions:
@@ -150,7 +189,7 @@ if __name__ == "__main__":
por = Portainer(base, token) por = Portainer(base, token)
if args.action == "secrets": if args.action == "secrets":
if args.endpoint_id == None: if args.endpoint_id is None:
args.endpoint_id = input("Endpoint ID is required for creating secrets : ") args.endpoint_id = input("Endpoint ID is required for creating secrets : ")
secrets = { secrets = {
@@ -182,41 +221,39 @@ if __name__ == "__main__":
("stack_mode", "Stack mode (swarm or compose)"), ("stack_mode", "Stack mode (swarm or compose)"),
("deploy_mode", "Deploy mode (git or upload)") ("deploy_mode", "Deploy mode (git or upload)")
]) ])
por.create_stack(args.endpoint_id, args.stack, args.deploy_mode, args.autostart, args.stack_mode)
por.create_stack(args.endpoint_id,args.stack, args.deploy_mode, args.autostart, args.stack_mode)
sys.exit() sys.exit()
if args.action == "stop_stack": if args.action == "stop_stack":
if args.endpoint_id == None: if args.endpoint_id is None:
args.endpoint_id = input("Endpoint ID is required for stopping stacks : ") args.endpoint_id = input("Endpoint ID is required for stopping stacks : ")
if args.stack == None: if args.stack is None:
args.stack = input("Stack name or ID is required for stopping stacks : ") args.stack = input("Stack name or ID is required for stopping stacks : ")
por.stop_stack(args.stack,args.endpoint_id) por.stop_stack(args.stack, args.endpoint_id)
sys.exit() sys.exit()
if args.action == "start_stack": if args.action == "start_stack":
if args.endpoint_id == None: if args.endpoint_id is None:
args.endpoint_id = input("Endpoint ID is required for starting stacks : ") args.endpoint_id = input("Endpoint ID is required for starting stacks : ")
if args.stack == None: if args.stack is None:
args.stack = input("Stack name or ID is required for starting stacks : ") args.stack = input("Stack name or ID is required for starting stacks : ")
por.start_stack(args.stack, args.endpoint_id) por.start_stack(args.stack, args.endpoint_id)
sys.exit() sys.exit()
if args.action == "list_stacks": if args.action == "list_stacks":
por.print_stacks(args.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() sys.exit()
if args.action == "list_containers": if args.action == "list_containers":
print("Getting containers") print("Getting containers")
por.get_containers(args.endpoint_id,args.stack) por.get_containers(args.endpoint_id, args.stack)
sys.exit() sys.exit()
if args.action == "update_stack": if args.action == "update_stack":
print("Updating stacks") print("Updating stacks")
autostart=True if args.autostart else False autostart = True if args.autostart else False
por.update_stack(args.endpoint_id,args.stack,autostart) por.update_stack(args.endpoint_id, args.stack, autostart)
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))