From 304d70e9b951fa6be03692f751380333fc27ee6c Mon Sep 17 00:00:00 2001 From: msterhuj Date: Sat, 2 Nov 2024 00:12:59 +0100 Subject: [PATCH 1/8] feat: add command sync --- labctl/api_driver.py | 3 +++ labctl/main.py | 24 ++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/labctl/api_driver.py b/labctl/api_driver.py index 3d7a5e9..06df7eb 100644 --- a/labctl/api_driver.py +++ b/labctl/api_driver.py @@ -18,3 +18,6 @@ def __init__(self): def get(self, path: str): return requests.get(self.api_url + path, headers=self.headers).json() + + def me(self): + return self.get("/status") \ No newline at end of file diff --git a/labctl/main.py b/labctl/main.py index 8f57e5a..5feb46f 100644 --- a/labctl/main.py +++ b/labctl/main.py @@ -1,4 +1,5 @@ from typing import Annotated +from time import sleep import requests import typer @@ -15,7 +16,6 @@ def callback(): labctl """ - @app.command() def version(): """ @@ -32,11 +32,31 @@ def status(): Print the current status of the fastonboard-api account """ api = APIDriver() - status: dict = api.get("/status") + status: dict = api.me() typer.echo("Status:") typer.echo(f" - User: {status['username']}") typer.echo(f" - Email: {status['email']}") +@app.command() +def sync(): + """ + Ask FastOnBoard-API to sync your account onto the vpn and openstack services + """ + api = APIDriver() + me = api.me() + task_id = api.get("/users/" + me['username'] + "/sync") + typer.echo(f"Syncing account for user {me['username']} this may take a while...") + typer.echo("Task ID: " + task_id.get("id")) + while True: + task = api.get("/users/" + me['username'] + "/sync/" + task_id.get("id")) + if task.get("status") == "SUCCESS": + typer.echo("Sync successful") + break + if task.get("status") == "FAILURE": + typer.echo("Sync failed") + break + sleep(1) + @app.command() def init( endpoint: Annotated[str, typer.Argument(help="The endpoint of the FastOnBoard-API server")], From 38296c5b8e3c9f94a55f2d26597f1d65eb4348c5 Mon Sep 17 00:00:00 2001 From: msterhuj Date: Sun, 17 Nov 2024 15:40:15 +0100 Subject: [PATCH 2/8] feat: simplify config mngt --- labctl/config.py | 55 ------------------------------------------ labctl/core/config.py | 56 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 55 deletions(-) delete mode 100644 labctl/config.py create mode 100644 labctl/core/config.py diff --git a/labctl/config.py b/labctl/config.py deleted file mode 100644 index dd6c51a..0000000 --- a/labctl/config.py +++ /dev/null @@ -1,55 +0,0 @@ -from pathlib import Path - -import yaml - -CONFIG_LOCATION_DIR = f"{Path.home()}/.labctl/" -CONIIG_FILE = "config.yaml" - -class Config: - api_endpoint = None - api_token = None - token_type = None - - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - -class ConfigNotInitialized(Exception): - pass - -class ConfigError(Exception): - pass - -class ConfigManager: - - config: Config = None - - def __init__(self, config: Config = None): - """ - Args: - config (Config, optional): new config object to save - Raises: - ConfigNotInitialized: When the config file is not found - ConfigError: When there is an error loading the config file - """ - if not Path(CONFIG_LOCATION_DIR).exists(): - Path(CONFIG_LOCATION_DIR).mkdir(parents=True) - - if config: - self.config = config - self.save() - - if not Path(CONFIG_LOCATION_DIR + CONIIG_FILE).exists(): - raise ConfigNotInitialized("Config file not found. Please run `labctl init` to initialize the config file.") - try: - self.load() - except Exception: - raise ConfigError("Error loading config file. Please run `labctl init` to reinitialize the config file.") - - def save(self): - with open(CONFIG_LOCATION_DIR + CONIIG_FILE, "w") as f: - yaml.dump(self.config.__dict__, f) - - def load(self): - with open(CONFIG_LOCATION_DIR + CONIIG_FILE, "r") as f: - self.config = Config() - self.config.__dict__ = yaml.load(f, Loader=yaml.FullLoader) \ No newline at end of file diff --git a/labctl/core/config.py b/labctl/core/config.py new file mode 100644 index 0000000..16487d2 --- /dev/null +++ b/labctl/core/config.py @@ -0,0 +1,56 @@ +from pathlib import Path + +import yaml + +CONFIG_LOCATION_DIR = f"{Path.home()}/" +CONIIG_FILE = ".labctl_config.yaml" + +class Config: + + api_endpoint: str + api_token: str + token_type: str + + def __init__(self, **kwargs): + """ + Initialize the configuration object + The kwargs are used to update the configuration on fly like so: + config = Config(api_endpoint="http://localhost:8000").save() + """ + if not Path(CONFIG_LOCATION_DIR).exists(): + Path(CONFIG_LOCATION_DIR).mkdir(parents=True) + + # If the config file does not exist, create a new one else load the existing one + if not Path(CONFIG_LOCATION_DIR + CONIIG_FILE).exists(): + self.save() + else: + self.load() + self.__dict__.update(kwargs) + + def __getattr__(self, name): + """ + Get the value of the attribute or return None if it does not exist + """ + return self.__dict__.get(name, None) + + def save(self): + """ + Save the current configuration to the configuration file + """ + with open(CONFIG_LOCATION_DIR + CONIIG_FILE, "w") as stream: + yaml.dump(self.__dict__, stream) + return self + + def load(self): + """ + Load the configuration from the configuration file + """ + with open(CONFIG_LOCATION_DIR + CONIIG_FILE, "r") as stream: + self.__dict__.update(yaml.load(stream, Loader=yaml.FullLoader)) + return self + + def ready(self): + """ + Check if the configuration is ready to be used + """ + return all([self.api_endpoint, self.api_token, self.token_type]) \ No newline at end of file From c12d97ab49d81355bf069266fe2831066e67d330 Mon Sep 17 00:00:00 2001 From: msterhuj Date: Sun, 17 Nov 2024 15:40:38 +0100 Subject: [PATCH 3/8] feat: rework api driver --- labctl/api_driver.py | 23 ----------------------- labctl/core/api.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 23 deletions(-) delete mode 100644 labctl/api_driver.py create mode 100644 labctl/core/api.py diff --git a/labctl/api_driver.py b/labctl/api_driver.py deleted file mode 100644 index 06df7eb..0000000 --- a/labctl/api_driver.py +++ /dev/null @@ -1,23 +0,0 @@ -import requests - -from labctl.config import Config, ConfigManager - -class APIDriver: - - api_url: str = None - api_token: str = None - headers: dict = None - - def __init__(self): - config: Config = ConfigManager().config - self.api_url = config.api_endpoint - self.headers = { - 'accept': 'application/json', - 'Authorization': f'Bearer {config.api_token}' - } - - def get(self, path: str): - return requests.get(self.api_url + path, headers=self.headers).json() - - def me(self): - return self.get("/status") \ No newline at end of file diff --git a/labctl/core/api.py b/labctl/core/api.py new file mode 100644 index 0000000..d75c547 --- /dev/null +++ b/labctl/core/api.py @@ -0,0 +1,33 @@ +import requests +import typer +from labctl import __version__ +from labctl.core import Config + + +class APIDriver: + + api_url: str = None + api_token: str = None + headers: dict = None + + def __init__(self): + config: Config = Config() + self.api_url = config.api_endpoint + if self.api_url.endswith("/"): + self.api_url = self.api_url.rstrip("/") + self.headers = { + 'accept': 'application/json', + 'User-Agent': 'labctl/' + __version__, + 'Authorization': f'Bearer {config.api_token}' + } + + def validate_token(self): + return self.get("/token/verify").get("valid", False) + + def get(self, path: str): + return requests.get(self.api_url + path, headers=self.headers).json() + + def post(self, path: str, data: dict = {}, additional_headers: dict = {}): + headers = self.headers + headers.update(additional_headers) + return requests.post(self.api_url + path, headers=headers, data=data).json() From 7c7e5ab7f9ef975f23b2ee44220465e9627e9dd4 Mon Sep 17 00:00:00 2001 From: msterhuj Date: Sun, 17 Nov 2024 15:41:03 +0100 Subject: [PATCH 4/8] feat: setup console --- labctl/commands/__init__.py | 1 + labctl/core/__init__.py | 4 ++++ labctl/core/console.py | 5 +++++ 3 files changed, 10 insertions(+) create mode 100644 labctl/commands/__init__.py create mode 100644 labctl/core/__init__.py create mode 100644 labctl/core/console.py diff --git a/labctl/commands/__init__.py b/labctl/commands/__init__.py new file mode 100644 index 0000000..53a6180 --- /dev/null +++ b/labctl/commands/__init__.py @@ -0,0 +1 @@ +from .config import app as config_app diff --git a/labctl/core/__init__.py b/labctl/core/__init__.py new file mode 100644 index 0000000..1e347fd --- /dev/null +++ b/labctl/core/__init__.py @@ -0,0 +1,4 @@ +from .config import Config +from .api import APIDriver +from .console import console +from .decorators import cli_ready diff --git a/labctl/core/console.py b/labctl/core/console.py new file mode 100644 index 0000000..0bf1aac --- /dev/null +++ b/labctl/core/console.py @@ -0,0 +1,5 @@ +from rich.console import Console + +console = Console( + color_system="auto", +) \ No newline at end of file From c73a4f36e4326ab783ac7f8b9c0b4502eaec33a1 Mon Sep 17 00:00:00 2001 From: msterhuj Date: Sun, 17 Nov 2024 15:41:54 +0100 Subject: [PATCH 5/8] feat: add commands to manage config --- labctl/commands/config.py | 70 +++++++++++++++++++++++++++++++++++++++ labctl/main.py | 5 +-- 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 labctl/commands/config.py diff --git a/labctl/commands/config.py b/labctl/commands/config.py new file mode 100644 index 0000000..43b100a --- /dev/null +++ b/labctl/commands/config.py @@ -0,0 +1,70 @@ +import typer + +from rich.table import Table + +from labctl.core import Config, APIDriver, console + + +app = typer.Typer() + +@app.command(name="show") +def show(): + """ + Show the current configuration + """ + config = Config() + api_token = config.api_token + if api_token: + me = APIDriver().get("/me") + # todo handle old token and valid token + if me.get("email"): + api_token = "Logged in as " + me["email"] + else: + api_token = "Token is invalid or expired (use `labctl login`)" + else: + api_token = "Not logged in" + table = Table(title="Configuration") + table.add_column("Key", style="cyan") + table.add_column("Value", style="magenta") + table.add_row("API URL", config.api_endpoint) + table.add_row("API Token", api_token) + console.print(table) + + if not config.api_endpoint: + console.print("[red]Warning: API Endpoint not set. Use `labctl config set --api-endpoint=` [/red]") + if not config.api_token: + console.print("[red]Warning: API Token not set. Use `labctl login`. [/red]") + +@app.command(name="set") +def set_config( + api_endpoint: str = typer.Option(None, help="Set the API endpoint") +): + """ + Set the configuration + """ + new_config = {} + if api_endpoint: + new_config["api_endpoint"] = api_endpoint + if not new_config: + console.print("[red]No settings provided[/red]") + raise typer.Abort() + Config(**new_config).save() + console.print("[green]Configuration updated[/green]") + show() + +@app.command(name="unset") +def unset_config( + api_endpoint: bool = typer.Option(False, help="Unset the API endpoint") +): + """ + Unset the configuration + """ + new_config = {} + if api_endpoint: + new_config["api_endpoint"] = None + if not new_config: + console.print("[red]No settings provided[/red]") + raise typer.Abort() + Config(**new_config).save() + console.print("[green]Configuration updated[/green]") + show() diff --git a/labctl/main.py b/labctl/main.py index 5feb46f..4a658b2 100644 --- a/labctl/main.py +++ b/labctl/main.py @@ -4,11 +4,12 @@ import requests import typer -from labctl import __version__ -from labctl.api_driver import APIDriver +from labctl import __version__, commands +from labctl.core import APIDriver, Config, console, cli_ready from labctl.config import Config, ConfigManager app = typer.Typer() +app.add_typer(commands.config_app, name="config", help="Manage the configuration") @app.callback() def callback(): From 91308d00129e829fca02d7d989386692cb167359 Mon Sep 17 00:00:00 2001 From: msterhuj Date: Sun, 17 Nov 2024 15:42:56 +0100 Subject: [PATCH 6/8] feat: rework login function --- labctl/main.py | 53 +++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/labctl/main.py b/labctl/main.py index 4a658b2..4fcf25d 100644 --- a/labctl/main.py +++ b/labctl/main.py @@ -59,36 +59,41 @@ def sync(): sleep(1) @app.command() -def init( - endpoint: Annotated[str, typer.Argument(help="The endpoint of the FastOnBoard-API server")], - username: Annotated[str, typer.Argument(help="The username to authenticate with")], - ): - password = typer.prompt("Enter your password", hide_input=True) +def login(username: Annotated[str, typer.Argument(help="The username to authenticate with")]): + """ + Login to the FastOnBoard-API server + Enter your password when prompted or set LABCTL_API_ENDPOINT_PASSWORD + """ + env_pass = environ.get("LABCTL_API_ENDPOINT_PASSWORD") + if env_pass: + password = env_pass + else: + password = typer.prompt("Enter your password", hide_input=True) - headers = { - 'accept': 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - } - data = { + api_driver = APIDriver() + + if not api_driver.api_url: + console.print("[red]Error: API endpoint not set use `labctl config set --api-endpoint=`[/red]") + return + + data = api_driver.post("/token", data={ 'username': username, 'password': password, - } - data = requests.post(endpoint + "/token", headers=headers, data=data).json() + }, additional_headers={ + 'Content-Type': 'application/x-www-form-urlencoded', + }) if 'detail' in data: if "Method Not Allowed" in data['detail']: - typer.echo("Invalid endpoint or path to api") + console.print("[red]Error: Invalid endpoint or path to api[/red]") return - typer.echo(data['detail']) + console.print(f"[red]Authentication failed : {data['detail']}[/red]") return if 'access_token' in data: - typer.echo("Successfully authenticated") - ConfigManager( - Config( - api_endpoint=endpoint, - api_token=data['access_token'], - token_type=data["token_type"] - ) - ) - print("Config file initialized and authentication successful") + config = Config() + config.api_token=data['access_token'] + config.token_type=data["token_type"] + config.save() + console.print("[green]Authentication successful[/green]") return - typer.echo("Authentication failed with unknown error") + console.print("[red]Authentication failed with unknown error[/red]") + console.print_json(data) From fb8a242640fec73adc59bba46d7a99d69dce7311 Mon Sep 17 00:00:00 2001 From: msterhuj Date: Sun, 17 Nov 2024 15:43:21 +0100 Subject: [PATCH 7/8] feat: add decorator to check if cli is ready --- labctl/core/decorators.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 labctl/core/decorators.py diff --git a/labctl/core/decorators.py b/labctl/core/decorators.py new file mode 100644 index 0000000..143bf20 --- /dev/null +++ b/labctl/core/decorators.py @@ -0,0 +1,26 @@ +from functools import wraps + +import typer +from labctl.core import APIDriver, Config, console + +def cli_ready(func): + """ + Decorator to check if the CLI is ready to be used + Validates the configuration have been token and url set and check if token is still valid + """ + @wraps(func) + def wrapper(*args, **kwargs): + try: + config = Config() + if not config.ready(): + console.print("[red]Configuration not ready, please run 'labctl config show' to check the current configuration[/red]") + raise typer.Exit(1) + api_driver = APIDriver() + if not api_driver.validate_token(): + console.print("[red]Token is not valid, please run 'labctl config show' to check the current configuration[/red]") + raise typer.Exit(1) + return func(*args, **kwargs) + except Exception as e: + typer.echo(f"Error: {e}") + raise typer.Exit(1) + return wrapper From f940ee14e067396d7fb5e25dae88c68e7f1f4faa Mon Sep 17 00:00:00 2001 From: msterhuj Date: Sun, 17 Nov 2024 15:43:40 +0100 Subject: [PATCH 8/8] feat: rework and added devices to /me --- labctl/main.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/labctl/main.py b/labctl/main.py index 4fcf25d..2c9fa1a 100644 --- a/labctl/main.py +++ b/labctl/main.py @@ -1,14 +1,17 @@ from typing import Annotated from time import sleep +from os import environ +from json import dumps import requests import typer +from rich.tree import Tree from labctl import __version__, commands from labctl.core import APIDriver, Config, console, cli_ready -from labctl.config import Config, ConfigManager app = typer.Typer() + app.add_typer(commands.config_app, name="config", help="Manage the configuration") @app.callback() @@ -24,21 +27,40 @@ def version(): """ version = __version__ if version == "0.0.0": - version = "dev" - typer.echo("labctl version {}".format(version)) + version = "dev or installed from source" + console.print(f"labctl version: {version} :rocket:") @app.command() -def status(): +@cli_ready +def me( + json: bool = typer.Option(False, help="Output the data as json") +): """ Print the current status of the fastonboard-api account """ - api = APIDriver() - status: dict = api.me() - typer.echo("Status:") - typer.echo(f" - User: {status['username']}") - typer.echo(f" - Email: {status['email']}") + api_driver = APIDriver() + data = api_driver.get("/me") + if json: + print(dumps(data)) + return + tree = Tree("[bold blue]:open_file_folder: FastOnBoard Account[/bold blue]") + tree.add("[bold]Username:[/bold] " + data.get("username")) + tree.add("[bold]Email:[/bold] " + data.get("email")) + + devices_tree = tree.add(":open_file_folder: Devices") + for device in data.get('devices_access', []): + device_tree = devices_tree.add(":computer: " + device.get('name', ':question: Unnamed Device')) + device_tree.add("[bold]ID:[/bold] " + device.get('id', '')) + device_tree.add("[bold]IPv4:[/bold] " + device.get('ipv4', '')) + device_tree.add("[bold]Latest Handshake:[/bold] " + str(device.get('latest_handshake', ''))) + device_tree.add("[bold]RX Bytes:[/bold] " + str(device.get('rx_bytes', 0))) + device_tree.add("[bold]TX Bytes:[/bold] " + str(device.get('tx_bytes', 0))) + device_tree.add("[bold]Remote IP:[/bold] " + str(device.get('remote_ip', ''))) + console.print(tree) + @app.command() +@cli_ready def sync(): """ Ask FastOnBoard-API to sync your account onto the vpn and openstack services