diff --git a/abst/__version__.py b/abst/__version__.py index 3cbbe73..e21691e 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.3.15" +__version__ = "2.3.16" __author__ = "Jiri Otoupal" __author_email__ = "jiri-otoupal@ips-database.eu" __license__ = "MIT" @@ -18,3 +18,9 @@ __pypi_repo__ = "https://pypi.org/project/abst/" __version_name__ = "Octopus X0" +__change_log__ = """ +Added alias to context, now you can use 'ctx'\n +Sharing and Pasting now works for sets too, you can use set_name/context for both + +When pasting, it will ask if you want to change IP and for bastion name renaming +""" diff --git a/abst/bastion_support/oci_bastion.py b/abst/bastion_support/oci_bastion.py index b2e8245..e36749c 100644 --- a/abst/bastion_support/oci_bastion.py +++ b/abst/bastion_support/oci_bastion.py @@ -244,7 +244,8 @@ def run_ssh_tunnel_managed_session(self, bid, host, private_key_path, username, f'-o ProxyCommand="ssh -i {private_key_path} -W %h:%p -p {port} {bid}@{host} -A" -p {port} ' f'{username}@{ip} -A') logging.info(f"Running ssh command {ssh_tunnel_arg_str}") - exit_code = self.__run_ssh_tunnel_call(ssh_tunnel_arg_str, shell, already_split=True) + exit_code = self.__run_ssh_tunnel_call(ssh_tunnel_arg_str, shell, + already_split=True) logging.info(f"SSH command exit code {exit_code}") return ssh_tunnel_arg_str, exit_code @@ -310,7 +311,8 @@ def init_session_details(cls, creds): rich.print("If you want to add region, run abst config upgrade ") ssh_pub_path = str(Path(creds.get("ssh-pub-path", - cfg.get("ssh-pub-path", "No Public key supplied"))).expanduser().resolve()) + cfg.get("ssh-pub-path", + "No Public key supplied"))).expanduser().resolve()) if ssh_pub_path == "No Public key supplied": rich.print( @@ -393,7 +395,9 @@ def load_json(cls, path=default_creds_path) -> dict: if not default_conf_path.exists() and path == default_conf_path: default_conf_path.parent.mkdir(exist_ok=True) with open(str(path), "w") as f: - json.dump({"last-check": datetime.datetime.timestamp(datetime.datetime.now())}, f, indent=3) + json.dump( + {"last-check": datetime.datetime.timestamp(datetime.datetime.now())}, + f, indent=3) with open(str(path), "r") as f: creds = json.load(f) @@ -426,7 +430,14 @@ def create_default_locations(cls): @classmethod def write_creds_json(cls, td: dict, path: Path): - with open(str(path), "w") as f: + + if not path.name.endswith(".json"): + # Construct new filename with .json extension + tmp_path = path.with_suffix(".json") + else: + tmp_path = path + + with open(str(tmp_path), "w") as f: json.dump(td, f, indent=4) return path diff --git a/abst/cli_commands/context/commands.py b/abst/cli_commands/context/commands.py index 705a62b..f322edb 100644 --- a/abst/cli_commands/context/commands.py +++ b/abst/cli_commands/context/commands.py @@ -1,15 +1,17 @@ import json import logging +from json import JSONDecodeError from pathlib import Path import click import pyperclip import rich +from InquirerPy import inquirer from abst.bastion_support.oci_bastion import Bastion from abst.config import default_contexts_location, share_excluded_keys from abst.tools import get_context_path -from abst.utils.misc_funcs import get_context_data, setup_calls +from abst.utils.misc_funcs import get_context_data, setup_calls, get_context_set_data @click.group(help="Contexts commands") @@ -17,6 +19,11 @@ def context(): pass +@click.group(help="Context commands alias") +def ctx(): + 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): @@ -37,13 +44,18 @@ def display(name, debug=False): rich.print_json(data=data) -@context.command(help="Will print context without local paths and put it in clipboard for sharing") +@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): +def share(name: str, debug=False): setup_calls(debug) rich.print("Copied context into clipboard") - data = get_context_data(name) + + if "/" in name: + data = get_context_set_data(name) + else: + data = get_context_data(name) if data is None: return for key in share_excluded_keys: @@ -64,5 +76,31 @@ def paste(name, debug=False): path = get_context_path(name) if data is None: return - Bastion.write_creds_json(json.loads(data.replace("'", "\"")), path) - rich.print(f"Wrote config into ~/.abst/contexts/{name}.json") + + try: + data_loaded = json.loads(data.replace("'", "\"")) + except JSONDecodeError: + rich.print("[red]Invalid data, please try copying context again[/red]") + exit(1) + + if data_loaded["default-name"] == "!YOUR NAME!": + data_loaded["default-name"] = inquirer.text( + "Please enter your name for bastion session that will be visible in OCI:").execute() + + if inquirer.confirm("Would you like to change IP?").execute(): + data_loaded["target-ip"] = inquirer.text( + "Please enter your target IP address:").execute() + + if path.exists(): + if not inquirer.confirm("File exists. Overwrite?").execute(): + rich.print("[red]Canceled.[/red]") + return + + Bastion.write_creds_json(data_loaded, path) + rich.print(f"Wrote config into '{path}'") + + +ctx.add_command(_list, "list") +ctx.add_command(display, "display") +ctx.add_command(share, "share") +ctx.add_command(paste, "paste") diff --git a/abst/main.py b/abst/main.py index 3c7819b..31f072d 100644 --- a/abst/main.py +++ b/abst/main.py @@ -8,11 +8,12 @@ from InquirerPy import inquirer from requests import ConnectTimeout -from abst.__version__ import __version_name__, __version__ +from packaging import version +from abst.__version__ import __version_name__, __version__, __change_log__ from abst.bastion_support.bastion_scheduler import BastionScheduler from abst.bastion_support.oci_bastion import Bastion from abst.cli_commands.config_cli.commands import config -from abst.cli_commands.context.commands import context +from abst.cli_commands.context.commands import context, ctx 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 @@ -26,7 +27,16 @@ @click.group() @click.version_option(f"{__version__} {__version_name__}") def cli(): - pass + Bastion.create_default_locations() + _config = Bastion.load_config() + if _config.get("changelog-version", None) is None: + _config["changelog-version"] = __version__ + elif version.parse(_config["changelog-version"]) > version.parse( + __version__) and __change_log__: + _config["changelog-version"] = __version__ + rich.print(f"[yellow]Version {__version__} {__version_name__}[/yellow]") + rich.print("[red]Changelog[/red]") + rich.print(__change_log__) @cli.command( @@ -104,6 +114,7 @@ def main(): cli.add_command(parallel) cli.add_command(config) cli.add_command(context) +cli.add_command(ctx) cli.add_command(helm) cli.add_command(cp) cli.add_command(ssh_pod) diff --git a/abst/tools.py b/abst/tools.py index 4c5810e..9af5e69 100644 --- a/abst/tools.py +++ b/abst/tools.py @@ -1,14 +1,27 @@ from pathlib import Path from typing import Optional +import rich + from abst.bastion_support.bastion_scheduler import BastionScheduler from abst.bastion_support.oci_bastion import Bastion -from abst.config import default_creds_path, default_contexts_location +from abst.config import default_creds_path, default_contexts_location, \ + default_parallel_sets_location def get_context_path(context_name): if context_name is None: return default_creds_path + elif "/" in context_name: + path = context_name.split("/") + if len(path) != 2: + rich.print(f"[red]Invalid path {path}[/red]") + return None + + set_path = default_parallel_sets_location / path[0] + set_path.mkdir(exist_ok=True) + + return set_path / path[1] else: return default_contexts_location / (context_name + ".json") @@ -17,14 +30,17 @@ def display_scheduled(set_dir: Optional[Path] = None): from rich.table import Table from rich.console import Console console = Console() - table = Table(title=f"Bastions of {set_dir.name} set" if set_dir else "Bastions of default stack", highlight=True) + table = Table( + title=f"Bastions of {set_dir.name} set" if set_dir else "Bastions of default stack", + highlight=True) table.add_column("Name", justify="left", style="cyan", no_wrap=True) table.add_column("Local Port", style="magenta", no_wrap=True) table.add_column("Active", justify="right", style="green", no_wrap=True) table.add_column("Status", justify="right", style="green", no_wrap=True) if set_dir: - for context_path in filter(lambda p: not str(p.name).startswith("."), set_dir.iterdir()): + for context_path in filter(lambda p: not str(p.name).startswith("."), + set_dir.iterdir()): conf = Bastion.load_json(context_path) context_name = context_path.name[:-5] table.add_row(context_name, conf.get('local-port', 'Not Specified'), diff --git a/abst/utils/misc_funcs.py b/abst/utils/misc_funcs.py index 59d77e5..24f354c 100644 --- a/abst/utils/misc_funcs.py +++ b/abst/utils/misc_funcs.py @@ -11,7 +11,7 @@ from rich.logging import RichHandler from abst.bastion_support.oci_bastion import Bastion -from abst.config import default_contexts_location +from abst.config import default_contexts_location, default_parallel_sets_location def setup_calls(debug): @@ -58,6 +58,29 @@ def get_context_data(name) -> Optional[dict]: return None +def get_context_set_data(name) -> Optional[dict]: + path = name.split("/") + if len(path) != 2: + rich.print(f"[red]Invalid path '{name}'[/red]") + + if path[0] in [file.name for file in + Path(default_parallel_sets_location).iterdir()]: + if path[1] in [file.name.replace(".json", "") for file in + Path(default_parallel_sets_location / path[0]).iterdir()]: + rich.print(f"[bold]Context '{name}' config contents:[/bold]\n") + with open( + Path(default_parallel_sets_location) / path[0] / (path[1] + ".json"), + "r") as f: + data = json.load(f) + return data + else: + rich.print("[red]Context does not exists[/red]") + return None + else: + rich.print("[red]Set does not exists[/red]") + return None + + def fetch_pods(): rich.print("Fetching pods") pod_lines = ( @@ -68,7 +91,8 @@ def fetch_pods(): return pod_lines -def recursive_copy(dir_to_iter: Path, dest_path: str, exclude: str, data: list, pod_name_precise: str, +def recursive_copy(dir_to_iter: Path, dest_path: str, exclude: str, data: list, + pod_name_precise: str, thread_list: list): if dir_to_iter.is_dir(): create_folder_kubectl(data, dest_path, pod_name_precise) @@ -83,7 +107,8 @@ def recursive_copy(dir_to_iter: Path, dest_path: str, exclude: str, data: list, create_folder_kubectl(data, final_dest_path, pod_name_precise) t = Thread(name=f"recursive_copy_{file}", target=recursive_copy, - args=[file, dest_path, exclude, data, pod_name_precise, thread_list]) + args=[file, dest_path, exclude, data, pod_name_precise, + thread_list]) t.start() thread_list.append(t) else: @@ -106,7 +131,8 @@ def create_folder_kubectl(data: list, dest_path: str, pod_name_precise: str): os.system(kubectl_create_dir_cmd) -def copy_file_alt_kubectl(data: list, dest_path: str, local_path: Path, pod_name_precise: str, tries=4): +def copy_file_alt_kubectl(data: list, dest_path: str, local_path: Path, + pod_name_precise: str, tries=4): kubectl_alt_copy_cmd = (f"cat \"{local_path}\" |" f" kubectl exec -i {pod_name_precise} -n {data[0]} -- tee \"{dest_path}\" > /dev/null") logging.info(f"Executing {kubectl_alt_copy_cmd}") diff --git a/requirements.txt b/requirements.txt index 5de21a2..b950bf1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,6 @@ setuptools~=60.2.0 semantic-version~=2.10.0 eventlet~=0.33.2 pyperclip~=1.8.2 -bext~=0.0.8 \ No newline at end of file +bext~=0.0.8 +requests~=2.28.1 +packaging~=23.0 \ No newline at end of file