mirror of
https://gitlab.sectorq.eu/jaydee/portainer.git
synced 2025-12-14 18:44:53 +01:00
build
This commit is contained in:
291
portainer.py
291
portainer.py
@@ -10,17 +10,26 @@ parser.add_argument("--user", "-u", default=os.getenv("PORTAINER_USER"),
|
|||||||
help="Portainer username (ENV: PORTAINER_USER)")
|
help="Portainer username (ENV: PORTAINER_USER)")
|
||||||
parser.add_argument("--password", "-p", default=os.getenv("PORTAINER_PASS"),
|
parser.add_argument("--password", "-p", default=os.getenv("PORTAINER_PASS"),
|
||||||
help="Portainer password (ENV: PORTAINER_PASS)")
|
help="Portainer password (ENV: PORTAINER_PASS)")
|
||||||
parser.add_argument("--endpoint-id", "-e", type=int, help="Endpoint ID to limit stack operations")
|
parser.add_argument("--endpoint-id", "-e", type=str, help="Endpoint ID to limit stack operations")
|
||||||
parser.add_argument("--list-endpoints", action="store_true", help="List endpoints")
|
parser.add_argument("--list-endpoints", action="store_true", help="List endpoints")
|
||||||
parser.add_argument("--list-stacks", action="store_true", help="List stacks")
|
parser.add_argument("--list-stacks", "-l", action="store_true", help="List stacks")
|
||||||
|
parser.add_argument("--delete-stack", "-d", action="store_true", help="Delete stack")
|
||||||
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("--create-stack", action='store_true')
|
parser.add_argument("--create-stack","-c", action='store_true')
|
||||||
|
parser.add_argument("--gpu","-g", action='store_true')
|
||||||
|
parser.add_argument("--create-stacks","-C", action='store_true')
|
||||||
parser.add_argument("--stack-id", "-s", type=str, help="Stack ID for operations")
|
parser.add_argument("--stack-id", "-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")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
nas_stacks = ["pihole","authentik","bitwarden","bookstack","dockermon","gitea","gitlab","grafana","home-assistant","homepage","immich","jupiter","kestra","mailu3","mealie","mediacenter"]
|
||||||
|
|
||||||
|
|
||||||
|
portainer_api_key = "ptr_QoHE/e6kfqIirE3fhzYMRFRxK7eL42wtCxo5P18i1zQ="
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
@@ -44,18 +53,29 @@ def api_get(base_url, path, token, timeout=10):
|
|||||||
"""Example authenticated GET request to Portainer API."""
|
"""Example authenticated GET request to Portainer API."""
|
||||||
url = f"{base_url.rstrip('/')}{path}"
|
url = f"{base_url.rstrip('/')}{path}"
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
headers = {"X-API-Key": f"{token}"}
|
||||||
resp = requests.get(url, headers=headers, timeout=timeout)
|
resp = requests.get(url, headers=headers, timeout=timeout)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
def api_post(base_url, path, token, data, timeout=20):
|
def api_post(base_url, path, token, data, timeout=120):
|
||||||
"""Example authenticated GET request to Portainer API."""
|
"""Example authenticated GET request to Portainer API."""
|
||||||
url = f"{base_url.rstrip('/')}{path}"
|
url = f"{base_url.rstrip('/')}{path}"
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
headers = {"X-API-Key": f"{token}"}
|
||||||
resp = requests.post(url, headers=headers, json=data, timeout=timeout)
|
resp = requests.post(url, headers=headers, json=data, timeout=timeout)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
|
def api_delete(base_url, path, token, timeout=20):
|
||||||
|
"""Example authenticated DELETE request to Portainer API."""
|
||||||
|
url = f"{base_url.rstrip('/')}{path}"
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
headers = {"X-API-Key": f"{token}"}
|
||||||
|
resp = requests.delete(url, headers=headers, timeout=timeout)
|
||||||
|
resp.raise_for_status()
|
||||||
|
print(resp.status_code)
|
||||||
|
return resp.status_code
|
||||||
|
|
||||||
def get_stacks(base_url, token, endpoint_id=None, timeout=10):
|
def get_stacks(base_url, token, endpoint_id=None, timeout=10):
|
||||||
"""
|
"""
|
||||||
@@ -91,7 +111,7 @@ def get_stack(base_url, identifier, token, endpoint_id=None, timeout=10):
|
|||||||
|
|
||||||
raise ValueError(f"Stack not found: {identifier}")
|
raise ValueError(f"Stack not found: {identifier}")
|
||||||
|
|
||||||
def create_stack(base_url, token, endpoint_id=None, data={}, timeout=10):
|
def create_stack(base_url, token, endpoint_id=None, data={}, timeout=120):
|
||||||
"""
|
"""
|
||||||
Return a list of stacks. If endpoint_id is provided, it will be added as a query param.
|
Return a list of stacks. If endpoint_id is provided, it will be added as a query param.
|
||||||
"""
|
"""
|
||||||
@@ -101,7 +121,7 @@ def create_stack(base_url, token, endpoint_id=None, data={}, timeout=10):
|
|||||||
try:
|
try:
|
||||||
stacks = api_post(base_url, path, token, data, timeout=timeout)
|
stacks = api_post(base_url, path, token, data, timeout=timeout)
|
||||||
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.")
|
||||||
return []
|
return []
|
||||||
@@ -109,43 +129,133 @@ def create_stack(base_url, token, endpoint_id=None, data={}, timeout=10):
|
|||||||
return []
|
return []
|
||||||
return stacks
|
return stacks
|
||||||
|
|
||||||
|
def delete_stack(base_url, token, endpoint_id=None, stack=None, timeout=30):
|
||||||
|
"""
|
||||||
|
Return a list of stacks. If endpoint_id is provided, it will be added as a query param.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if stack == "all":
|
||||||
|
stacks = get_stacks(base_url, token, endpoint_id=endpoint_id, timeout=timeout)
|
||||||
|
for s in stacks:
|
||||||
|
#print(s['EndpointId'], endpoint_id)
|
||||||
|
if int(s['EndpointId']) != int(endpoint_id):
|
||||||
|
continue
|
||||||
|
print("Deleting stack:", s['Name'])
|
||||||
|
path = f"/api/stacks/{s['Id']}"
|
||||||
|
if endpoint_id is not None:
|
||||||
|
path += f"?endpointId={endpoint_id}&removeVolumes=true"
|
||||||
|
#print(path)
|
||||||
|
out = api_delete(base_url, path, token, timeout=timeout)
|
||||||
|
return "Done"
|
||||||
|
else:
|
||||||
|
path = f"/api/stacks/{stack}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
#print(path)
|
||||||
|
stacks = api_delete(base_url, path, token, timeout=timeout)
|
||||||
|
except Exception as e:
|
||||||
|
#print(f"Error creating stack: {e}")
|
||||||
|
if "Conflict for url" in str(e):
|
||||||
|
print("Stack with this name may already exist.")
|
||||||
|
else:
|
||||||
|
print(f"Error deleting stack: {e}")
|
||||||
|
print(stacks)
|
||||||
|
return []
|
||||||
|
if stacks is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return stacks
|
||||||
|
|
||||||
|
def print_stacks(base, token):
|
||||||
|
stacks = get_stacks(base, token)
|
||||||
|
for stack in stacks:
|
||||||
|
try:
|
||||||
|
print(f"Stack ID: {stack['Id']}, Name: {stack['Name']}, EndpointName: {eps[stack['EndpointId']]}")
|
||||||
|
except KeyError as e:
|
||||||
|
print(f"Stack ID: {stack['Id']}, Name: {stack['Name']}, EndpointName: ?")
|
||||||
|
|
||||||
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.
|
||||||
base = os.getenv("PORTAINER_URL", "https://portainer.sectorq.eu")
|
base = os.getenv("PORTAINER_URL", "https://portainer.sectorq.eu")
|
||||||
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")
|
||||||
|
token = portainer_api_key
|
||||||
# Example: list endpoints
|
# Example: list endpoints
|
||||||
endpoints = api_get(base, "/api/endpoints", token)
|
endpoints = api_get(base, "/api/endpoints", token)
|
||||||
#print(endpoints)
|
#print(endpoints)
|
||||||
|
eps = {}
|
||||||
|
install_endpoint_id = None
|
||||||
for ep in endpoints:
|
for ep in endpoints:
|
||||||
|
eps[ep['Id']] = ep['Name']
|
||||||
print(f"Endpoint ID: {ep['Id']}, Name: {ep['Name']}")
|
print(f"Endpoint ID: {ep['Id']}, Name: {ep['Name']}")
|
||||||
|
if args.endpoint_id == str(ep['Name']):
|
||||||
|
install_endpoint_id = ep['Id']
|
||||||
|
if install_endpoint_id == None:
|
||||||
|
install_endpoint_id = args.endpoint_id
|
||||||
|
|
||||||
|
if args.list_stacks:
|
||||||
|
print_stacks(base, token)
|
||||||
|
|
||||||
|
|
||||||
stacks = get_stacks(base, token)
|
#print(json.dumps(req, indent=2))
|
||||||
for stack in stacks:
|
if args.create_stack:
|
||||||
print(f"Stack ID: {stack['Id']}, Name: {stack['Name']}")
|
stck = get_stack(base, args.stack_id, token)
|
||||||
|
print(f"Found stack: ID {stck['Id']}, Name: {stck['Name']}")
|
||||||
stck = get_stack(base, args.stack_id, token)
|
print(json.dumps(stck, indent=2))
|
||||||
|
|
||||||
|
for e in stck["Env"]:
|
||||||
|
print(f"Env: {e['name']} = {e['value']}")
|
||||||
print(f"Found stack: ID {stck['Id']}, Name: {stck['Name']}")
|
HWS = ["HW_MODE","HW_MODE1","HW_MODE2"]
|
||||||
print(json.dumps(stck, indent=2))
|
if e['name'] in HWS:
|
||||||
|
print("Found HW_MODE env var.")
|
||||||
uid = str(uuid.uuid4())
|
if args.gpu:
|
||||||
try:
|
e['value'] = "hw"
|
||||||
stck["AutoUpdate"]["Webhook"] = uid
|
else:
|
||||||
except:
|
e['value'] = "cpu"
|
||||||
stck["AutoUpdate"] = {}
|
if e['name'] == "LOGGING":
|
||||||
try:
|
print("Found LOGGING env var.")
|
||||||
req = {
|
if args.gpu:
|
||||||
|
e['value'] = "journald"
|
||||||
|
else:
|
||||||
|
e['value'] = "syslog"
|
||||||
|
#print(json.dumps(stck, indent=2))
|
||||||
|
uid = str(uuid.uuid4())
|
||||||
|
try:
|
||||||
|
stck["AutoUpdate"]["Webhook"] = uid
|
||||||
|
except:
|
||||||
|
stck["AutoUpdate"] = None
|
||||||
|
try:
|
||||||
|
req = {
|
||||||
|
"Name": stck["Name"],
|
||||||
|
"Env": stck["Env"],
|
||||||
|
"AdditionalFiles": stck["AdditionalFiles"],
|
||||||
|
"AutoUpdate": stck["AutoUpdate"],
|
||||||
|
"repositoryURL": stck["GitConfig"]["URL"],
|
||||||
|
"ReferenceName": "refs/heads/main",
|
||||||
|
"composeFile": f"{stck['Name']}/docker-compose.yml",
|
||||||
|
"ConfigFilePath": f"{stck['Name']}/docker-compose.yml",
|
||||||
|
"repositoryAuthentication": True,
|
||||||
|
"repositoryUsername": "jaydee",
|
||||||
|
"repositoryPassword": "glpat-uj-n-eEfTY398PE4vKSS",
|
||||||
|
"AuthorizationType": 0,
|
||||||
|
"TLSSkipVerify": False,
|
||||||
|
"supportRelativePath": True,
|
||||||
|
"repositoryAuthentication": True,
|
||||||
|
"fromAppTemplate": False,
|
||||||
|
"registries": [6,3],
|
||||||
|
"FromAppTemplate": False,
|
||||||
|
"Namespace": "",
|
||||||
|
"CreatedByUserId": "",
|
||||||
|
"Webhook": "",
|
||||||
|
"filesystemPath": "/share/docker_data/portainer/portainer-data/stacks",
|
||||||
|
"RegistryID": 4
|
||||||
|
}
|
||||||
|
except:
|
||||||
|
req = {
|
||||||
"Name": stck["Name"],
|
"Name": stck["Name"],
|
||||||
"Env": stck["Env"],
|
"Env": stck["Env"],
|
||||||
"AdditionalFiles": stck["AdditionalFiles"],
|
"AdditionalFiles": stck["AdditionalFiles"],
|
||||||
"AutoUpdate": stck["AutoUpdate"],
|
"AutoUpdate": None,
|
||||||
"repositoryURL": stck["GitConfig"]["URL"],
|
"repositoryURL": "https://gitlab.sectorq.eu/home/docker-compose.git",
|
||||||
"ReferenceName": "refs/heads/main",
|
"ReferenceName": "refs/heads/main",
|
||||||
"composeFile": f"{stck['Name']}/docker-compose.yml",
|
"composeFile": f"{stck['Name']}/docker-compose.yml",
|
||||||
"ConfigFilePath": f"{stck['Name']}/docker-compose.yml",
|
"ConfigFilePath": f"{stck['Name']}/docker-compose.yml",
|
||||||
@@ -157,42 +267,103 @@ if __name__ == "__main__":
|
|||||||
"supportRelativePath": True,
|
"supportRelativePath": True,
|
||||||
"repositoryAuthentication": True,
|
"repositoryAuthentication": True,
|
||||||
"fromAppTemplate": False,
|
"fromAppTemplate": False,
|
||||||
"registries": [6],
|
"registries": [6,3],
|
||||||
"FromAppTemplate": False,
|
"FromAppTemplate": False,
|
||||||
"Namespace": "",
|
"Namespace": "",
|
||||||
"CreatedByUserId": "",
|
"CreatedByUserId": "",
|
||||||
"Webhook": "",
|
"Webhook": "",
|
||||||
"filesystemPath": "/tmp",
|
"filesystemPath": "/share/docker_data/portainer/portainer-data/stacks",
|
||||||
"RegistryID": 4
|
"RegistryID": 6
|
||||||
}
|
}
|
||||||
except:
|
print(json.dumps(req, indent=2))
|
||||||
req = {
|
create_stack(base, token, install_endpoint_id, data=req)
|
||||||
"Name": stck["Name"],
|
|
||||||
"Env": stck["Env"],
|
|
||||||
"AdditionalFiles": stck["AdditionalFiles"],
|
|
||||||
"AutoUpdate": None,
|
|
||||||
"repositoryURL": "https://gitlab.sectorq.eu/home/docker-compose.git",
|
|
||||||
"ReferenceName": "refs/heads/main",
|
|
||||||
"composeFile": f"{stck['Name']}/docker-compose.yml",
|
|
||||||
"ConfigFilePath": f"{stck['Name']}/docker-compose.yml",
|
|
||||||
"repositoryAuthentication": True,
|
|
||||||
"repositoryUsername": "jaydee",
|
|
||||||
"repositoryPassword": "glpat-uj-n-eEfTY398PE4vKSS",
|
|
||||||
"AuthorizationType": 0,
|
|
||||||
"TLSSkipVerify": False,
|
|
||||||
"supportRelativePath": True,
|
|
||||||
"repositoryAuthentication": True,
|
|
||||||
"fromAppTemplate": False,
|
|
||||||
"registries": [6],
|
|
||||||
"FromAppTemplate": False,
|
|
||||||
"Namespace": "",
|
|
||||||
"CreatedByUserId": "",
|
|
||||||
"Webhook": "",
|
|
||||||
"filesystemPath": "/tmp",
|
|
||||||
"RegistryID": 6
|
|
||||||
}
|
|
||||||
print(json.dumps(req, indent=2))
|
|
||||||
if args.create_stack:
|
|
||||||
create_stack(base, token, 41, data=req)
|
|
||||||
else:
|
else:
|
||||||
print("Not creating stack, --create-stack not provided.")
|
print("Not creating stack, --create-stack not provided.")
|
||||||
|
if args.create_stacks:
|
||||||
|
for ns in nas_stacks:
|
||||||
|
stck = get_stack(base, ns, token)
|
||||||
|
print(f"Found stack: ID {stck['Id']}, Name: {stck['Name']}")
|
||||||
|
#print(json.dumps(stck, indent=2))
|
||||||
|
for e in stck["Env"]:
|
||||||
|
#print(f"Env: {e['name']} = {e['value']}")
|
||||||
|
HWS = ["HW_MODE","HW_MODE1","HW_MODE2"]
|
||||||
|
if e['name'] in HWS:
|
||||||
|
print("Found HW_MODE env var.")
|
||||||
|
if args.gpu:
|
||||||
|
e['value'] = "hw"
|
||||||
|
else:
|
||||||
|
e['value'] = "cpu"
|
||||||
|
if e['name'] == "LOGGING":
|
||||||
|
print("Found LOGGING env var.")
|
||||||
|
if args.gpu:
|
||||||
|
e['value'] = "journald"
|
||||||
|
else:
|
||||||
|
e['value'] = "syslog"
|
||||||
|
#print(stck["Env"])
|
||||||
|
uid = str(uuid.uuid4())
|
||||||
|
try:
|
||||||
|
stck["AutoUpdate"]["Webhook"] = uid
|
||||||
|
except:
|
||||||
|
stck["AutoUpdate"] = {}
|
||||||
|
try:
|
||||||
|
req = {
|
||||||
|
"Name": stck["Name"],
|
||||||
|
"Env": stck["Env"],
|
||||||
|
"AdditionalFiles": stck["AdditionalFiles"],
|
||||||
|
"AutoUpdate": stck["AutoUpdate"],
|
||||||
|
"repositoryURL": stck["GitConfig"]["URL"],
|
||||||
|
"ReferenceName": "refs/heads/main",
|
||||||
|
"composeFile": f"{stck['Name']}/docker-compose.yml",
|
||||||
|
"ConfigFilePath": f"{stck['Name']}/docker-compose.yml",
|
||||||
|
"repositoryAuthentication": True,
|
||||||
|
"repositoryUsername": "jaydee",
|
||||||
|
"repositoryPassword": "glpat-uj-n-eEfTY398PE4vKSS",
|
||||||
|
"AuthorizationType": 0,
|
||||||
|
"TLSSkipVerify": False,
|
||||||
|
"supportRelativePath": True,
|
||||||
|
"repositoryAuthentication": True,
|
||||||
|
"fromAppTemplate": False,
|
||||||
|
"registries": [6,3],
|
||||||
|
"FromAppTemplate": False,
|
||||||
|
"Namespace": "",
|
||||||
|
"CreatedByUserId": "",
|
||||||
|
"Webhook": "",
|
||||||
|
"filesystemPath": "/share/docker_data/portainer/portainer-data/stacks",
|
||||||
|
"RegistryID": 4
|
||||||
|
}
|
||||||
|
except:
|
||||||
|
req = {
|
||||||
|
"Name": stck["Name"],
|
||||||
|
"Env": stck["Env"],
|
||||||
|
"AdditionalFiles": stck["AdditionalFiles"],
|
||||||
|
"AutoUpdate": None,
|
||||||
|
"repositoryURL": "https://gitlab.sectorq.eu/home/docker-compose.git",
|
||||||
|
"ReferenceName": "refs/heads/main",
|
||||||
|
"composeFile": f"{stck['Name']}/docker-compose.yml",
|
||||||
|
"ConfigFilePath": f"{stck['Name']}/docker-compose.yml",
|
||||||
|
"repositoryAuthentication": True,
|
||||||
|
"repositoryUsername": "jaydee",
|
||||||
|
"repositoryPassword": "glpat-uj-n-eEfTY398PE4vKSS",
|
||||||
|
"AuthorizationType": 0,
|
||||||
|
"TLSSkipVerify": False,
|
||||||
|
"supportRelativePath": True,
|
||||||
|
"repositoryAuthentication": True,
|
||||||
|
"fromAppTemplate": False,
|
||||||
|
"registries": [6,3],
|
||||||
|
"FromAppTemplate": False,
|
||||||
|
"Namespace": "",
|
||||||
|
"CreatedByUserId": "",
|
||||||
|
"Webhook": "",
|
||||||
|
"filesystemPath": "/share/docker_data/portainer/portainer-data/stacks",
|
||||||
|
"RegistryID": 6
|
||||||
|
}
|
||||||
|
print(f"Creating stack: {ns}")
|
||||||
|
create_stack(base, token, install_endpoint_id, data=req)
|
||||||
|
else:
|
||||||
|
print("Not creating stack, --create-stack not provided.")
|
||||||
|
|
||||||
|
if args.delete_stack:
|
||||||
|
delete_stack(base, token, install_endpoint_id, args.stack_id)
|
||||||
|
else:
|
||||||
|
print("Not deleting stack, --create-stack not provided.")
|
||||||
|
|
||||||
Reference in New Issue
Block a user