diff --git a/abst/__version__.py b/abst/__version__.py index 4012eff..c6ecb42 100644 --- a/abst/__version__.py +++ b/abst/__version__.py @@ -10,7 +10,7 @@ "CLI Command making OCI Bastion and kubernetes usage simple and fast" ) -__version__ = "2.1.5" +__version__ = "2.1.6" __author__ = "Jiri Otoupal" __author_email__ = "jiri-otoupal@ips-database.eu" __license__ = "MIT" diff --git a/abst/bastion_support/oci_bastion.py b/abst/bastion_support/oci_bastion.py index c2f6f05..b343431 100644 --- a/abst/bastion_support/oci_bastion.py +++ b/abst/bastion_support/oci_bastion.py @@ -257,6 +257,7 @@ def load_response(self, res): def create_bastion_forward_port_session(self, creds): ssh_key_path = self.get_ssh_pub_key_path(creds) + cfg = Bastion.load_config() if "region" not in creds.keys(): rich.print("Missing region, will use profile default") @@ -266,12 +267,17 @@ def create_bastion_forward_port_session(self, creds): rich.print("[red]SSH key has invalid path[/red]") exit(1) + ssh_pub_path = creds.get(ssh_key_path, cfg.get("ssh-pub-path", "No Public key supplied")) + + if ssh_pub_path == "No Public key supplied": + raise Exception(ssh_pub_path) + res = self.__create_bastion_session_port_forward(creds["bastion-id"], creds["target-ip"], f'{creds["default-name"]}-ctx-' f'{self.get_print_name()}', int(creds["target-port"]), - ssh_key_path, + ssh_pub_path, int(creds["ttl"]), False, creds.get("region", None)) try: trs = Bastion.parse_response(res) @@ -280,10 +286,11 @@ def create_bastion_forward_port_session(self, creds): logging.debug(f"Added session id of {self.context_name}") except: pass - return creds["host"], creds["target-ip"], creds["target-port"], creds["ssh-pub-path"], res + return creds["host"], creds["target-ip"], creds["target-port"], ssh_pub_path, res def create_bastion_ssh_session_managed(self, creds): ssh_key_path = self.get_ssh_pub_key_path(creds) + cfg = Bastion.load_config() if "region" not in creds.keys(): rich.print("Missing region, will use profile default") @@ -293,13 +300,18 @@ def create_bastion_ssh_session_managed(self, creds): rich.print("[red]SSH key has invalid path[/red]") exit(1) + ssh_pub_path = creds.get(ssh_key_path, cfg.get("ssh-pub-path", "No Public key supplied")) + + if ssh_pub_path == "No Public key supplied": + raise Exception(ssh_pub_path) + try: res = self.__create_bastion_ssh_session_managed(creds["bastion-id"], creds["resource-id"], f'{creds["default-name"]}-ctx-' f'{self.get_print_name()}', creds["resource-os-username"], - ssh_key_path, + ssh_pub_path, int(creds["ttl"]), False, creds.get("region", None) ) try: @@ -368,11 +380,22 @@ def load_json(cls, path=default_creds_path) -> dict: @classmethod def create_default_location(cls): + Path(default_creds_path.parent).mkdir(exist_ok=True) Path(default_contexts_location).mkdir(exist_ok=True) if not default_conf_path.exists(): cls.write_creds_json(default_conf_contents, default_conf_path) + else: + something_changed = False + data = Bastion.load_config() + for key, value in default_conf_contents.items(): + if key not in data.keys(): + data[key] = value + something_changed = True + if something_changed: + cls.write_creds_json(default_conf_contents, default_conf_path) + rich.print("[green]Updated .abst/config file[/green]") @classmethod def write_creds_json(cls, td: dict, path: Path): diff --git a/abst/cli_commands/__init__.py b/abst/cli_commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/abst/cli_commands/config_cli/__init__.py b/abst/cli_commands/config_cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/abst/cli_commands/config_cli/commands.py b/abst/cli_commands/config_cli/commands.py new file mode 100644 index 0000000..aa49914 --- /dev/null +++ b/abst/cli_commands/config_cli/commands.py @@ -0,0 +1,107 @@ +import click +import rich +from InquirerPy import inquirer + +from abst.bastion_support.oci_bastion import Bastion +from abst.cfg_func import __upgrade +from abst.config import default_contexts_location +from abst.utils.misc_funcs import setup_calls +from abst.tools import get_context_path + + +@click.group(help="Group of commands for operations with config") +def config(): + pass + + +@config.command("generate", help="Will generate sample json and overwrite changes") +@click.option("--debug", is_flag=True, default=False) +@click.argument("context-name", default=None, required=False) +def generate(debug, context_name): + setup_calls(debug) + + path = get_context_path(context_name) + + Bastion.create_default_location() + td = Bastion.generate_sample_dict() + creds_path = Bastion.write_creds_json(td, path) + print( + f"Sample credentials generated, please fill 'creds.json' in {creds_path} with " + f"your credentials for this to work, you can use 'abst json fill " + f"{context_name if context_name else ''}'" + ) + + +@config.command( + "fill", help="Fills Json config with credentials you enter interactively" +) +@click.option("--debug", is_flag=True, default=False) +@click.argument("context-name", default=None, required=False) +def fill(debug, context_name): + setup_calls(debug) + + path = get_context_path(context_name) + + if not path.exists(): + rich.print("Generating sample Creds file") + Bastion.create_default_location() + td = Bastion.generate_sample_dict() + Bastion.write_creds_json(td, path) + + if not default_contexts_location.exists(): + rich.print("Generating contexts location") + Bastion.create_default_location() + + rich.print(f"[green]Filling {str(path)}") + rich.print("Please fill field one by one as displayed") + n_dict = dict() + + creds_json_ro = Bastion.load_json(path) + + for key, value in creds_json_ro.items(): + n_dict[key] = inquirer.text( + message=f"{key.capitalize()}:", default=value + ).execute() + rich.print("\n[red]New json looks like this:[/red]") + rich.print_json(data=n_dict) + if inquirer.confirm(message="Write New Json ?", default=False).execute(): + Bastion.write_creds_json(n_dict, path) + rich.print("[green]Wrote changes[/green]") + else: + rich.print("[red]Fill interrupted, nothing changed[/red]") + + +@config.command( + "locate", help="Locates Json config with credentials you enter interactively" +) +@click.option("--debug", is_flag=True, default=False) +@click.argument("context-name", default=None, required=False) +def locate(debug, context_name): + setup_calls(debug) + + path = get_context_path(context_name) + + if path.exists(): + rich.print(f"[green]Config file location: {path.absolute()}[/green]") + else: + rich.print( + f"[red]Config does not exist yet, future location" + f" {path.absolute()}[/red]" + ) + + +@config.command( + "upgrade", help="Locates Json config with credentials you enter interactively" +) +@click.option("--debug", is_flag=True, default=False) +@click.argument("context-name", default=None, required=False) +def upgrade(debug, context_name): + setup_calls(debug) + + path = get_context_path(context_name) + + if not path.exists(): + rich.print("[green]No config to upgrade[/green]") + return + + __upgrade(context_name, path) diff --git a/abst/cli_commands/context/__init__.py b/abst/cli_commands/context/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/abst/cli_commands/context/commands.py b/abst/cli_commands/context/commands.py new file mode 100644 index 0000000..8b0950e --- /dev/null +++ b/abst/cli_commands/context/commands.py @@ -0,0 +1,51 @@ +import logging +from pathlib import Path + +import click +import pyperclip +import rich + +from abst.config import default_contexts_location, share_excluded_keys +from abst.utils.misc_funcs import get_context_data, setup_calls + + +@click.group(help="Contexts commands") +def context(): + pass + + +@context.command("list", help="Will list all contexts in ~/.abst/context/ folder") +@click.option("--debug", is_flag=True, default=False) +def _list(debug=False): + setup_calls(debug) + rich.print("Contexts:") + for file in Path(default_contexts_location).iterdir(): + rich.print(f" {file.name.replace('.json', '')}") + + +@context.command(help="Will display JSON format of context") +@click.option("--debug", is_flag=True, default=False) +@click.argument("name") +def display(name, debug=False): + setup_calls(debug) + data = get_context_data(name) + if data is None: + return + rich.print_json(data=data) + + +@context.command(help="Will print context without local paths and put it in clipboard for sharing") +@click.option("--debug", is_flag=True, default=False) +@click.argument("name") +def share(name, debug=False): + setup_calls(debug) + + data = get_context_data(name) + if data is None: + return + for key in share_excluded_keys: + data.pop(key) + data["default-name"] = "!YOUR NAME!" + rich.print_json(data=data) + logging.info("Data transmitted into clipboard") + pyperclip.copy(str(data)) diff --git a/abst/cli_commands/cp_cli/__init__.py b/abst/cli_commands/cp_cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/abst/cli_commands/cp_cli/commands.py b/abst/cli_commands/cp_cli/commands.py new file mode 100644 index 0000000..2abb462 --- /dev/null +++ b/abst/cli_commands/cp_cli/commands.py @@ -0,0 +1,42 @@ +import os + +import click +import rich + +from abst.utils.misc_funcs import setup_calls + + +@click.group("cp", help="Copy commands for special usage") +def cp(): + pass + + +@cp.command("login", help="Login to docker and helm registry") +@click.argument("secret_name") +@click.argument("source_namespace") +@click.argument("target_namespace") +@click.option("--debug", is_flag=True, default=False) +def cp_secret( + secret_name: str, + target_namespace: str, + source_namespace: str = "default", + debug=False, +): + """ + Copy Secret in current cluster from source namespace to target + @param secret_name: Secret Name + @param target_namespace: Target Namespace name + @param source_namespace: Source Namespace name + @return: + :param debug: + """ + setup_calls(debug) + try: + rich.print("Trying Copy secret") + os.system( + f"kubectl get secret {secret_name} --namespace={source_namespace} -o yaml | sed " + f"'s/namespace: .*/namespace: {target_namespace}/' | kubectl apply -f -" + ) + except FileNotFoundError: + rich.print("[red]kubectl not found on this machine[/red]") + return diff --git a/abst/cli_commands/create_cli/__init__.py b/abst/cli_commands/create_cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/abst/cli_commands/create_cli/commands.py b/abst/cli_commands/create_cli/commands.py new file mode 100644 index 0000000..7c1803d --- /dev/null +++ b/abst/cli_commands/create_cli/commands.py @@ -0,0 +1,74 @@ +from time import sleep + +import click + +from abst.bastion_support.oci_bastion import Bastion +from abst.utils.misc_funcs import setup_calls, print_eligible + + +@click.group(help="Group of commands for creating Bastion sessions") +def create(): + pass + + +@create.command( + "forward", + help="Creates and connects to Bastion session indefinitely until terminated by user", +) +@click.option("--shell", is_flag=True, default=False) +@click.option("--debug", is_flag=True, default=False) +@click.argument("context-name", default=None, required=False) +def fullauto_forward(shell, debug, context_name): + """Creates and connects to bastion sessions + automatically until terminated""" + + setup_calls(debug) + + if context_name == "?": + print_eligible("required only for Port Forward session") + return + + if context_name is None: + conf = Bastion.load_config() + used_name = conf["used_context"] + else: + used_name = context_name + + while True: + Bastion(used_name, region= + Bastion.load_json(Bastion.get_creds_path_resolve(context_name)).get("region", + None)).create_forward_loop( + shell=shell) + + sleep(1) + + +@create.command( + "managed", + help="Creates and connects to Bastion session indefinitely until terminated by user", +) +@click.option("--shell", is_flag=True, default=False) +@click.option("--debug", is_flag=True, default=False) +@click.argument("context-name", default=None, required=False) +def fullauto_managed(shell, debug, context_name): + """Creates and connects to bastion sessions + automatically until terminated""" + setup_calls(debug) + + if context_name == "?": + print_eligible("required only for Managed SSH session") + return + + if context_name is None: + conf = Bastion.load_config() + used_name = conf["used_context"] + else: + used_name = context_name + + while True: + Bastion(used_name, region= + Bastion.load_json(Bastion.get_creds_path_resolve(context_name)).get("region", + None)).create_managed_loop( + shell=shell) + + sleep(1) diff --git a/abst/cli_commands/helm_cli/__init__.py b/abst/cli_commands/helm_cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/abst/cli_commands/helm_cli/commands.py b/abst/cli_commands/helm_cli/commands.py new file mode 100644 index 0000000..d479a02 --- /dev/null +++ b/abst/cli_commands/helm_cli/commands.py @@ -0,0 +1,120 @@ +import os +import subprocess + +import click +import rich +from InquirerPy import inquirer + +from abst.bastion_support.oci_bastion import Bastion +from abst.config import default_conf_path +from abst.dialogs import helm_login_dialog +from abst.utils.misc_funcs import setup_calls + + +@click.group("helm", help="Commands for helm and docker") +def helm(): + pass + + +@helm.command("login") +@click.option("--debug", is_flag=True, default=False) +@click.option( + "--edit", + default=False, + help="Edit credentials with index as argument (starts from 1)", +) +def helm_login(debug, edit): + """ + Copy Secret in current cluster from source namespace to target + @return: + :param debug: + """ + setup_calls(debug) + from subprocess import PIPE + + try: + _config = Bastion.load_config() + if "helm" not in _config.keys(): + _config["helm"] = [] + host, password, remote, username = helm_login_dialog() + _config["helm"].append( + { + "host": host, + "remote": remote, + "username": username, + "password": password, + } + ) + Bastion.write_creds_json(_config, default_conf_path) + rich.print("Added Credentials") + elif edit: + if "helm" not in _config.keys(): + _config["helm"] = [] + + host, password, remote, username = helm_login_dialog() + _config["helm"][edit - 1] = { + "host": host, + "remote": remote, + "username": username, + "password": password, + } + rich.print_json(data=_config["helm"][edit - 1]) + if inquirer.confirm(message="Write New Json ?", default=False).execute(): + Bastion.write_creds_json(_config, default_conf_path) + rich.print("Edited Credentials") + + choices = [ + {"name": f'{choice["remote"]} {choice["username"]}', "value": choice} + for choice in _config["helm"] + ] + + selected = inquirer.select("Select login", choices).execute() + rich.print("Trying to login") + p = subprocess.Popen( + f'helm registry login {selected["host"]} -u {selected["username"]} --password-stdin'.split( + " " + ), + stdout=PIPE, + stdin=PIPE, + stderr=PIPE, + ) + rich.print(p.communicate(input=selected["password"].encode())[0].decode()) + + except Exception as ex: + rich.print(f"[red]Exception during execution {ex}[/red]") + return + + +@helm.command("push") +@click.argument("chart") +@click.option("--debug", is_flag=True, default=False) +def helm_push(chart, debug): + """ + Copy Secret in current cluster from source namespace to target + @return: + :param debug: + :param chart: + :param debug: + """ + setup_calls(debug) + try: + _config = Bastion.load_config() + if "helm" not in _config.keys(): + _config["helm"] = [] + rich.print( + f"Please Fill in Repository details that" + f" are going to be saved in {default_conf_path}" + f" with command 'abst helm login'" + ) + choices = [ + {"name": f'{choice["remote"]} {choice["username"]}', "value": choice} + for choice in _config["helm"] + ] + + selected = inquirer.select("Select remote", choices).execute() + rich.print("Trying to push") + os.system(f'helm push {chart} {selected["remote"]}') + + except Exception as ex: + rich.print(f"[red]Exception during execution {ex}[/red]") + return diff --git a/abst/cli_commands/kubectl_cli/__init__.py b/abst/cli_commands/kubectl_cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/abst/cli_commands/kubectl_cli/commands.py b/abst/cli_commands/kubectl_cli/commands.py new file mode 100644 index 0000000..4f03861 --- /dev/null +++ b/abst/cli_commands/kubectl_cli/commands.py @@ -0,0 +1,93 @@ +import os +import re +import subprocess + +import click +import rich +from InquirerPy import inquirer + +from abst.utils.misc_funcs import setup_calls + + +@click.command("ssh", help="Will SSH into pod with containing string name") +@click.argument("pod_name") +@click.option("--debug", is_flag=True, default=False) +def ssh_pod(pod_name, debug): + setup_calls(debug) + + try: + rich.print("Fetching pods") + pod_lines = ( + subprocess.check_output(f"kubectl get pods -A".split(" ")) + .decode() + .split("\n") + ) + except FileNotFoundError: + rich.print("[red]kubectl not found on this machine[/red]") + return + + found = list(filter(lambda pod_line: pod_name in pod_line, pod_lines)) + + if len(found) > 1: + data = re.sub( + " +", + " ", + inquirer.select("Found more pods, choose one:", list(found)).execute(), + ).split(" ") + pod_name_precise = data[1] + elif len(found) == 1: + tmp = found.pop() + data = re.sub(" +", " ", tmp).split(" ") + pod_name_precise = data[1] + else: + rich.print(f"[red]No pods with name {pod_name} found[/red]") + return + + rich.print( + f"[green]Connecting to {pod_name_precise} in namespace: {data[0]}[/green]" + ) + os.system( + f"kubectl exec -n {data[0]} --stdin --tty {pod_name_precise} -- /bin/bash" + ) + + +@click.command("logs", help="Will get logs from a pod with containing string name") +@click.argument("pod_name") +@click.option("--debug", is_flag=True, default=False) +def log_pod(pod_name, debug): + setup_calls(debug) + + try: + rich.print("Fetching pods") + pod_lines = ( + subprocess.check_output(f"kubectl get pods -A".split(" ")) + .decode() + .split("\n") + ) + except FileNotFoundError: + rich.print("[red]kubectl not found on this machine[/red]") + return + + found = list(filter(lambda pod_line: pod_name in pod_line, pod_lines)) + + if len(found) > 1: + data = re.sub( + " +", + " ", + inquirer.select("Found more pods, choose one:", found).execute(), + ).split(" ") + pod_name_precise = data[1] + elif len(found) == 1: + tmp = found.pop() + data = re.sub(" +", " ", tmp).split(" ") + pod_name_precise = data[1] + else: + rich.print(f"[red]No pods with name {pod_name} found[/red]") + return + + rich.print( + f"[green]Getting logs from {pod_name_precise} in namespace: {data[0]}[/green]" + ) + os.system( + f"kubectl -n {data[0]} logs {pod_name_precise}" + ) diff --git a/abst/cli_commands/parallel/__init__.py b/abst/cli_commands/parallel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/abst/cli_commands/parallel/commands.py b/abst/cli_commands/parallel/commands.py new file mode 100644 index 0000000..20d8393 --- /dev/null +++ b/abst/cli_commands/parallel/commands.py @@ -0,0 +1,73 @@ +import click +import rich +from InquirerPy import inquirer + +from abst.bastion_support.bastion_scheduler import BastionScheduler +from abst.utils.misc_funcs import setup_calls +from abst.tools import display_scheduled + + +@click.group(help="Parallel Bastion Control group") +def parallel(): + """ + Only Port Forwarded sessions are supported + + This makes it possible to run multiple forward sessions of multiple context in + the same time, useful when working with multiple clusters + """ + pass + + +@parallel.command("add", help="Add Bastion to stack") +@click.option("--debug", is_flag=True, default=False) +@click.argument("context-name", default="default") +def add(debug, context_name): + """ + Add Bastions to stack + + use default for adding default cluster ! this is reserved name ! + :param debug: + :param context_name: + :return: + """ + setup_calls(debug) + + BastionScheduler.add_bastion(context_name) + display_scheduled() + + +@parallel.command("remove", help="Remove Bastion from stack") +@click.option("--debug", is_flag=True, default=False) +@click.argument("context-name", default="default") +def remove(debug, context_name): + setup_calls(debug) + BastionScheduler.remove_bastion(context_name) + display_scheduled() + + +@parallel.command("run", help="Run All Bastions in fullauto") +@click.option("--debug", is_flag=True, default=False) +@click.option("-y", is_flag=True, default=False) +def run(debug, y): + setup_calls(debug) + display_scheduled() + if not y: + try: + confirm = inquirer.confirm( + "Do you really want to run following contexts?" + ).execute() + except KeyError: + rich.print("Unknown inquirer error, continuing...") + confirm = True + if not confirm: + rich.print("[green]Cancelling, nothing started[/green]") + exit(0) + BastionScheduler.run() + + +@parallel.command("display", help="Display current Bastions is stack") +@click.option("--debug", is_flag=True, default=False) +def display(debug): + setup_calls(debug) + + display_scheduled() diff --git a/abst/config.py b/abst/config.py index 3e9e435..597e1b3 100644 --- a/abst/config.py +++ b/abst/config.py @@ -1,17 +1,22 @@ from pathlib import Path +# Default config default_stack_location: Path = (Path().home().resolve() / ".abst" / "stack.json") default_conf_path: Path = (Path().home().resolve() / ".abst" / "config.json") default_creds_path: Path = (Path().home().resolve() / ".abst" / "creds.json") default_config_keys: tuple = ( "host", "bastion-id", "default-name", "ssh-pub-path", "private-key-path", "target-ip", - "local-port", "target-port", "ttl", "resource-id", "resource-os-username", - "private-key-path", "region") + "local-port", "target-port", "ttl", "resource-id", "resource-os-username", "region") default_contexts_location: Path = (Path().home().resolve() / ".abst" / "contexts") default_stack_contents: dict = {"stack": []} -default_conf_contents: dict = {"used_context": None} +default_conf_contents: dict = {"used_context": None, "private-key-path": "~/.ssh/id_rsa", + "ssh-pub-path": "~/.ssh/id_rsa" + ".pub"} + +# Share Config +share_excluded_keys: tuple = ("private-key-path", "ssh-pub-path") def get_public_key(ssh_path): - with open(ssh_path, "r") as f: + with open(str(Path(ssh_path).expanduser().resolve()), "r") as f: return f.read() diff --git a/abst/main.py b/abst/main.py index eda6ee8..9fb4674 100644 --- a/abst/main.py +++ b/abst/main.py @@ -1,26 +1,24 @@ -import json import logging import os -import re import signal -import subprocess -from pathlib import Path -from time import sleep import click import rich from InquirerPy import inquirer -from requests import ConnectTimeout from rich.logging import RichHandler from abst.__version__ import __version_name__, __version__ from abst.bastion_support.bastion_scheduler import BastionScheduler from abst.bastion_support.oci_bastion import Bastion -from abst.cfg_func import __upgrade +from abst.cli_commands.config_cli.commands import config +from abst.cli_commands.context.commands import context +from abst.cli_commands.cp_cli.commands import cp +from abst.cli_commands.create_cli.commands import create +from abst.cli_commands.helm_cli.commands import helm +from abst.cli_commands.kubectl_cli.commands import ssh_pod, log_pod +from abst.cli_commands.parallel.commands import parallel from abst.config import default_creds_path, default_contexts_location, default_conf_path -from abst.dialogs import helm_login_dialog -from abst.notifier.version_notifier import Notifier -from abst.tools import get_context_path, display_scheduled +from abst.utils.misc_funcs import setup_calls @click.group() @@ -29,101 +27,6 @@ def cli(): pass -@cli.group(help="Group of commands for operations with config") -def config(): - pass - - -@cli.group(help="Parallel Bastion Control group") -def parallel(): - """ - Only Port Forwarded sessions are supported - - This makes it possible to run multiple forward sessions of multiple context in - the same time, useful when working with multiple clusters - """ - pass - - -@cli.group(help="Contexts commands") -def context(): - pass - - -@context.command("list", help="Will list all contexts in ~/.abst/context/ folder") -def _list(): - rich.print("Contexts:") - for file in Path(default_contexts_location).iterdir(): - rich.print(f" {file.name.replace('.json', '')}") - - -@context.command(help="Will display JSON format of context") -@click.argument("name") -def display(name): - if name in [file.name.replace(".json", "") for file in - Path(default_contexts_location).iterdir()]: - rich.print("[bold]Context config contents:[/bold]\n") - with open(Path(default_contexts_location) / (name + ".json"), "r") as f: - rich.print_json(data=json.load(f)) - else: - rich.print("[red]Context does not exists[/red]") - - -@parallel.command("add", help="Add Bastion to stack") -@click.option("--debug", is_flag=True, default=False) -@click.argument("context-name", default="default") -def add(debug, context_name): - """ - Add Bastions to stack - - use default for adding default cluster ! this is reserved name ! - :param debug: - :param context_name: - :return: - """ - setup_calls(debug) - - BastionScheduler.add_bastion(context_name) - display_scheduled() - - -@parallel.command("remove", help="Remove Bastion from stack") -@click.option("--debug", is_flag=True, default=False) -@click.argument("context-name", default="default") -def remove(debug, context_name): - setup_calls(debug) - BastionScheduler.remove_bastion(context_name) - display_scheduled() - - -@parallel.command("run", help="Run All Bastions in fullauto") -@click.option("--debug", is_flag=True, default=False) -@click.option("-y", is_flag=True, default=False) -def run(debug, y): - setup_calls(debug) - display_scheduled() - if not y: - try: - confirm = inquirer.confirm( - "Do you really want to run following contexts?" - ).execute() - except KeyError: - rich.print("Unknown inquirer error, continuing...") - confirm = True - if not confirm: - rich.print("[green]Cancelling, nothing started[/green]") - exit(0) - BastionScheduler.run() - - -@parallel.command("display", help="Display current Bastions is stack") -@click.option("--debug", is_flag=True, default=False) -def display(debug): - setup_calls(debug) - - display_scheduled() - - @cli.command( "use", help="Will Switch context to be used," @@ -157,103 +60,10 @@ def use(debug, context_name): Bastion.write_creds_json(conf, default_conf_path) -@config.command("generate", help="Will generate sample json and overwrite changes") -@click.option("--debug", is_flag=True, default=False) -@click.argument("context-name", default=None, required=False) -def generate(debug, context_name): - setup_calls(debug) - - path = get_context_path(context_name) - - Bastion.create_default_location() - td = Bastion.generate_sample_dict() - creds_path = Bastion.write_creds_json(td, path) - print( - f"Sample credentials generated, please fill 'creds.json' in {creds_path} with " - f"your credentials for this to work, you can use 'abst json fill " - f"{context_name if context_name else ''}'" - ) - - -@config.command( - "fill", help="Fills Json config with credentials you enter interactively" -) -@click.option("--debug", is_flag=True, default=False) -@click.argument("context-name", default=None, required=False) -def fill(debug, context_name): - setup_calls(debug) - - path = get_context_path(context_name) - - if not path.exists(): - rich.print("Generating sample Creds file") - Bastion.create_default_location() - td = Bastion.generate_sample_dict() - Bastion.write_creds_json(td, path) - - if not default_contexts_location.exists(): - rich.print("Generating contexts location") - Bastion.create_default_location() - - rich.print(f"[green]Filling {str(path)}") - rich.print("Please fill field one by one as displayed") - n_dict = dict() - - creds_json_ro = Bastion.load_json(path) - - for key, value in creds_json_ro.items(): - n_dict[key] = inquirer.text( - message=f"{key.capitalize()}:", default=value - ).execute() - rich.print("\n[red]New json looks like this:[/red]") - rich.print_json(data=n_dict) - if inquirer.confirm(message="Write New Json ?", default=False).execute(): - Bastion.write_creds_json(n_dict, path) - rich.print("[green]Wrote changes[/green]") - else: - rich.print("[red]Fill interrupted, nothing changed[/red]") - - -@config.command( - "locate", help="Locates Json config with credentials you enter interactively" -) -@click.option("--debug", is_flag=True, default=False) -@click.argument("context-name", default=None, required=False) -def locate(debug, context_name): - setup_calls(debug) - - path = get_context_path(context_name) - - if path.exists(): - rich.print(f"[green]Config file location: {path.absolute()}[/green]") - else: - rich.print( - f"[red]Config does not exist yet, future location" - f" {path.absolute()}[/red]" - ) - - -@config.command( - "upgrade", help="Locates Json config with credentials you enter interactively" -) -@click.option("--debug", is_flag=True, default=False) -@click.argument("context-name", default=None, required=False) -def upgrade(debug, context_name): - setup_calls(debug) - - path = get_context_path(context_name) - - if not path.exists(): - rich.print("[green]No config to upgrade[/green]") - return - - __upgrade(context_name, path) - - @cli.command("clean", help="Cleans all credentials created by abst") def clean(): """ """ - + file = "not specified" files = [*default_contexts_location.iterdir()] if default_creds_path.exists(): files.append(default_creds_path) @@ -274,339 +84,6 @@ def clean(): print(f"Please delete manually in {file}") -@cli.group(help="Group of commands for creating Bastion sessions") -def create(): - signal.signal(signal.SIGINT, BastionScheduler.kill_all) - signal.signal(signal.SIGTERM, BastionScheduler.kill_all) - - -def setup_calls(debug): - setup_debug(debug) - try: - Notifier.notify() - except (ConnectionError, ConnectTimeout): - return False - - -def setup_debug(debug): - if not debug: - logging.disable(logging.DEBUG) - logging.disable(logging.INFO) - - logging.basicConfig( - level=logging.DEBUG if debug else logging.CRITICAL, - format="%(message)s", - datefmt="[%X]", - handlers=[RichHandler()], - ) - - -@create.command( - "forward", - help="Creates and connects to Bastion session indefinitely until terminated by user", -) -@click.option("--shell", is_flag=True, default=False) -@click.option("--debug", is_flag=True, default=False) -@click.argument("context-name", default=None, required=False) -def fullauto_forward(shell, debug, context_name): - """Creates and connects to bastion sessions - automatically until terminated""" - - setup_calls(debug) - - if context_name == "?": - print_eligible("required only for Port Forward session") - return - - if context_name is None: - conf = Bastion.load_config() - used_name = conf["used_context"] - else: - used_name = context_name - - while True: - Bastion(used_name, region= - Bastion.load_json(Bastion.get_creds_path_resolve(context_name)).get("region", - None)).create_forward_loop( - shell=shell) - - sleep(1) - - -def print_eligible(searched: str): - contexts = Bastion.get_contexts() - rich.print("[bold]Configured contexts:[/bold]") - for key, context in contexts.items(): - do_skip = False - for value in context.values(): - if searched in value: - do_skip = True - break - if do_skip: - continue - rich.print(key) - - -@create.command( - "managed", - help="Creates and connects to Bastion session indefinitely until terminated by user", -) -@click.option("--shell", is_flag=True, default=False) -@click.option("--debug", is_flag=True, default=False) -@click.argument("context-name", default=None, required=False) -def fullauto_managed(shell, debug, context_name): - """Creates and connects to bastion sessions - automatically until terminated""" - setup_calls(debug) - - if context_name == "?": - print_eligible("required only for Managed SSH session") - return - - if context_name is None: - conf = Bastion.load_config() - used_name = conf["used_context"] - else: - used_name = context_name - - while True: - Bastion(used_name, region= - Bastion.load_json(Bastion.get_creds_path_resolve(context_name)).get("region", - None)).create_managed_loop( - shell=shell) - - sleep(1) - - -@cli.command("ssh", help="Will SSH into pod with containing string name") -@click.argument("pod_name") -@click.option("--debug", is_flag=True, default=False) -def ssh_pod(pod_name, debug): - setup_calls(debug) - - try: - rich.print("Fetching pods") - pod_lines = ( - subprocess.check_output(f"kubectl get pods -A".split(" ")) - .decode() - .split("\n") - ) - except FileNotFoundError: - rich.print("[red]kubectl not found on this machine[/red]") - return - - found = list(filter(lambda pod_line: pod_name in pod_line, pod_lines)) - - if len(found) > 1: - data = re.sub( - " +", - " ", - inquirer.select("Found more pods, choose one:", list(found)).execute(), - ).split(" ") - pod_name_precise = data[1] - elif len(found) == 1: - tmp = found.pop() - data = re.sub(" +", " ", tmp).split(" ") - pod_name_precise = data[1] - else: - rich.print(f"[red]No pods with name {pod_name} found[/red]") - return - - rich.print( - f"[green]Connecting to {pod_name_precise} in namespace: {data[0]}[/green]" - ) - os.system( - f"kubectl exec -n {data[0]} --stdin --tty {pod_name_precise} -- /bin/bash" - ) - - -@cli.command("logs", help="Will get logs from a pod with containing string name") -@click.argument("pod_name") -@click.option("--debug", is_flag=True, default=False) -def log_pod(pod_name, debug): - setup_calls(debug) - - try: - rich.print("Fetching pods") - pod_lines = ( - subprocess.check_output(f"kubectl get pods -A".split(" ")) - .decode() - .split("\n") - ) - except FileNotFoundError: - rich.print("[red]kubectl not found on this machine[/red]") - return - - found = list(filter(lambda pod_line: pod_name in pod_line, pod_lines)) - - if len(found) > 1: - data = re.sub( - " +", - " ", - inquirer.select("Found more pods, choose one:", found).execute(), - ).split(" ") - pod_name_precise = data[1] - elif len(found) == 1: - tmp = found.pop() - data = re.sub(" +", " ", tmp).split(" ") - pod_name_precise = data[1] - else: - rich.print(f"[red]No pods with name {pod_name} found[/red]") - return - - rich.print( - f"[green]Getting logs from {pod_name_precise} in namespace: {data[0]}[/green]" - ) - os.system( - f"kubectl -n {data[0]} logs {pod_name_precise}" - ) - - -@cli.group("cp") -def cp(): - pass - - -@cli.group("helm") -def helm(): - pass - - -@helm.command("login") -@click.option("--debug", is_flag=True, default=False) -@click.option( - "--edit", - default=False, - help="Edit credentials with index as argument (starts from 1)", -) -def helm_login(debug, edit): - """ - Copy Secret in current cluster from source namespace to target - @return: - :param debug: - """ - setup_calls(debug) - from subprocess import PIPE - - try: - _config = Bastion.load_config() - if "helm" not in _config.keys(): - _config["helm"] = [] - host, password, remote, username = helm_login_dialog() - _config["helm"].append( - { - "host": host, - "remote": remote, - "username": username, - "password": password, - } - ) - Bastion.write_creds_json(_config, default_conf_path) - rich.print("Added Credentials") - elif edit: - if "helm" not in _config.keys(): - _config["helm"] = [] - - host, password, remote, username = helm_login_dialog() - _config["helm"][edit - 1] = { - "host": host, - "remote": remote, - "username": username, - "password": password, - } - rich.print_json(data=_config["helm"][edit - 1]) - if inquirer.confirm(message="Write New Json ?", default=False).execute(): - Bastion.write_creds_json(_config, default_conf_path) - rich.print("Edited Credentials") - - choices = [ - {"name": f'{choice["remote"]} {choice["username"]}', "value": choice} - for choice in _config["helm"] - ] - - selected = inquirer.select("Select login", choices).execute() - rich.print("Trying to login") - p = subprocess.Popen( - f'helm registry login {selected["host"]} -u {selected["username"]} --password-stdin'.split( - " " - ), - stdout=PIPE, - stdin=PIPE, - stderr=PIPE, - ) - rich.print(p.communicate(input=selected["password"].encode())[0].decode()) - - except Exception as ex: - rich.print(f"[red]Exception during execution {ex}[/red]") - return - - -@helm.command("push") -@click.argument("chart") -@click.option("--debug", is_flag=True, default=False) -def helm_push(chart, debug): - """ - Copy Secret in current cluster from source namespace to target - @return: - :param debug: - :param chart: - :param debug: - """ - setup_calls(debug) - try: - _config = Bastion.load_config() - if "helm" not in _config.keys(): - _config["helm"] = [] - rich.print( - f"Please Fill in Repository details that" - f" are going to be saved in {default_conf_path}" - f" with command 'abst helm login'" - ) - choices = [ - {"name": f'{choice["remote"]} {choice["username"]}', "value": choice} - for choice in _config["helm"] - ] - - selected = inquirer.select("Select remote", choices).execute() - rich.print("Trying to push") - os.system(f'helm push {chart} {selected["remote"]}') - - except Exception as ex: - rich.print(f"[red]Exception during execution {ex}[/red]") - return - - -@cp.command("login") -@click.argument("secret_name") -@click.argument("source_namespace") -@click.argument("target_namespace") -@click.option("--debug", is_flag=True, default=False) -def cp_secret( - secret_name: str, - target_namespace: str, - source_namespace: str = "default", - debug=False, -): - """ - Copy Secret in current cluster from source namespace to target - @param secret_name: Secret Name - @param target_namespace: Target Namespace name - @param source_namespace: Source Namespace name - @return: - :param debug: - """ - setup_calls(debug) - try: - rich.print("Trying Copy secret") - os.system( - f"kubectl get secret {secret_name} --namespace={source_namespace} -o yaml | sed " - f"'s/namespace: .*/namespace: {target_namespace}/' | kubectl apply -f -" - ) - except FileNotFoundError: - rich.print("[red]kubectl not found on this machine[/red]") - return - - def main(): logging.basicConfig( level=logging.DEBUG, @@ -622,5 +99,14 @@ def main(): signal.signal(signal.SIGTERM, BastionScheduler.kill_all) signal.signal(signal.SIGABRT, BastionScheduler.kill_all) +cli.add_command(parallel) +cli.add_command(config) +cli.add_command(context) +cli.add_command(helm) +cli.add_command(cp) +cli.add_command(ssh_pod) +cli.add_command(log_pod) +cli.add_command(create) + if __name__ == "__main__": main() diff --git a/abst/utils/__init__.py b/abst/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/abst/utils/misc_funcs.py b/abst/utils/misc_funcs.py new file mode 100644 index 0000000..5dc4a36 --- /dev/null +++ b/abst/utils/misc_funcs.py @@ -0,0 +1,59 @@ +import json +import logging +from pathlib import Path +from typing import Optional + +import rich +from requests import ConnectTimeout +from rich.logging import RichHandler + +from abst.bastion_support.oci_bastion import Bastion +from abst.config import default_contexts_location +from abst.notifier.version_notifier import Notifier + + +def setup_calls(debug): + setup_debug(debug) + try: + Notifier.notify() + except (ConnectionError, ConnectTimeout): + return False + + +def setup_debug(debug): + if not debug: + logging.disable(logging.DEBUG) + logging.disable(logging.INFO) + + logging.basicConfig( + level=logging.DEBUG if debug else logging.CRITICAL, + format="%(message)s", + datefmt="[%X]", + handlers=[RichHandler()], + ) + + +def print_eligible(searched: str): + contexts = Bastion.get_contexts() + rich.print("[bold]Configured contexts:[/bold]") + for key, context in contexts.items(): + do_skip = False + for value in context.values(): + if searched in value: + do_skip = True + break + if do_skip: + continue + rich.print(key) + + +def get_context_data(name) -> Optional[dict]: + if name in [file.name.replace(".json", "") for file in + Path(default_contexts_location).iterdir()]: + rich.print("[bold]Context config contents:[/bold]\n") + with open(Path(default_contexts_location) / (name + ".json"), "r") as f: + data = json.load(f) + return data + else: + rich.print("[red]Context does not exists[/red]") + return None