diff --git a/port.py b/port.py index 3cf37a5..3c34165 100644 --- a/port.py +++ b/port.py @@ -472,230 +472,236 @@ class Portainer: self.stack_name = s.get("Name") self.stack_ids.append(s.get("Id")) return s - - print(ValueError(f"Stack not found: {stack}")) - sys.exit(1) + RED = "\033[91m" + RESET = "\033[0m" + print(ValueError(f"{RED}✗{RESET} >> Stack not found: {stack}")) + return 1 def create_stack( self, endpoint, - stack=None, + stacks=None, mode="git", autostart=False, stack_mode="swarm", ): - if stack_mode == "swarm": - swarm_id = self.get_swarm_id(endpoint) - p = "swarm" - env_path = f"{self.repo_dir}/__swarm/{stack}/.env" - else: - p = "standalone" - env_path = f"{self.repo_dir}/{stack}/.env" - # input(swarm_id) - self.endpoint_id = self.get_endpoint_id(endpoint) - if os.path.exists(self.repo_dir): - shutil.rmtree(self.repo_dir) - else: - print(f"Folder '{self.repo_dir}' does not exist.") - Repo.clone_from(self.git_url, self.repo_dir) - if mode == "git": - path = f"/stacks/create/{p}/repository" - print(p) - if self.endpoint_id is not None: - path += f"?endpointId={self.endpoint_id}" - - if stack == "all": - if self.endpoint_name == "rack": - stacks = self.rack_stacks - elif self.endpoint_name == "m-server": - stacks = self.m_server_stacks - elif self.endpoint_name == "rpi5": - stacks = self.rpi5_stacks - elif self.endpoint_name == "nas": - stacks = self.nas_stacks + + + for stack in stacks: + if stack_mode == "swarm": + swarm_id = self.get_swarm_id(endpoint) + p = "swarm" + env_path = f"{self.repo_dir}/__swarm/{stack}/.env" else: - stacks = [stack] - # print(json.dumps(self.stacks_all, indent=2)) - # input(json.dumps(self.stacks_all, indent=2)) - for stack in stacks: - if self.endpoint_id in self.stacks_all: + p = "standalone" + env_path = f"{self.repo_dir}/{stack}/.env" + # input(swarm_id) + self.endpoint_id = self.get_endpoint_id(endpoint) + if os.path.exists(self.repo_dir): + shutil.rmtree(self.repo_dir) + else: + print(f"Folder '{self.repo_dir}' does not exist.") + Repo.clone_from(self.git_url, self.repo_dir) + if mode == "git": + path = f"/stacks/create/{p}/repository" + # print(p) + if self.endpoint_id is not None: + path += f"?endpointId={self.endpoint_id}" - # 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") - continue - print(f"Working on {stack} , stack mode: {stack_mode}") - - envs = [] - if os.path.exists(f"{env_path}"): - f = open(f"{env_path}", "r") - env_vars = f.read().splitlines() - for ev in env_vars: - if ev.startswith("#") or ev.strip() == "": - continue - if "=" in ev: - name, value = ev.split("=", 1) - envs.append({"name": name, "value": value}) - f.close() - # wl(envs) - for e in envs: - # print(f"Env: {e['name']} = {e['value']}") - HWS = ["HW_MODE", "HW_MODE1", "HW_MODE2"] - if e["name"] == "RESTART" and self.endpoint_name == "m-server": - e["value"] = "always" - if e["name"] in HWS: - # print("Found HW_MODE env var.") - if self.hw_mode: - e["value"] = "hw" - else: - e["value"] = "cpu" - if e["name"] == "LOGGING": - # print("Found LOGGING env var.") - if self.log_mode: - e["value"] = "journald" - else: - e["value"] = "syslog" - - uid = uuid.uuid4() - # print(uid) - req = { - "Name": stack, - "Env": envs, - "AdditionalFiles": [], - "AutoUpdate": { - "forcePullImage": True, - "forceUpdate": False, - "webhook": f"{uid}", - }, - "repositoryURL": "https://gitlab.sectorq.eu/home/docker-compose.git", - "ReferenceName": "refs/heads/main", - "composeFile": f"{stack}/docker-compose.yml", - "ConfigFilePath": f"{stack}/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/", - "RegistryID": 4, - "isDetachedFromGit": True, - "method": "repository", - "swarmID": None, - } - if stack_mode == "swarm": - req["type"] = "swarm" - req["swarmID"] = swarm_id - req["composeFile"] = f"__swarm/{stack}/{stack}-swarm.yml" - req["ConfigFilePath"] = f"__swarm/{stack}/{stack}-swarm.yml" - if self._debug: - print(json.dumps(req)) - res = self._api_post(path, req) - if "Id" in res: - # print("Deploy request OK") - pass + if stack == "all": + if self.endpoint_name == "rack": + stacks = self.rack_stacks + elif self.endpoint_name == "m-server": + stacks = self.m_server_stacks + elif self.endpoint_name == "rpi5": + stacks = self.rpi5_stacks + elif self.endpoint_name == "nas": + stacks = self.nas_stacks else: - print(res) - tries = 0 - created = False - while True: - try: - # print(self.endpoint_id) - # print(stack) - self.get_stack(stack, self.endpoint_id) - created = True - break - except Exception as e: - print( - f"Waiting for stack {stack} to be created...{tries}/50", - end="\r", + stacks = [stack] + # print(json.dumps(self.stacks_all, indent=2)) + # input(json.dumps(self.stacks_all, indent=2)) + for stack in stacks: + if self.endpoint_id in self.stacks_all: + + # 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"] ) - time.sleep(10) - tries += 1 - if tries > 50: - print( - f"Error retrieving stack {stack} after creation: {self.endpoint_name}" - ) - break - logger.debug(f"Exception while getting stack {stack}: {e}") - - if created: - if stack != "pihole": - # print(autostart) - if not autostart: - # self.get_stacks() - # self.stop_stack(stack,self.endpoint_id) - conts = self.get_containers(self.endpoint_name, stack) - # print(conts) - self.stop_containers(self.endpoint_name, conts) - - if mode == "file": - print("Creating new stack from file...") - path = "/stacks/create/standalone/file" - if self.endpoint_id is not None: - path += f"?endpointId={self.endpoint_id}" - - if stack == "all": - if self.endpoint_name == "rack": - stacks = self.rack_stacks - elif self.endpoint_name == "m-server": - stacks = self.m_server_stacks - elif self.endpoint_name == "rpi5": - stacks = self.rpi5_stacks - else: - stacks = [stack] - for stack in stacks: - print(f"Working on {stack}") - if os.path.exists(f"{self.repo_dir}/{stack}/.env"): - f = open(f"{self.repo_dir}/{stack}/.env", "r") - - env_vars = f.read().splitlines() - envs = [] - for ev in env_vars: - if ev.startswith("#") or ev.strip() == "": + if stack_check: + GREEN = "\033[92m" + RESET = "\033[0m" + print(f"{GREEN}✓{RESET} >> Stack {stack} already exist") continue - if "=" in ev: - name, value = ev.split("=", 1) - envs.append({"name": name, "value": value}) - f.close() - # wl(envs) - for e in envs: - # print(f"Env: {e['name']} = {e['value']}") - HWS = ["HW_MODE", "HW_MODE1", "HW_MODE2"] - if e["name"] == "RESTART" and self.endpoint_name == "m-server": - e["value"] = "always" - if e["name"] in HWS: - print("Found HW_MODE env var.") - if self.hw_mode: - e["value"] = "hw" - else: - e["value"] = "cpu" - if e["name"] == "LOGGING": - print("Found LOGGING env var.") - if self.log_mode: - e["value"] = "journald" - else: - e["value"] = "syslog" + print(f"Working on {stack} , stack mode: {stack_mode}") - file = { - # ("filename", file_object) - "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) + envs = [] + if os.path.exists(f"{env_path}"): + f = open(f"{env_path}", "r") + env_vars = f.read().splitlines() + for ev in env_vars: + if ev.startswith("#") or ev.strip() == "": + continue + if "=" in ev: + name, value = ev.split("=", 1) + envs.append({"name": name, "value": value}) + f.close() + # wl(envs) + for e in envs: + # print(f"Env: {e['name']} = {e['value']}") + HWS = ["HW_MODE", "HW_MODE1", "HW_MODE2"] + if e["name"] == "RESTART" and self.endpoint_name == "m-server": + e["value"] = "always" + if e["name"] in HWS: + # print("Found HW_MODE env var.") + if self.hw_mode: + e["value"] = "hw" + else: + e["value"] = "cpu" + if e["name"] == "LOGGING": + # print("Found LOGGING env var.") + if self.log_mode: + e["value"] = "journald" + else: + e["value"] = "syslog" + + uid = uuid.uuid4() + # print(uid) + req = { + "Name": stack, + "Env": envs, + "AdditionalFiles": [], + "AutoUpdate": { + "forcePullImage": True, + "forceUpdate": False, + "webhook": f"{uid}", + }, + "repositoryURL": "https://gitlab.sectorq.eu/home/docker-compose.git", + "ReferenceName": "refs/heads/main", + "composeFile": f"{stack}/docker-compose.yml", + "ConfigFilePath": f"{stack}/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/", + "RegistryID": 4, + "isDetachedFromGit": True, + "method": "repository", + "swarmID": None, + } + if stack_mode == "swarm": + req["type"] = "swarm" + req["swarmID"] = swarm_id + req["composeFile"] = f"__swarm/{stack}/{stack}-swarm.yml" + req["ConfigFilePath"] = f"__swarm/{stack}/{stack}-swarm.yml" + if self._debug: + print(json.dumps(req)) + res = self._api_post(path, req) + if "Id" in res: + # print("Deploy request OK") + pass + else: + print(res) + tries = 0 + created = False + while True: + try: + # print(self.endpoint_id) + # print(stack) + if self.get_stack(stack, self.endpoint_id) != 1: + created = True + break + except Exception as e: + print( + f"Waiting for stack {stack} to be created...{tries}/50", + end="\r", + ) + time.sleep(10) + tries += 1 + if tries > 50: + print( + f"Error retrieving stack {stack} after creation: {self.endpoint_name}" + ) + break + logger.debug(f"Exception while getting stack {stack}: {e}") + + if created: + if stack != "pihole": + # print(autostart) + if not autostart: + # self.get_stacks() + # self.stop_stack(stack,self.endpoint_id) + conts = self.get_containers(self.endpoint_name, stack) + # print(conts) + self.stop_containers(self.endpoint_name, conts) + + if mode == "file": + print("Creating new stack from file...") + path = "/stacks/create/standalone/file" + if self.endpoint_id is not None: + path += f"?endpointId={self.endpoint_id}" + + if stack == "all": + if self.endpoint_name == "rack": + stacks = self.rack_stacks + elif self.endpoint_name == "m-server": + stacks = self.m_server_stacks + elif self.endpoint_name == "rpi5": + stacks = self.rpi5_stacks + else: + stacks = [stack] + for stack in stacks: + print(f"Working on {stack}") + if os.path.exists(f"{self.repo_dir}/{stack}/.env"): + f = open(f"{self.repo_dir}/{stack}/.env", "r") + + env_vars = f.read().splitlines() + envs = [] + for ev in env_vars: + if ev.startswith("#") or ev.strip() == "": + continue + if "=" in ev: + name, value = ev.split("=", 1) + envs.append({"name": name, "value": value}) + f.close() + # wl(envs) + for e in envs: + # print(f"Env: {e['name']} = {e['value']}") + HWS = ["HW_MODE", "HW_MODE1", "HW_MODE2"] + if e["name"] == "RESTART" and self.endpoint_name == "m-server": + e["value"] = "always" + if e["name"] in HWS: + print("Found HW_MODE env var.") + if self.hw_mode: + e["value"] = "hw" + else: + e["value"] = "cpu" + if e["name"] == "LOGGING": + print("Found LOGGING env var.") + if self.log_mode: + e["value"] = "journald" + else: + e["value"] = "syslog" + + file = { + # ("filename", file_object) + "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) def print_stacks(self, endpoint="all"): """Print a table of stacks, optionally filtered by endpoint.""" @@ -851,13 +857,17 @@ class Portainer: """Delete one stack or all stacks on an endpoint.""" self._resolve_endpoint(endpoint_id) endpoint_id = self.endpoint_id - - stack_id = self._resolve_stack_id(stack, endpoint_id) + + + if stack == "all": return self._delete_all_stacks(endpoint_id) - - return self._delete_single_stack(stack_id, endpoint_id) + else: + for s in stack: + stack_id = self._resolve_stack_id(s, endpoint_id) + self._delete_single_stack(stack_id, endpoint_id) + return "Done" # def delete_stack(self, endpoint_id=None, stack=None): # """ diff --git a/portainer.py b/portainer.py index 9c484f9..5a3ed50 100755 --- a/portainer.py +++ b/portainer.py @@ -141,7 +141,7 @@ 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("--stack", "-s", type=str, nargs="+", help="Stack ID for operations") parser.add_argument( "--token-only", action="store_true", help="Print auth token and exit" ) @@ -152,7 +152,6 @@ 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: @@ -272,12 +271,13 @@ def prompt_missing_args(args_in, defaults_in, fields): ) try: if field == "stack": - result = checkboxlist_dialog( + commands_tuples = [(cmd, cmd) for cmd in commands] + value_in = checkboxlist_dialog( title="Select Services", text="Choose one or more services:", - values=commands, + values=commands_tuples, ).run() - input(result) + print(" >> Stacks :", ",".join(value_in)) else: value_in = ( prompt( @@ -316,9 +316,14 @@ def prompt_missing_args(args_in, defaults_in, fields): print("Value entered:", value_in) defaults_in[f"PORTAINER_{field}".upper()] = value_in setattr(args, field, value_in) - os.environ[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( @@ -375,8 +380,14 @@ if __name__ == "__main__": if args.debug: por._debug = True if args.action == "secrets": - if args.endpoint_id is None: - args.endpoint_id = input("Endpoint ID is required for creating secrets : ") + args = prompt_missing_args( + args, + cur_config, + [ + ("site", "Site"), + ("endpoint_id", "Endpoint ID"), + ], + ) secrets = { "gitea_runner_registration_token": "8nmKqJhkvYwltmNfF2o9vs0tzo70ufHSQpVg6ymb",