From 5f31230aa440130c5616f6e198775d792da4a886 Mon Sep 17 00:00:00 2001 From: Freewheelin Date: Thu, 9 Oct 2025 11:29:06 +1000 Subject: [PATCH 1/8] feat: add user preferences configuration options Add comprehensive configuration options for user preferences including: - Output format preferences (output-format, show-header) - Per-command format preferences (format-images, format-servers, etc.) - Server creation defaults (default-region, default-size, default-image, etc.) - Terminal settings (terminal-width) --- src/binarylane/config/options.py | 141 +++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/src/binarylane/config/options.py b/src/binarylane/config/options.py index acc064a5..493edda1 100644 --- a/src/binarylane/config/options.py +++ b/src/binarylane/config/options.py @@ -7,11 +7,43 @@ class OptionName(str, Enum): + # Existing API options API_URL = "api-url" API_TOKEN = "api-token" API_DEVELOPMENT = "api-development" CONFIG_SECTION = "context" + # Output preferences + OUTPUT_FORMAT = "output-format" + SHOW_HEADER = "show-header" + + # Per-command format preferences + FORMAT_IMAGES = "format-images" + FORMAT_SERVERS = "format-servers" + FORMAT_DOMAINS = "format-domains" + FORMAT_VPCS = "format-vpcs" + FORMAT_LOAD_BALANCERS = "format-load-balancers" + FORMAT_SSH_KEYS = "format-ssh-keys" + FORMAT_ACTIONS = "format-actions" + FORMAT_SIZES = "format-sizes" + FORMAT_REGIONS = "format-regions" + FORMAT_INVOICES = "format-invoices" + FORMAT_SOFTWARE = "format-software" + + # Server creation defaults + DEFAULT_REGION = "default-region" + DEFAULT_SIZE = "default-size" + DEFAULT_IMAGE = "default-image" + DEFAULT_BACKUPS = "default-backups" + DEFAULT_SSH_KEYS = "default-ssh-keys" + DEFAULT_USER_DATA = "default-user-data" + DEFAULT_PORT_BLOCKING = "default-port-blocking" + DEFAULT_PASSWORD = "default-password" + DEFAULT_VPC = "default-vpc" + + # Terminal settings + TERMINAL_WIDTH = "terminal-width" + def __str__(self) -> str: return self.value @@ -89,3 +121,112 @@ def api_development(self) -> bool: @property def config_section(self) -> str: return self.required_option(OptionName.CONFIG_SECTION) + + # Output preference properties + + @property + def output_format(self) -> Optional[str]: + return self.get_option(OptionName.OUTPUT_FORMAT) + + @property + def show_header(self) -> Optional[bool]: + value = self.get_option(OptionName.SHOW_HEADER) + return self.to_bool(value) if value else None + + # Per-command format preference properties + + @property + def format_images(self) -> Optional[str]: + return self.get_option(OptionName.FORMAT_IMAGES) + + @property + def format_servers(self) -> Optional[str]: + return self.get_option(OptionName.FORMAT_SERVERS) + + @property + def format_domains(self) -> Optional[str]: + return self.get_option(OptionName.FORMAT_DOMAINS) + + @property + def format_vpcs(self) -> Optional[str]: + return self.get_option(OptionName.FORMAT_VPCS) + + @property + def format_load_balancers(self) -> Optional[str]: + return self.get_option(OptionName.FORMAT_LOAD_BALANCERS) + + @property + def format_ssh_keys(self) -> Optional[str]: + return self.get_option(OptionName.FORMAT_SSH_KEYS) + + @property + def format_actions(self) -> Optional[str]: + return self.get_option(OptionName.FORMAT_ACTIONS) + + @property + def format_sizes(self) -> Optional[str]: + return self.get_option(OptionName.FORMAT_SIZES) + + @property + def format_regions(self) -> Optional[str]: + return self.get_option(OptionName.FORMAT_REGIONS) + + @property + def format_invoices(self) -> Optional[str]: + return self.get_option(OptionName.FORMAT_INVOICES) + + @property + def format_software(self) -> Optional[str]: + return self.get_option(OptionName.FORMAT_SOFTWARE) + + # Server creation default properties + + @property + def default_region(self) -> Optional[str]: + return self.get_option(OptionName.DEFAULT_REGION) + + @property + def default_size(self) -> Optional[str]: + return self.get_option(OptionName.DEFAULT_SIZE) + + @property + def default_image(self) -> Optional[str]: + return self.get_option(OptionName.DEFAULT_IMAGE) + + @property + def default_backups(self) -> Optional[bool]: + value = self.get_option(OptionName.DEFAULT_BACKUPS) + return self.to_bool(value) if value else None + + @property + def default_ssh_keys(self) -> Optional[str]: + return self.get_option(OptionName.DEFAULT_SSH_KEYS) + + @property + def default_user_data(self) -> Optional[str]: + return self.get_option(OptionName.DEFAULT_USER_DATA) + + @property + def default_port_blocking(self) -> Optional[bool]: + value = self.get_option(OptionName.DEFAULT_PORT_BLOCKING) + return self.to_bool(value) if value else None + + @property + def default_password(self) -> Optional[str]: + return self.get_option(OptionName.DEFAULT_PASSWORD) + + @property + def default_vpc(self) -> Optional[str]: + return self.get_option(OptionName.DEFAULT_VPC) + + # Terminal setting properties + + @property + def terminal_width(self) -> Optional[int]: + value = self.get_option(OptionName.TERMINAL_WIDTH) + if value and value.lower() != "auto": + try: + return int(value) + except ValueError: + return None + return None From 1c8c6895729844932cb18ad9a36486d461004a09 Mon Sep 17 00:00:00 2001 From: Freewheelin Date: Thu, 9 Oct 2025 11:30:33 +1000 Subject: [PATCH 2/8] feat: add preferences management commands --- src/binarylane/console/commands/__init__.py | 8 +- .../console/commands/preferences_get.py | 90 +++++++++ .../console/commands/preferences_set.py | 188 ++++++++++++++++++ 3 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 src/binarylane/console/commands/preferences_get.py create mode 100644 src/binarylane/console/commands/preferences_set.py diff --git a/src/binarylane/console/commands/__init__.py b/src/binarylane/console/commands/__init__.py index 4269b0f6..3168138b 100644 --- a/src/binarylane/console/commands/__init__.py +++ b/src/binarylane/console/commands/__init__.py @@ -6,7 +6,13 @@ from binarylane.console.runners import Descriptor __all__ = ["descriptors"] -descriptors: List[Descriptor] = list(api.descriptors) + [ + +# Filter out the auto-generated server create, we'll replace it with our wrapper +descriptors: List[Descriptor] = [d for d in api.descriptors if d.name != "server create"] + [ Descriptor(".commands.configure", "configure", "Configure access to BinaryLane API"), + Descriptor(".commands.preferences_get", "preferences get", "Display a preference value"), + Descriptor(".commands.preferences_set", "preferences set", "Set or unset a preference value"), + Descriptor(".commands.preferences_show", "preferences show", "Display preferences for a command or resource"), + Descriptor(".commands.server_create", "server create", "Create a new server"), Descriptor(".commands.version", "version", "Show the current version"), ] diff --git a/src/binarylane/console/commands/preferences_get.py b/src/binarylane/console/commands/preferences_get.py new file mode 100644 index 00000000..d853c7ad --- /dev/null +++ b/src/binarylane/console/commands/preferences_get.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, List + +from binarylane.config.options import OptionName + +from binarylane.console.runners import ExitCode, Runner + +if TYPE_CHECKING: + from binarylane.console.parser import Parser + + +class Command(Runner): + """Display a preference value for a given key, or list all set preferences""" + + def configure(self, parser: Parser) -> None: + parser.add_argument( + "key", + nargs="?", # Make key optional + help="Preference key to retrieve (omit to list all set preferences)", + ) + + def run(self, args: List[str]) -> None: + if args == [self.CHECK]: + return + + # Check for help first + if args and args[0] in [self.HELP, "-h", "--help"]: + self.parse(args) + return + + # If no arguments, list all set preferences + if not args: + self._list_all_preferences() + return + + # Get specific preference + key = args[0] + + # Validate key is a known option + try: + option = OptionName(key) + except ValueError: + print(f"Unknown preference key: {key}") + print("\nValid keys:") + for opt in OptionName: + print(f" {opt.value}") + self.error(ExitCode.API, "Invalid preference key") + + value = self._context.get_option(option) + if value is None: + print(f"{key} is not set") + else: + print(f"{key} = {value}") + + def _list_all_preferences(self) -> None: + """List all preferences that have been set""" + # Sensitive keys to exclude from listing + sensitive_keys = {"api-token", "default-password"} + + set_preferences = [] + + for option in OptionName: + # Skip sensitive values + if option.value in sensitive_keys: + continue + + value = self._context.get_option(option) + if value is not None: + set_preferences.append((option.value, value)) + + if not set_preferences: + print("No preferences are currently set.") + print("\nTo set a preference, use:") + print(" bl preferences set KEY VALUE") + print("\nAvailable preference keys:") + for opt in OptionName: + print(f" {opt.value}") + else: + print("Currently set preferences:") + print() + # Find the longest key name for formatting + max_key_len = max(len(key) for key, _ in set_preferences) + for key, value in sorted(set_preferences): + print(f" {key:<{max_key_len}} = {value}") + print() + print(f"Total: {len(set_preferences)} preference(s) set") + print() + print("Note: Sensitive values (api-token, default-password) are not listed.") + print(" Use 'bl preferences get ' to retrieve them individually.") diff --git a/src/binarylane/console/commands/preferences_set.py b/src/binarylane/console/commands/preferences_set.py new file mode 100644 index 00000000..113e7428 --- /dev/null +++ b/src/binarylane/console/commands/preferences_set.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, List + +from binarylane.config.options import OptionName +from binarylane.config.sources import FileSource + +from binarylane.console.runners import ExitCode, Runner + +if TYPE_CHECKING: + from binarylane.console.parser import Parser + + +class Command(Runner): + """Set or unset a preference value""" + + def configure(self, parser: Parser) -> None: + parser.add_argument("key", help="Preference key to set") + parser.add_argument("value", help='Value to set (or "null" to unset)') + + def run(self, args: List[str]) -> None: + if args == [self.CHECK]: + return + + # Check for help first + if not args or args[0] in [self.HELP, "-h", "--help"]: + self.parse(args) + return + + key = args[0] + + # Validate key is a known option + try: + option = OptionName(key) + except ValueError: + print(f"Unknown preference key: {key}") + print("\nValid keys:") + for opt in OptionName: + print(f" {opt.value}") + self.error(ExitCode.API, "Invalid preference key") + + # If no value provided, show contextual help + if len(args) < 2: + self._show_preference_help(option) + return + + value = args[1] + + # Update the file source directly + file_source = self._context.get_source(FileSource) + + if value and value.lower() == "null": + # Unset the key + file_source._section.pop(option.value, None) + print(f"Unset {key}") + else: + # Warn about sensitive values + if key.lower() in ["api-token", "password", "default-password"]: + print("⚠️ WARNING: Storing sensitive value in config file.") + print(f" Consider using environment variable instead: BL_{key.upper().replace('-', '_')}") + print() + + # Set the key + file_source._section[option.value] = value + print(f"Set {key} = {value}") + + # Save to disk + if file_source._path is None: + self.error(ExitCode.API, "Cannot save preferences: config file path is not set") + + with open(file_source._path, "w", encoding="utf-8") as f: + file_source._parser.write(f) + + def _show_preference_help(self, option: OptionName) -> None: + """Show contextual help for a specific preference key""" + key = option.value + current_value = self._context.get_option(option) + + # Show current value + if current_value is not None: + # Don't display sensitive values + if key in ["api-token", "default-password"]: + print("Currently set to: (hidden)") + else: + print(f"Currently set to: {current_value}") + else: + print("(not currently set)") + + print() + + # Category-specific help + help_info = self._get_help_info(key) + for line in help_info: + print(line) + + print() + print(f"To set: bl preferences set {key} ") + print(f"To unset: bl preferences set {key} null") + + def _get_help_info(self, key: str) -> List[str]: + """Get help information for a specific preference key""" + + # API-sourced list values + if key == "default-region": + return ["Available regions: bl region list", "Example: bl preferences set default-region syd"] + elif key == "default-size": + return ["Available sizes: bl size list", "Example: bl preferences set default-size std-min"] + elif key == "default-image": + return ["Available images: bl image list", "Example: bl preferences set default-image ubuntu-24.04"] + + # Boolean values + elif key == "default-port-blocking": + return [ + "Valid values: false (to disable port blocking)", + "Default (if not set): true (port blocking enabled)", + "Example: bl preferences set default-port-blocking false", + ] + elif key == "default-backups": + return [ + "Valid values: true (to enable backups)", + "Default (if not set): false (backups disabled)", + "Example: bl preferences set default-backups true", + ] + elif key == "default-ipv6": + return ["Valid values: true, false", "Example: bl preferences set default-ipv6 true"] + + # Format preferences + elif key.startswith("format-"): + # Map format-* keys to their CLI command names + format_to_command = { + "format-servers": "server", + "format-images": "image", + "format-domains": "domain", + "format-vpcs": "vpc", + "format-load-balancers": "load-balancer", + "format-ssh-keys": "ssh-key", + "format-actions": "action", + "format-sizes": "size", + "format-regions": "region", + "format-invoices": "account invoice", + "format-software": "software", + } + command = format_to_command.get(key, key.replace("format-", "")) + return [ + f'Available fields: bl {command} list --output "*"', + f'Example: bl preferences set {key} "id,name,status"', + "Note: Use quotes for comma-separated values", + ] + + # File path + elif key == "default-user-data": + return [ + "Path to cloud-config YAML file for server initialization", + "Example: bl preferences set default-user-data ~/.config/bl/cloud-init.yaml", + "Note: File must exist and contain valid cloud-config YAML", + ] + + # SSH keys + elif key == "default-ssh-keys": + return [ + "Available SSH keys: bl ssh-key list", + 'Example (single): bl preferences set default-ssh-keys "aa:bb:cc:..."', + 'Example (multiple): bl preferences set default-ssh-keys "aa:bb:cc:...,11:22:33:..."', + ] + + # VPC + elif key == "default-vpc": + return ["Available VPCs: bl vpc list", "Example: bl preferences set default-vpc 12345"] + + # Terminal width + elif key == "terminal-width": + return [ + "Valid values: numeric (e.g., 120)", + "Example: bl preferences set terminal-width 120", + "To use auto-detection: bl preferences set terminal-width null", + ] + + # Output format + elif key == "output-format": + return ["Valid values: table, plain, tsv, json", "Example: bl preferences set output-format json"] + + # Show header + elif key == "show-header": + return ["Valid values: true, false", "Example: bl preferences set show-header false"] + + # Generic fallback + else: + return [f"Example: bl preferences set {key} "] From 72ecd4a3eadbdb096fd16d710e67bdaf65b4d5d0 Mon Sep 17 00:00:00 2001 From: Freewheelin Date: Thu, 9 Oct 2025 11:31:06 +1000 Subject: [PATCH 3/8] feat: add preferences show command with grouped display --- .../console/commands/preferences_show.py | 539 ++++++++++++++++++ 1 file changed, 539 insertions(+) create mode 100644 src/binarylane/console/commands/preferences_show.py diff --git a/src/binarylane/console/commands/preferences_show.py b/src/binarylane/console/commands/preferences_show.py new file mode 100644 index 00000000..23774c20 --- /dev/null +++ b/src/binarylane/console/commands/preferences_show.py @@ -0,0 +1,539 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Dict, List + +from binarylane.config.options import OptionName + +from binarylane.console.runners import ExitCode, Runner + +if TYPE_CHECKING: + from binarylane.console.parser import Parser + + +class Command(Runner): + """Display preference values for a specific command or resource, or all preferences grouped by category""" + + # Map command patterns to relevant option prefixes + COMMAND_GROUPS: Dict[str, tuple[str, List[OptionName]]] = { + "server create": ( + "Server Creation Defaults", + [ + OptionName.DEFAULT_REGION, + OptionName.DEFAULT_SIZE, + OptionName.DEFAULT_IMAGE, + OptionName.DEFAULT_BACKUPS, + OptionName.DEFAULT_SSH_KEYS, + OptionName.DEFAULT_USER_DATA, + OptionName.DEFAULT_PORT_BLOCKING, + OptionName.DEFAULT_PASSWORD, + OptionName.DEFAULT_VPC, + ], + ), + "image list": ( + "Image List Preferences", + [ + OptionName.FORMAT_IMAGES, + OptionName.OUTPUT_FORMAT, + OptionName.SHOW_HEADER, + ], + ), + "server list": ( + "Server List Preferences", + [ + OptionName.FORMAT_SERVERS, + OptionName.OUTPUT_FORMAT, + OptionName.SHOW_HEADER, + ], + ), + "domain list": ( + "Domain List Preferences", + [ + OptionName.FORMAT_DOMAINS, + OptionName.OUTPUT_FORMAT, + OptionName.SHOW_HEADER, + ], + ), + "vpc list": ( + "VPC List Preferences", + [ + OptionName.FORMAT_VPCS, + OptionName.OUTPUT_FORMAT, + OptionName.SHOW_HEADER, + ], + ), + "load-balancer list": ( + "Load Balancer List Preferences", + [ + OptionName.FORMAT_LOAD_BALANCERS, + OptionName.OUTPUT_FORMAT, + OptionName.SHOW_HEADER, + ], + ), + "ssh-key list": ( + "SSH Key List Preferences", + [ + OptionName.FORMAT_SSH_KEYS, + OptionName.OUTPUT_FORMAT, + OptionName.SHOW_HEADER, + ], + ), + "action list": ( + "Action List Preferences", + [ + OptionName.FORMAT_ACTIONS, + OptionName.OUTPUT_FORMAT, + OptionName.SHOW_HEADER, + ], + ), + "size list": ( + "Size List Preferences", + [ + OptionName.FORMAT_SIZES, + OptionName.OUTPUT_FORMAT, + OptionName.SHOW_HEADER, + ], + ), + "region list": ( + "Region List Preferences", + [ + OptionName.FORMAT_REGIONS, + OptionName.OUTPUT_FORMAT, + OptionName.SHOW_HEADER, + ], + ), + "invoice list": ( + "Invoice List Preferences", + [ + OptionName.FORMAT_INVOICES, + OptionName.OUTPUT_FORMAT, + OptionName.SHOW_HEADER, + ], + ), + "software list": ( + "Software List Preferences", + [ + OptionName.FORMAT_SOFTWARE, + OptionName.OUTPUT_FORMAT, + OptionName.SHOW_HEADER, + ], + ), + "terminal": ( + "Terminal Settings", + [ + OptionName.TERMINAL_WIDTH, + ], + ), + } + + # Category groupings for displaying all preferences + ALL_CATEGORIES: List[tuple[str, List[OptionName]]] = [ + ( + "Server Creation Defaults", + [ + OptionName.DEFAULT_REGION, + OptionName.DEFAULT_SIZE, + OptionName.DEFAULT_IMAGE, + OptionName.DEFAULT_BACKUPS, + OptionName.DEFAULT_SSH_KEYS, + OptionName.DEFAULT_USER_DATA, + OptionName.DEFAULT_PORT_BLOCKING, + OptionName.DEFAULT_PASSWORD, + OptionName.DEFAULT_VPC, + ], + ), + ( + "Output Formatting", + [ + OptionName.FORMAT_IMAGES, + OptionName.FORMAT_SERVERS, + OptionName.FORMAT_DOMAINS, + OptionName.FORMAT_VPCS, + OptionName.FORMAT_LOAD_BALANCERS, + OptionName.FORMAT_SSH_KEYS, + OptionName.FORMAT_ACTIONS, + OptionName.FORMAT_SIZES, + OptionName.FORMAT_REGIONS, + OptionName.FORMAT_INVOICES, + OptionName.FORMAT_SOFTWARE, + OptionName.OUTPUT_FORMAT, + OptionName.SHOW_HEADER, + ], + ), + ( + "Terminal Settings", + [ + OptionName.TERMINAL_WIDTH, + ], + ), + ] + + def configure(self, parser: Parser) -> None: + parser.add_argument( + "command_group", + nargs="*", # Optional - if empty, show all grouped + help=f"Command or resource to show preferences for. Omit to show all preferences grouped by category. Valid options: {', '.join(repr(g) for g in self.COMMAND_GROUPS.keys())}", + ) + + def run(self, args: List[str]) -> None: + if args == [self.CHECK]: + return + + # Check for help first + if args and args[0] in [self.HELP, "-h", "--help"]: + self.parse(args) + return + + # If no arguments, show all preferences grouped by category + if not args: + self._show_all_grouped() + return + + # Join args to handle multi-word commands like "server create" + command_group = " ".join(args).lower() + + # Check if it's a command group first + if command_group in self.COMMAND_GROUPS: + self._show_command_group(command_group) + return + + # Check if it's a single preference key + if len(args) == 1: + preference_key = args[0].lower() + # Try to find matching OptionName + matching_option = None + for option in OptionName: + if option.value == preference_key: + matching_option = option + break + + if matching_option: + self._show_single_preference(matching_option) + return + + # Not a valid command group or preference key + print(f"Unknown command group or preference: '{command_group}'") + print("\nValid command groups:") + for group in sorted(self.COMMAND_GROUPS.keys()): + print(f" {group}") + print("\nOr show a specific preference by name:") + print(" bl preferences show default-region") + print(" bl preferences show terminal-width") + print() + print("Example usage:") + print(" bl preferences show") + print(" bl preferences show server create") + print(" bl preferences show default-region") + self.error(ExitCode.API, "Invalid command group or preference") + + def _get_option_description(self, option: OptionName) -> str: + """Get a description for an option""" + descriptions = { + OptionName.TERMINAL_WIDTH: "Terminal width for help text formatting", + OptionName.OUTPUT_FORMAT: "Output format (table, plain, tsv, json)", + OptionName.SHOW_HEADER: "Show column headers in output", + OptionName.FORMAT_IMAGES: "Custom columns for image list", + OptionName.FORMAT_SERVERS: "Custom columns for server list", + OptionName.FORMAT_DOMAINS: "Custom columns for domain list", + OptionName.FORMAT_VPCS: "Custom columns for VPC list", + OptionName.FORMAT_LOAD_BALANCERS: "Custom columns for load balancer list", + OptionName.FORMAT_SSH_KEYS: "Custom columns for SSH key list", + OptionName.FORMAT_ACTIONS: "Custom columns for action list", + OptionName.FORMAT_SIZES: "Custom columns for size list", + OptionName.FORMAT_REGIONS: "Custom columns for region list", + OptionName.FORMAT_INVOICES: "Custom columns for invoice list", + OptionName.FORMAT_SOFTWARE: "Custom columns for software list", + OptionName.DEFAULT_REGION: "Default region for server creation", + OptionName.DEFAULT_SIZE: "Default size for server creation", + OptionName.DEFAULT_IMAGE: "Default image for server creation", + OptionName.DEFAULT_BACKUPS: "Enable backups by default (API default: false)", + OptionName.DEFAULT_PORT_BLOCKING: "Disable port blocking (API default: true/enabled)", + OptionName.DEFAULT_SSH_KEYS: "Default SSH key IDs (comma-separated)", + OptionName.DEFAULT_USER_DATA: "Default cloud-init user data", + OptionName.DEFAULT_PASSWORD: "Default root/administrator password", + OptionName.DEFAULT_VPC: "Default VPC ID for server creation", + } + return descriptions.get(option, "") + + def _show_command_group(self, command_group: str) -> None: + """Display preferences for a specific command group""" + title, option_names = self.COMMAND_GROUPS[command_group] + print(f"{title}:") + print() + + # Find the longest key name for formatting + max_key_len = max(len(opt.value) for opt in option_names) + + # Separate set and unset options + set_options = [] + unset_options = [] + for option in option_names: + value = self._context.get_option(option) + if value is not None: + set_options.append((option, value)) + else: + unset_options.append(option) + + # Show currently set preferences + if set_options: + print("Currently set:") + print() + for option, value in set_options: + print(f" {option.value:<{max_key_len}} = {value}") + print() + + # Show available (not set) preferences + if unset_options: + print("Available (not set):") + print() + for option in unset_options: + description = self._get_option_description(option) + if description: + print(f" {option.value:<{max_key_len}} - {description}") + else: + print(f" {option.value}") + print() + + if not set_options and not unset_options: + print(f" (no preferences available for {command_group})") + print() + + # For server create, show missing required fields and example command + if command_group == "server create": + self._show_missing_required_fields() + self._show_example_command() + + print("To set a preference: bl preferences set ") + print("To unset: bl preferences set null") + + def _show_missing_required_fields(self) -> None: + """Show which required server creation fields are not set as defaults""" + # Required fields for server creation + required_fields = [ + (OptionName.DEFAULT_REGION, "--region REGION", "bl region list"), + (OptionName.DEFAULT_SIZE, "--size SIZE", "bl size list"), + (OptionName.DEFAULT_IMAGE, "--image IMAGE", "bl image list"), + ] + + missing = [] + for option, arg_format, list_cmd in required_fields: + value = self._context.get_option(option) + if value is None: + missing.append((arg_format, list_cmd)) + + if missing: + print() + print("Required for server creation (provide via command line or set as preference defaults):") + for arg_format, list_cmd in missing: + print(f" {arg_format:<20} (see: {list_cmd})") + else: + print() + print("All required server creation fields have default values set.") + + def _show_example_command(self) -> None: + """Show what the command would look like with current defaults applied""" + # Check which preferences are set + preference_map = [ + (OptionName.DEFAULT_REGION, "--region"), + (OptionName.DEFAULT_SIZE, "--size"), + (OptionName.DEFAULT_IMAGE, "--image"), + (OptionName.DEFAULT_BACKUPS, "--backups"), + (OptionName.DEFAULT_PORT_BLOCKING, "--port-blocking"), + (OptionName.DEFAULT_SSH_KEYS, "--ssh-keys"), + (OptionName.DEFAULT_USER_DATA, "--user-data"), + (OptionName.DEFAULT_PASSWORD, "--password"), + (OptionName.DEFAULT_VPC, "--vpc"), + ] + + args = [] + for option, flag in preference_map: + value = self._context.get_option(option) + if value is not None: + # Handle boolean values + if option in (OptionName.DEFAULT_BACKUPS, OptionName.DEFAULT_PORT_BLOCKING): + args.append(f"{flag} {value}") + else: + args.append(f"{flag} {value}") + + if args: + print() + print("With your current defaults, this command:") + print(" bl server create --name myserver") + print() + print("Expands to (what gets sent to the API):") + print(f" bl server create --name myserver {' '.join(args)}") + print() + + def _show_single_preference(self, option: OptionName) -> None: + """Display detailed information about a single preference""" + key = option.value + value = self._context.get_option(option) + description = self._get_option_description(option) + + # Don't show sensitive values + if key in {"api-token", "default-password"}: + print(f"{key}: (sensitive - use 'bl preferences get {key}' to view)") + return + + # Show current value + if value is not None: + print(f"{key} = {value}") + else: + print(f"{key} is not set") + + print() + + # Show description if available + if description: + print(f"Description: {description}") + print() + + # Show help/examples based on the key + help_info = self._get_help_info_for_key(key) + if help_info: + for line in help_info: + print(f" {line}") + print() + + print(f"To set: bl preferences set {key} ") + print(f"To unset: bl preferences set {key} null") + + def _get_help_info_for_key(self, key: str) -> List[str]: + """Get help information for a specific preference key""" + + # API-sourced list values + if key == "default-region": + return ["Available regions: bl region list", "Example: bl preferences set default-region syd"] + elif key == "default-size": + return ["Available sizes: bl size list", "Example: bl preferences set default-size std-min"] + elif key == "default-image": + return ["Available images: bl image list", "Example: bl preferences set default-image ubuntu-24.04"] + + # Boolean values + elif key == "default-port-blocking": + return [ + "Valid values: false (to disable port blocking)", + "Default (if not set): true (port blocking enabled)", + "Example: bl preferences set default-port-blocking false", + ] + elif key == "default-backups": + return [ + "Valid values: true (to enable backups)", + "Default (if not set): false (backups disabled)", + "Example: bl preferences set default-backups true", + ] + + # Format preferences + elif key.startswith("format-"): + format_to_command = { + "format-servers": "server", + "format-images": "image", + "format-domains": "domain", + "format-vpcs": "vpc", + "format-load-balancers": "load-balancer", + "format-ssh-keys": "ssh-key", + "format-actions": "action", + "format-sizes": "size", + "format-regions": "region", + "format-invoices": "account invoice", + "format-software": "software", + } + command = format_to_command.get(key, key.replace("format-", "")) + return [ + f'Available fields: bl {command} list --output "*"', + f'Example: bl preferences set {key} "id,name,status"', + "Note: Use quotes for comma-separated values", + ] + + # File path + elif key == "default-user-data": + return [ + "Path to user-data file for server initialization", + "Example: bl preferences set default-user-data ~/.config/bl/my-user-data.yaml", + "Note: File must exist", + ] + + # SSH keys + elif key == "default-ssh-keys": + return [ + "Available SSH keys: bl ssh-key list", + 'Example (single): bl preferences set default-ssh-keys "aa:bb:cc:..."', + 'Example (multiple): bl preferences set default-ssh-keys "aa:bb:cc:...,11:22:33:..."', + ] + + # VPC + elif key == "default-vpc": + return ["Available VPCs: bl vpc list", "Example: bl preferences set default-vpc 12345"] + + # Terminal width + elif key == "terminal-width": + return [ + "Valid values: numeric (e.g., 120)", + "Example: bl preferences set terminal-width 120", + "To use auto-detection: bl preferences set terminal-width null", + ] + + # Output format + elif key == "output-format": + return ["Valid values: table, plain, tsv, json", "Example: bl preferences set output-format json"] + + # Show header + elif key == "show-header": + return ["Valid values: true, false", "Example: bl preferences set show-header false"] + + # Password (discouraged) + elif key == "default-password": + return [ + "Warning: Storing passwords is not recommended", + "Consider using SSH keys instead (default-ssh-keys)", + "Example: bl preferences set default-password ", + ] + + # Generic fallback + return [f"Example: bl preferences set {key} "] + + def _show_all_grouped(self) -> None: + """Display all set preferences grouped by category""" + # Sensitive keys to exclude from display + sensitive_keys = {"api-token", "default-password"} + + total_count = 0 + has_any_values = False + + for category_name, option_names in self.ALL_CATEGORIES: + # Collect values for this category + category_values = [] + for option in option_names: + if option.value in sensitive_keys: + continue + value = self._context.get_option(option) + if value is not None: + category_values.append((option.value, value)) + + # Only display category if it has values + if category_values: + has_any_values = True + print(f"{category_name}:") + + # Find the longest key name in this category for formatting + max_key_len = max(len(key) for key, _ in category_values) + + for key, value in category_values: + print(f" {key:<{max_key_len}} = {value}") + total_count += 1 + + print() # Blank line between categories + + if not has_any_values: + print("No preferences are currently set.") + print() + print("To set a preference, use:") + print(" bl preferences set ") + print() + print("To view preferences for a specific command:") + print(" bl preferences show server create") + print(" bl preferences show output") + else: + print(f"Total: {total_count} preference(s) set") + print() + print("Note: Sensitive values (api-token, default-password) are not displayed.") + print(" Use 'bl preferences show ' to view specific command preferences.") From 2ba79711a3b36963cd4d6fc8ad0cb518ff0e09e6 Mon Sep 17 00:00:00 2001 From: Freewheelin Date: Thu, 9 Oct 2025 11:30:25 +1000 Subject: [PATCH 4/8] feat: implement format preferences for list commands --- src/binarylane/console/runners/list.py | 39 ++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/binarylane/console/runners/list.py b/src/binarylane/console/runners/list.py index 58f84ee6..b4fc6046 100644 --- a/src/binarylane/console/runners/list.py +++ b/src/binarylane/console/runners/list.py @@ -3,7 +3,7 @@ import fnmatch import re from abc import abstractmethod -from typing import TYPE_CHECKING, Dict, List +from typing import TYPE_CHECKING, Dict, List, Optional from binarylane.console.runners.command import CommandRunner @@ -28,9 +28,44 @@ def default_format(self) -> List[str]: def fields(self) -> Dict[str, str]: """Map of field name: description for all available fields""" + def _get_config_format(self) -> Optional[str]: + """Get format preference from config based on command name""" + # Map command patterns to config properties + # This could be more sophisticated, but explicit mapping is clearer + command_name = self._context.name.lower() + + if "image" in command_name: + return self._context.format_images + elif "server" in command_name and "list" in command_name: + return self._context.format_servers + elif "domain" in command_name: + return self._context.format_domains + elif "vpc" in command_name: + return self._context.format_vpcs + elif "load-balancer" in command_name: + return self._context.format_load_balancers + elif "ssh-key" in command_name: + return self._context.format_ssh_keys + elif "action" in command_name: + return self._context.format_actions + elif "size" in command_name: + return self._context.format_sizes + elif "region" in command_name: + return self._context.format_regions + elif "invoice" in command_name: + return self._context.format_invoices + elif "software" in command_name: + return self._context.format_software + + return None + def configure(self, parser: Parser) -> None: super().configure(parser) + # Use config format as default if available + config_format = self._get_config_format() + default_format = config_format if config_format else ",".join(self.default_format) + parser.add_group_help(title="Available fields", entries=self.fields) parser.add_argument( "--format", @@ -38,7 +73,7 @@ def configure(self, parser: Parser) -> None: help='Comma-separated list of fields to display. Wildcards are supported: \ e.g. --format "*" will display all fields. (default: "%(default)s")', metavar="FIELD,...", - default=",".join(self.default_format), + default=default_format, ) parser.add_argument( "-1", From dcc30bbec223ae2278e1f593234550797b80bd90 Mon Sep 17 00:00:00 2001 From: Freewheelin Date: Thu, 9 Oct 2025 11:30:50 +1000 Subject: [PATCH 5/8] feat: integrate terminal-width and output format preferences --- src/binarylane/config/__init__.py | 43 +++++++++++++++++++++++ src/binarylane/console/parser/parser.py | 21 ++++++++--- src/binarylane/console/runners/command.py | 5 ++- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/binarylane/config/__init__.py b/src/binarylane/config/__init__.py index 37c28c00..e5e2b18e 100644 --- a/src/binarylane/config/__init__.py +++ b/src/binarylane/config/__init__.py @@ -51,6 +51,49 @@ def save(self) -> None: str(self.api_development).lower() if self.api_development != default.api_development else None ) + # Output preferences - only save if explicitly set + if self.output_format: + config_options[OptionName.OUTPUT_FORMAT] = self.output_format + if self.show_header is not None: + config_options[OptionName.SHOW_HEADER] = str(self.show_header).lower() + + # Per-command format preferences + for opt in [ + OptionName.FORMAT_IMAGES, + OptionName.FORMAT_SERVERS, + OptionName.FORMAT_DOMAINS, + OptionName.FORMAT_VPCS, + OptionName.FORMAT_LOAD_BALANCERS, + OptionName.FORMAT_SSH_KEYS, + ]: + value = self.get_option(opt) + if value: + config_options[opt] = value + + # Server creation defaults + for opt in [ + OptionName.DEFAULT_REGION, + OptionName.DEFAULT_SIZE, + OptionName.DEFAULT_IMAGE, + OptionName.DEFAULT_SSH_KEYS, + OptionName.DEFAULT_USER_DATA, + OptionName.DEFAULT_PASSWORD, + OptionName.DEFAULT_VPC, + ]: + value = self.get_option(opt) + if value: + config_options[opt] = value + + # Boolean defaults + if self.default_backups is not None: + config_options[OptionName.DEFAULT_BACKUPS] = str(self.default_backups).lower() + if self.default_port_blocking is not None: + config_options[OptionName.DEFAULT_PORT_BLOCKING] = str(self.default_port_blocking).lower() + + # Terminal settings + if self.terminal_width: + config_options[OptionName.TERMINAL_WIDTH] = str(self.terminal_width) + # Write configuration to disk file = self.get_source(src.FileSource) file.save(config_options) diff --git a/src/binarylane/console/parser/parser.py b/src/binarylane/console/parser/parser.py index 57dde76a..b639c460 100644 --- a/src/binarylane/console/parser/parser.py +++ b/src/binarylane/console/parser/parser.py @@ -32,13 +32,17 @@ class Parser(argparse.ArgumentParser): _keywords: List[str] _groups: Dict[str, ArgumentGroup] _dest_counter: int = 0 + _context: Optional[Any] = None # Optional Context to access terminal width config # Optional callback on completion of argument parsing, but prior to constructing mapped_object on_parse_args: Callable[[Namespace], None] = staticmethod(lambda _: None) # type: ignore - def __init__(self, prog: str, description: Optional[str] = None, epilog: Optional[str] = None) -> None: + def __init__( + self, prog: str, description: Optional[str] = None, epilog: Optional[str] = None, context: Optional[Any] = None + ) -> None: super().__init__(prog=prog, description=description, epilog=epilog, add_help=False, allow_abbrev=False) + self._context = context self._groups = { "required=True": self.add_argument_group(title="Arguments"), "required=False": self.add_argument_group(title="Parameters"), @@ -48,9 +52,18 @@ def __init__(self, prog: str, description: Optional[str] = None, epilog: Optiona self._keywords = [] def _get_formatter(self) -> HelpFormatter: - # argparse defaults to 70 when terminal size is unavailable, which is rather narrow - size = shutil.get_terminal_size((80, 25)) - return CommandHelpFormatter(self.prog, width=size.columns - 2) + # Check if user configured a specific width + config_width = getattr(self._context, "terminal_width", None) if self._context else None + + if config_width: + # User specified exact width in config + width = config_width + else: + # Auto-detect terminal width with fallback to 80 + size = shutil.get_terminal_size((80, 25)) + width = size.columns + + return CommandHelpFormatter(self.prog, width=width - 2) @property def argument_names(self) -> List[str]: diff --git a/src/binarylane/console/runners/command.py b/src/binarylane/console/runners/command.py index c03dad66..e764b4ba 100644 --- a/src/binarylane/console/runners/command.py +++ b/src/binarylane/console/runners/command.py @@ -37,7 +37,10 @@ class CommandRunner(Runner): def __init__(self, context: Context) -> None: super().__init__(context) - self._output = self._default_output + # Apply config defaults, can be overridden by CLI args + self._output = context.output_format or self._default_output + if context.show_header is not None: + self._header = context.show_header @property def _default_output(self) -> str: From 7fb4ea32ca247bdb658e416c2e91ecf06441d1cf Mon Sep 17 00:00:00 2001 From: Freewheelin Date: Thu, 9 Oct 2025 11:31:19 +1000 Subject: [PATCH 6/8] feat: create server create wrapper for preference defaults --- .../console/commands/server_create.py | 81 +++++++++++++++++++ src/binarylane/console/runners/__init__.py | 2 +- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/binarylane/console/commands/server_create.py diff --git a/src/binarylane/console/commands/server_create.py b/src/binarylane/console/commands/server_create.py new file mode 100644 index 00000000..ad85d669 --- /dev/null +++ b/src/binarylane/console/commands/server_create.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Tuple, Union + +from binarylane.types import Unset + +from binarylane.console.commands.api import post_v2_servers + +if TYPE_CHECKING: + from http import HTTPStatus + + from binarylane.client import Client + from binarylane.models.create_server_response import CreateServerResponse + from binarylane.models.validation_problem_details import ValidationProblemDetails + + +class Command(post_v2_servers.Command): + """Server create command with preference defaults + + This wraps the auto-generated server create command to inject + user-configured default values from preferences. + """ + + def run(self, args: List[str]) -> None: + """Override run to inject config defaults into args before parsing""" + if args == [self.CHECK]: + return super().run(args) + + # Inject CLI defaults for required parameters + ctx = self._context + modified_args = list(args) + + if "--size" not in modified_args and ctx.default_size: + modified_args.extend(["--size", ctx.default_size]) + if "--image" not in modified_args and ctx.default_image: + modified_args.extend(["--image", str(ctx.default_image)]) + if "--region" not in modified_args and ctx.default_region: + modified_args.extend(["--region", ctx.default_region]) + + super().run(modified_args) + + def request( + self, + client: Client, + request: object, + ) -> Tuple[HTTPStatus, Union[None, CreateServerResponse, ValidationProblemDetails]]: + """Override request to inject config defaults for optional parameters""" + # First, inject defaults into request body + body = request.json_body # type: ignore[attr-defined] + ctx = self._context + + # Optional parameters with config defaults + if isinstance(getattr(body, "backups", Unset), type(Unset)): + if ctx.default_backups is not None: + body.backups = ctx.default_backups + + if isinstance(getattr(body, "ssh_keys", Unset), type(Unset)): + if ctx.default_ssh_keys: + body.ssh_keys = [key.strip() for key in ctx.default_ssh_keys.split(",")] + + if isinstance(getattr(body, "user_data", Unset), type(Unset)): + if ctx.default_user_data: + body.user_data = ctx.default_user_data + + if isinstance(getattr(body, "port_blocking", Unset), type(Unset)): + if ctx.default_port_blocking is not None: + body.port_blocking = ctx.default_port_blocking + + if isinstance(getattr(body, "password", Unset), type(Unset)): + if ctx.default_password: + body.password = ctx.default_password + + if isinstance(getattr(body, "vpc_id", Unset), type(Unset)): + if ctx.default_vpc: + try: + body.vpc_id = int(ctx.default_vpc) + except ValueError: + pass # Let lookup mechanism handle it + + # Now call parent's request with modified body + return super().request(client, request) diff --git a/src/binarylane/console/runners/__init__.py b/src/binarylane/console/runners/__init__.py index e2916761..bdbfe032 100644 --- a/src/binarylane/console/runners/__init__.py +++ b/src/binarylane/console/runners/__init__.py @@ -69,7 +69,7 @@ class Runner(ABC): _parser: Parser def __init__(self, context: Context) -> None: - self._parser = Parser(context.prog, context.description) + self._parser = Parser(context.prog, context.description, context=context) self._parser.add_argument("--help", help=self.HELP_DESCRIPTION, action="help") self._context = context From e55c5309668bca78ff803c37e0ea3888e01f63e1 Mon Sep 17 00:00:00 2001 From: Freewheelin Date: Thu, 9 Oct 2025 12:02:24 +1000 Subject: [PATCH 7/8] docs: add user preferences guide and update README --- PREFERENCES.md | 967 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 43 ++- 2 files changed, 1007 insertions(+), 3 deletions(-) create mode 100644 PREFERENCES.md diff --git a/PREFERENCES.md b/PREFERENCES.md new file mode 100644 index 00000000..6b91448e --- /dev/null +++ b/PREFERENCES.md @@ -0,0 +1,967 @@ +# BinaryLane CLI User Preferences Guide + +This guide covers all aspects of customizing the BinaryLane CLI using preferences, including output formatting and server creation defaults. + +## Table of Contents + +- [Quick Start](#quick-start) +- [Managing Preferences](#managing-preferences) +- [Server Creation Defaults](#server-creation-defaults) +- [Output Formatting Preferences](#output-formatting-preferences) +- [Terminal Settings](#console-settings) +- [Configuration Priority](#configuration-priority) +- [Field Reference](#field-reference) + - [Images](#images) + - [Servers](#servers) + - [Domains](#domains) + - [VPCs](#vpcs) + - [Load Balancers](#load-balancers) + - [SSH Keys](#ssh-keys) + - [Actions](#actions) + - [And more...](#additional-resources) + +--- + +## Quick Start + +Preferences allow you to customize the CLI's behavior without repeating the same options every time. + +### Without Preferences (Full Command) + +```bash +# Create a server - must specify all required parameters +bl server create --size std-min --image ubuntu-24.04 --region syd + +# Name is optional (auto-generated if omitted) +bl server create --name web-01 --size std-min --image ubuntu-24.04 --region syd + +# Every server requires repeating these options +bl server create --name api-01 --size std-min --image ubuntu-24.04 --region syd +bl server create --size std-2vcpu --image ubuntu-24.04 --region syd # Different size +``` + +### With Preferences (Simplified) + +```bash +# Step 1: Set preferences once +bl preferences set default-region syd +bl preferences set default-size std-min +bl preferences set default-image ubuntu-24.04 + +# Step 2: Verify your configuration +bl preferences show server create +``` + +Output: +``` +Server Creation Defaults: + +Currently set: + + default-region = syd + default-size = std-min + default-image = ubuntu-24.04 + +Available (not set): + + default-backups - Enable backups by default (API default: false) + default-ssh-keys - Default SSH key IDs (comma-separated) + default-user-data - Default user data file path + default-port-blocking - Disable port blocking (API default: true/enabled) + default-password - Default root/administrator password + default-vpc - Default VPC ID for server creation + + +All required server creation fields have default values set. + +With your current defaults, this command: + bl server create --name myserver + +Expands to (what gets sent to the API): + bl server create --name myserver --region syd --size std-min --image ubuntu-24.04 + +To set a preference: bl preferences set +To unset: bl preferences set null +``` + +```bash +# Step 3: Now create servers with zero arguments +bl server create +# ↑ Uses: size=std-min, image=ubuntu-24.04, region=syd (all from preferences) + +# Or specify a custom name if you want +bl server create --name web-01 +# ↑ Uses: name=web-01 (specified), size=std-min, image=ubuntu-24.04, region=syd (from preferences) + +# Create multiple servers quickly +bl server create +bl server create +bl server create --name db-worker --size std-2vcpu +# ↑ Last one overrides: size=std-2vcpu, uses preferences for other settings +``` + +### Customize Output Format + +```bash +# Set your preferred columns for server lists +bl preferences set format-servers "id,name,status,region,created_at" + +# Now lists use your format automatically +bl server list +# Shows only: id, name, status, region, created_at columns +``` + +--- + +## Managing Preferences + +### Set a Preference + +```bash +bl preferences set +``` + +Examples: +```bash +bl preferences set default-region syd +bl preferences set format-servers "id,name,status,region" +bl preferences set terminal-width 120 +``` + +**Need help with a preference?** Omit the value to see contextual help: + +```bash +# Show help for a specific preference +bl preferences set default-port-blocking + +# Output: +# (not currently set) +# +# Valid values: false (to disable port blocking) +# Default (if not set): true (port blocking enabled) +# Example: bl preferences set default-port-blocking false +# +# To set: bl preferences set default-port-blocking +# To unset: bl preferences set default-port-blocking null +``` + +This works for all preferences and will show: +- Current value (if set) +- Where to find valid values (e.g., `bl region list`) +- Examples +- How to unset + +### View a Specific Preference + +There are two ways to view preferences: + +**`preferences get`** - Shows just the value (quick lookup): +```bash +bl preferences get default-region +# Output: default-region = syd +``` + +**`preferences show`** - Shows detailed information with help text: +```bash +bl preferences show default-region +# Output: +# default-region = syd +# +# Description: Default region for server creation +# +# Available regions: bl region list +# Example: bl preferences set default-region syd +# +# To set: bl preferences set default-region +# To unset: bl preferences set default-region null +``` + +**When to use which:** +- Use `get` when you just need the value +- Use `show` when you want help on how to set it or what values are valid + +### List All Preferences + +```bash +bl preferences get +``` + +This displays all currently set preferences (excludes sensitive values like `api-token`). + +### View Preferences by Command Group + +The `preferences show` command can also group related preferences together: + +```bash +# Show all server creation defaults +bl preferences show server create + +# Show Terminal Settings +bl preferences show terminal + +# Show list formatting preferences +bl preferences show image list +bl preferences show server list +bl preferences show domain list +bl preferences show vpc list +bl preferences show load-balancer list +bl preferences show ssh-key list +bl preferences show action list +bl preferences show size list +bl preferences show region list +bl preferences show invoice list +bl preferences show software list +``` + +Example output with all required fields set: +``` +Server Creation Defaults: + +Currently set: + + default-region = syd + default-size = std-min + default-image = ubuntu-24.04 + +Available (not set): + + default-backups - Enable backups by default (API default: false) + default-ssh-keys - Default SSH key IDs (comma-separated) + default-user-data - Default user data file path + default-port-blocking - Disable port blocking (API default: true/enabled) + default-password - Default root/administrator password + default-vpc - Default VPC ID for server creation + + +All required server creation fields have default values set. + +With your current defaults, this command: + bl server create --name myserver + +Expands to (what gets sent to the API): + bl server create --name myserver --region syd --size std-min --image ubuntu-24.04 + +To set a preference: bl preferences set +To unset: bl preferences set null +``` + +**Note:** `region`, `size`, and `image` are required for server creation. All other preferences (like `default-backups`) are optional. + +Example output with missing required fields: +``` +Server Creation Defaults: + +Currently set: + + default-region = syd + default-image = ubuntu-24.04 + +Available (not set): + + default-size - Default size for server creation + default-backups - Enable backups by default (API default: false) + default-ssh-keys - Default SSH key IDs (comma-separated) + default-user-data - Default user data file path + default-port-blocking - Disable port blocking (API default: true/enabled) + default-password - Default root/administrator password + default-vpc - Default VPC ID for server creation + + +Required for server creation (provide via command line or set as preference defaults): + --size SIZE (see: bl size list) + +To set a preference: bl preferences set +To unset: bl preferences set null +``` + +### Reset a Preference + +Set any preference to `null` to reset it to the CLI's built-in default: + +```bash +bl preferences set default-region null +bl preferences set format-images null +bl preferences set terminal-width null +``` + +--- + +## Server Creation Defaults + +Set default values for server creation to speed up your workflow and ensure consistency: + +### Available Server Defaults + +| Preference Key | Description | Example Value | +|----------------|-------------|---------------| +| `default-region` | Default deployment region | `syd`, `bne`, `per` | +| `default-size` | Default server size | `std-min`, `std-1vcpu`, `std-2vcpu` | +| `default-image` | Default OS image | `ubuntu-24.04`, `debian-12` | +| `default-backups` | Enable backups by default (API default: false) | `true` | +| `default-ssh-keys` | Default SSH key fingerprints (comma-separated) | `aa:bb:cc:...` | +| `default-port-blocking` | Disable port blocking for email/SSH/RDP (API default: enabled) | `false` | +| `default-user-data` | Path to user-data file | `~/.config/bl/my-user-data.yaml` | +| `default-password` | Default password (not recommended) | Use SSH keys instead! | +| `default-vpc` | Default VPC ID | `12345` | + +### Basic Example + +**Before setting preferences:** +```bash +# Must specify all required fields every time +bl server create --name web-01 --size std-min --image ubuntu-24.04 --region syd +bl server create --name web-02 --size std-min --image ubuntu-24.04 --region syd +bl server create --name web-03 --size std-min --image ubuntu-24.04 --region syd +``` + +**After setting preferences:** +```bash +# Set your common defaults once +bl preferences set default-region syd +bl preferences set default-size std-min +bl preferences set default-image ubuntu-24.04 + +# Now create servers without specifying these every time +bl server create +# ↑ Automatically uses: size=std-min, image=ubuntu-24.04, region=syd + +bl server create +# ↑ Another server with same config + +# Or specify custom names when needed +bl server create --name web-01 +# ↑ Custom name: web-01, uses preferences for everything else +``` + +**Override when needed:** +```bash +# Override specific values while keeping other preferences +bl server create --name db-server --size std-4vcpu +# ↑ Override: size=std-4vcpu +# ↑ Still uses preferences: image=ubuntu-24.04, region=syd + +bl server create --name test-debian --image debian-12 --region per +# ↑ Override: image=debian-12, region=per +# ↑ Still uses preferences: size=std-min +``` + +### Advanced Example: User Data Files + +The `default-user-data` preference allows you to specify a file path that will be automatically passed to all new servers. This is commonly used with cloud-init configuration files. + +```bash +# Set a default user-data file +bl preferences set default-user-data ~/.config/bl/my-user-data.yaml + +# Also set other common defaults +bl preferences set default-region syd +bl preferences set default-size std-min +bl preferences set default-image ubuntu-24.04 + +# Verify your settings +bl preferences show server create +# Output: +# Server Creation Defaults: +# default-region = syd +# default-size = std-min +# default-image = ubuntu-24.04 +# default-user-data = /home/user/.config/bl/my-user-data.yaml +# +# With your current defaults, this command: +# bl server create --name myserver +# +# Expands to (what gets sent to the API): +# bl server create --name myserver --region syd --size std-min --image ubuntu-24.04 --user-data ~/.config/bl/my-user-data.yaml + +# Now create servers with zero arguments - user-data is automatically included +bl server create +# ↑ Uses: size=std-min, image=ubuntu-24.04, region=syd, user-data=my-user-data.yaml + +# Or specify a custom name +bl server create --name web-01 +# ↑ Custom name: web-01, uses all preferences including user-data + +# Override user-data for a specific server +bl server create --user-data ~/custom-setup.yaml +# ↑ Uses: custom-setup.yaml (override), other preferences still apply +``` + +**Tip:** You can maintain different user-data files for different environments by using contexts: + +```bash +# Development context with dev-specific user-data +bl -c dev preferences set default-user-data ~/.config/bl/user-data-dev.yaml +bl -c dev preferences set default-region syd +bl -c dev preferences set default-size std-min +bl -c dev preferences set default-image ubuntu-24.04 + +# Production context with prod-specific user-data +bl -c prod preferences set default-user-data ~/.config/bl/user-data-prod.yaml +bl -c prod preferences set default-region per +bl -c prod preferences set default-size std-2vcpu +bl -c prod preferences set default-image ubuntu-24.04 + +# Create servers in different contexts +bl -c dev server create --name app-dev-01 # Uses user-data-dev.yaml +bl -c prod server create --name app-prod-01 # Uses user-data-prod.yaml +``` + +--- + +## Output Formatting Preferences + +Customize which fields are displayed in list commands. Each resource type has its own format preference. + +### Available Format Preferences + +| Preference Key | Command | Default Fields | +|----------------|---------|----------------| +| `format-images` | `bl image list` | `id,slug,distribution,name` | +| `format-servers` | `bl server list` | `id,name,image,vcpus,memory,disk,region,networks` | +| `format-domains` | `bl domain list` | `id,name,zone_file` | +| `format-vpcs` | `bl vpc list` | `id,name,ip_range` | +| `format-load-balancers` | `bl load-balancer list` | `id,name,ip,created_at` | +| `format-ssh-keys` | `bl ssh-key list` | `id,name,fingerprint` | +| `format-actions` | `bl action list` | `id,status,type,started_at` | +| `format-sizes` | `bl size list` | `slug,description,vcpus,memory` | +| `format-regions` | `bl region list` | `slug,name,available` | +| `format-invoices` | `bl account invoice list` | `id,amount,created_at,status` | +| `format-software` | `bl software list` | `name,status,version` | + +### Setting Format Preferences + +```bash +# Set default format for image lists +bl preferences set format-images "id,full_name,distribution,created_at,status" + +# Set default format for server lists +bl preferences set format-servers "id,name,status,region,created_at" + +# Set default format for domains +bl preferences set format-domains "id,name,current_nameservers" +``` + +### Using Format Override + +You can always override your preference for a single command: + +```bash +# Override default format +bl image list --format "id,slug,name" + +# Show all available fields +bl image list --format "*" + +# Use wildcards for partial matching +bl image list --format "id,*name,distribution" +``` + +### Example Workflow + +**Without format preferences:** +```bash +# Default shows many columns (can be overwhelming) +bl server list +# Shows: id, name, image, vcpus, memory, disk, region, networks (8 columns!) + +# Must specify format every time for cleaner output +bl server list --format "id,name,status,region" +bl server list --format "id,name,status,region" # Repetitive! +``` + +**With format preferences:** +```bash +# Set your preferred columns once +bl preferences set format-servers "id,name,status,region,created_at" + +# Now lists automatically use your preferred format +bl server list +# Output: +# ID NAME STATUS REGION CREATED_AT +# 12345 web-01 active syd 2024-01-15T10:30:00Z +# 12346 db-01 active syd 2024-01-15T10:35:00Z + +# Need more detail for one specific command? Override it +bl server list --format "id,name,vcpus,memory,disk,networks" +# ↑ One-time override, doesn't change your preference + +# Reset to CLI's built-in default +bl preferences set format-servers null +``` + +--- + +## Terminal Settings + +### Terminal Width + +Control the terminal width for help text formatting: + +```bash +# Set a specific width +bl preferences set terminal-width 120 + +# Use auto-detection (default) +bl preferences set terminal-width null +``` + +This affects how help text wraps in `--help` output. + +--- + +## Configuration Priority + +Preferences provide defaults that can always be overridden. The priority chain is: + +1. **Command-line flags** (highest priority) + - Example: `--region syd` + +2. **Environment variables** + - Example: `BL_API_TOKEN=your-token` + - Format: `BL_` (uppercase, dashes become underscores) + +3. **Preferences** (config file) + - Example: `default-region = syd` in config.ini + +4. **Built-in defaults** (lowest priority) + - CLI's hardcoded defaults + +### Example + +**Setup:** +```bash +# Set preferences (all three required fields) +bl preferences set default-region syd +bl preferences set default-size std-min +bl preferences set default-image ubuntu-24.04 +``` + +**Priority in action:** +```bash +# 1. Uses preferences (nothing overrides them) +bl server create +# ↑ Creates server with: region=syd, size=std-min, image=ubuntu-24.04 (all from preferences) + +# 2. Command-line flag overrides preference +bl server create --region per +# ↑ Creates server with: region=per (CLI flag overrides preference) +# ↑ Still uses: size=std-min, image=ubuntu-24.04 (from preferences) +``` + +**Key Takeaway:** Command-line flags always win, so you can always override preferences when needed. + +--- + +## Field Reference + +This section provides a comprehensive reference for all available formatter fields. Each resource type includes: +- The config key for setting the preference +- The command that uses it +- Default fields +- All available fields with descriptions +- Human-readable formatting suggestions + +### Images + +**Config Key**: `format-images` +**Command**: `bl image list` + +**Default Fields:** +``` +id,slug,distribution,name +``` + +**Available Fields:** + +| Field | Description | +|-------|-------------| +| `id` | The ID of this image | +| `name` | If this is an operating system image, this is the name of the operating system version. If this is a backup image, this is the label of the backup if it exists, otherwise it is the UTC timestamp of the creation of the image | +| `slug` | If this is an operating system image this is a slug which may be used as an alternative to the ID as a reference | +| `distribution` | If this is an operating system image, this is the name of the distribution. If this is a backup image, this is the name of the distribution the server is using | +| `full_name` | If this is an operating system image, this is the name and version of the distribution. If this is a backup image, this is the server hostname and label of the backup if it exists, otherwise it is the server hostname and UTC timestamp of the creation of the image | +| `type` | Image type: `custom`, `snapshot`, or `backup` | +| `public` | A public image is available to all users. A private image is available only to the account that created the image | +| `regions` | The slugs of the regions where the image is available for use | +| `min_disk_size` | For a distribution image this is the minimum disk size in GB required to install the operating system. For a backup image this is the minimum total disk size in GB required to restore the backup | +| `size_gigabytes` | For a distribution image this is the disk size used in GB by the operating system on initial install. For a backup image this is the size of the compressed backup image in GB | +| `status` | Image status: `NEW`, `available`, `pending`, or `deleted` | +| `created_at` | If this is a backup image this is the date and time in ISO8601 format when the image was created | +| `description` | A description that may provide further details or warnings about the image | +| `error_message` | If the image creation failed this may provide further information | +| `min_memory_megabytes` | This is minimum memory in MB necessary to support this operating system (or the base operating system for a backup image) | +| `distribution_info` | This object may provide further information about the distribution | +| `distribution_surcharges` | If this is not null the use of this image may incur surcharges above the base cost of the server. All costs are in AU$ | +| `backup_info` | If this image is a backup, this object will provide further information | + +**Suggested Formats:** +```bash +# Compact overview +bl preferences set format-images "id,slug,distribution,name" + +# Detailed for backups +bl preferences set format-images "id,full_name,distribution,created_at,min_disk_size,status" + +# With availability info +bl preferences set format-images "id,name,distribution,regions,status,size_gigabytes" +``` + +--- + +### Servers + +**Config Key**: `format-servers` +**Command**: `bl server list` + +**Default Fields:** +``` +id,name,image,vcpus,memory,disk,region,networks +``` + +**Available Fields:** + +| Field | Description | +|-------|-------------| +| `id` | The ID of this server | +| `name` | The hostname of this server | +| `status` | Server status: `new`, `active`, `archive`, or `off` | +| `memory` | The memory in MB of this server | +| `vcpus` | The number of virtual CPUs of this server | +| `disk` | The total disk in GB of this server | +| `created_at` | The date and time in ISO8601 format of this server's initial creation | +| `region` | The region this server is allocated to | +| `image` | The base image used to create this server | +| `size` | The currently selected size for this server | +| `size_slug` | The slug of the currently selected size for this server | +| `networks` | A list of the networks of the server | +| `disks` | A list of the disks that are currently attached to the server | +| `backup_ids` | A list of the currently existing backup image IDs for this server (if any) | +| `features` | A list of the currently enabled features on this server | +| `backup_settings` | Detailed backup settings for the server | +| `failover_ips` | A list of any assigned failover IP addresses for this server | +| `host` | Summary information about the host of this server | +| `password_change_supported` | If this is true the password_reset server action can be called to change a user's password | +| `advanced_features` | The currently enabled advanced features, machine type and processor flags | +| `vpc_id` | The VPC ID that this server is allocated to. If this value is null the server is in the default (public) network for the region | +| `selected_size_options` | An object that details the selected options for the current size | +| `kernel` | The currently selected kernel for the server | +| `next_backup_window` | The details of the next scheduled backup, if any | +| `cancelled_at` | If the server has been cancelled, this is the date and time in ISO8601 format of that cancellation | +| `partner_id` | The server ID of the partner of this server, if one has been assigned | +| `permalink` | A randomly generated two-word identifier assigned to servers in regions that support this feature | +| `attached_backup` | An object that provides details of any backup image currently attached to the server | + +**Suggested Formats:** +```bash +# Compact overview +bl preferences set format-servers "id,name,status,region,created_at" + +# With resources +bl preferences set format-servers "id,name,vcpus,memory,disk,region,status" + +# Network focused +bl preferences set format-servers "id,name,region,networks,vpc_id" + +# Management view +bl preferences set format-servers "id,name,status,size_slug,image,created_at" +``` + +--- + +### Domains + +**Config Key**: `format-domains` +**Command**: `bl domain list` + +**Default Fields:** +``` +id,name,zone_file +``` + +**Available Fields:** + +| Field | Description | +|-------|-------------| +| `id` | The ID of this domain | +| `name` | The name of the domain | +| `current_nameservers` | The current authoritative name servers for this domain | +| `zone_file` | The zone file for the selected domain. If the DNS records for this domain are not managed locally this is what the zone file would be if the authority was delegated to us | +| `ttl` | The time to live for records in this domain in seconds. If the DNS records for this domain are not managed locally this will be what the TTL would be if the authority was delegated to us | + +**Suggested Formats:** +```bash +# Simple list +bl preferences set format-domains "id,name" + +# With nameservers +bl preferences set format-domains "id,name,current_nameservers" + +# Detailed +bl preferences set format-domains "id,name,current_nameservers,ttl" +``` + +--- + +### Additional Resources + +For complete field references for all resource types (VPCs, Load Balancers, SSH Keys, Actions, Sizes, Regions, Invoices, Software, etc.), the CLI provides inline help: + +```bash +# View available fields for any list command +bl image list --format "*" +bl server list --format "*" +bl domain list --format "*" +# etc. +``` + +Using `--format "*"` displays all available fields you can use in `--format` or preferences. + +--- + +## Getting Help + +### Command-Specific Help + +```bash +# See available fields for a list command +bl image list --format "*" +bl server list --format "*" + +# See all command options +bl server create --help +bl preferences --help +``` + +### View Current Preferences + +```bash +# List all set preferences +bl preferences get + +# View specific preference (value only) +bl preferences get default-region + +# View preference with help and context +bl preferences show default-region + +# View all preferences grouped by command +bl preferences show server create +bl preferences show terminal +``` + +### Configuration File Location + +Preferences are stored in your system's config directory: + +- **Linux/Mac**: `~/.config/binarylane/config.ini` +- **Windows**: `%APPDATA%\binarylane\config.ini` + +You can manually edit this file if needed, though using `bl preferences set` is recommended. + +--- + +## Tips and Best Practices + +### 1. Start with Server Defaults + +If you frequently create servers with similar configurations, set defaults first: + +```bash +bl preferences set default-region syd +bl preferences set default-size std-min +bl preferences set default-image ubuntu-24.04 +``` + +### 2. Use User Data Files for Reproducibility + +Store your user-data files in version control and reference them in preferences: + +```bash +bl preferences set default-user-data ~/projects/infra/user-data/dev-server.yaml +``` + +### 3. Different Contexts for Different Environments + +Use contexts to maintain separate preferences for different environments: + +```bash +# Development context +bl -c dev preferences set default-region syd +bl -c dev preferences set default-size std-min +bl -c dev preferences set default-image ubuntu-24.04 + +# Production context +bl -c prod preferences set default-region per +bl -c prod preferences set default-size std-4vcpu +bl -c prod preferences set default-image ubuntu-24.04 + +# Use them +bl -c dev server create --name dev-app-01 +bl -c prod server create --name prod-app-01 +``` + +### 4. Customize Output for Your Workflow + +Set format preferences that match your needs: + +```bash +# For quick scans +bl preferences set format-servers "id,name,status" + +# For detailed management +bl preferences set format-servers "id,name,status,vcpus,memory,region,created_at" +``` + +### 5. Use preferences show for Discovery + +When you can't remember what preferences are set for a workflow: + +```bash +bl preferences show server create +bl preferences show terminal +``` + +--- + +## Security Notes + +### API Tokens + +While `api-token` is a valid configuration option, be aware that: +- Setting it via command line exposes it in shell history +- Use the interactive `bl configure` command for token management +- Environment variables are also supported (e.g., for CI/CD workflows) + +```bash +# Recommended: Interactive (no shell history) +bl configure +``` + +### Sensitive Defaults + +Be cautious with `default-password` - using SSH keys is more secure: + +```bash +# Better: Use SSH keys +bl preferences set default-ssh-keys "aa:bb:cc:dd:..." + +# Avoid: Storing passwords +bl preferences set default-password "..." # Shows warning +``` + +--- + +## Complete Example Workflow + +Here's a complete, realistic workflow for setting up a development environment: + +### Step 1: Initial Setup (One Time) + +```bash +# Set server creation defaults +bl preferences set default-region syd +bl preferences set default-size std-min +bl preferences set default-image ubuntu-24.04 +bl preferences set default-user-data ~/.config/bl/my-user-data.yaml + +# Set output formatting preferences +bl preferences set format-servers "id,name,status,region,created_at" +bl preferences set format-images "id,slug,distribution,status" + +# Set terminal preferences +bl preferences set terminal-width 120 +``` + +### Step 2: Verify Your Configuration + +```bash +# Check server creation preferences +bl preferences show server create +# Output: +# Server Creation Defaults: +# default-region = syd +# default-size = std-min +# default-image = ubuntu-24.04 +# default-user-data = /home/user/.config/bl/my-user-data.yaml +# +# With your current defaults, this command: +# bl server create --name myserver +# +# Expands to (what gets sent to the API): +# bl server create --name myserver --region syd --size std-min --image ubuntu-24.04 --user-data ~/.config/bl/my-user-data.yaml + +# Check Terminal Settings +bl preferences show terminal +# Output: +# Terminal Settings: +# terminal-width = 120 +``` + +### Step 3: Create Servers (Simplified) + +**Before preferences:** +```bash +bl server create --name web-01 --size std-min --image ubuntu-24.04 --region syd --user-data ~/.config/bl/my-user-data.yaml +bl server create --name web-02 --size std-min --image ubuntu-24.04 --region syd --user-data ~/.config/bl/my-user-data.yaml +bl server create --name api-01 --size std-min --image ubuntu-24.04 --region syd --user-data ~/.config/bl/my-user-data.yaml +``` + +**After preferences:** +```bash +# Create servers with zero arguments +bl server create +# ↑ Uses: size=std-min, image=ubuntu-24.04, region=syd, user-data=my-user-data.yaml + +bl server create +# ↑ Another server with same config + +bl server create +# ↑ And another! + +# Specify custom names when you need them +bl server create --name web-01 +bl server create --name api-01 + +# Need a bigger server? Just override the size +bl server create --name db-01 --size std-4vcpu +# ↑ Override: size=std-4vcpu +# ↑ Uses preferences: image=ubuntu-24.04, region=syd, user-data=my-user-data.yaml + +# Need a different OS? Just override the image +bl server create --image debian-12 +# ↑ Override: image=debian-12 +# ↑ Uses preferences: size=std-min, region=syd, user-data=my-user-data.yaml +``` + +### Step 4: View Servers with Your Format + +```bash +# Lists automatically use your preferred format +bl server list +# Output (only shows your preferred columns): +# ID NAME STATUS REGION CREATED_AT +# 12345 web-01 active syd 2024-01-15T10:30:00Z +# 12346 web-02 active syd 2024-01-15T10:32:00Z +# 12347 api-01 active syd 2024-01-15T10:35:00Z +# 12348 db-01 active syd 2024-01-15T10:40:00Z + +# Need more details for one command? Override the format +bl server list --format "id,name,vcpus,memory,disk,networks" +# ↑ Shows additional columns just for this one command +``` + +### Summary + +With preferences configured, the minimal server create command is: +```bash +bl server create +``` + +All required fields (region, size, image) are populated from preferences. Optional fields like user-data are also applied if configured. + +--- + +**For More Information:** +- Main documentation: [README.md](README.md) +- CLI source: [https://github.com/binarylane/binarylane-cli](https://github.com/binarylane/binarylane-cli) +- API documentation: [https://api.binarylane.com.au/reference](https://api.binarylane.com.au/reference) diff --git a/README.md b/README.md index fc6e8bbe..78309ff9 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Available Commands: domain Access domain commands image Access image commands load-balancer Access load-balancer commands + preferences Manage user preferences region Access region commands server Access server commands size Access size commands @@ -79,7 +80,7 @@ $ bl usage: bl [OPTIONS] COMMAND -bl is a command-line interface for the binaryLane API +bl is a command-line interface for the BinaryLane API Options: --help Display available commands and descriptions @@ -87,10 +88,11 @@ Options: Available Commands: account Access account commands action Access action commands - configure Configure access to binaryLane API + configure Configure access to BinaryLane API domain Access domain commands image Access image commands load-balancer Access load-balancer commands + preferences Manage user preferences region Access region commands server Access server commands size Access size commands @@ -160,7 +162,7 @@ Server creation is provided by the `bl server create` command. Use `--help` to view all arguments and parameters: ``` -$ bl server list --help +$ bl server create --help usage: bl server create [OPTIONS] --size SIZE --image IMAGE --region REGION [PARAMETERS] Create a new server. @@ -353,6 +355,41 @@ soon as the BinaryLane API accepts the requested command. To do so, include the $ bl server create --size std-min --image ubuntu-22.04-lts --region syd --async ``` +## Preferences + +Store commonly used options to streamline your workflow: + +```bash +# Set default region, size, and image for server creation +bl preferences set default-region syd +bl preferences set default-size std-min +bl preferences set default-image ubuntu-24.04 + +# Now create servers more quickly +bl server create --name myserver +# Region, size, and image will use your preferences +``` + +**Terminal Settings** + +- `terminal-width` - Set terminal width for help text (numeric value, or null for auto-detection) + +Example: +```bash +bl preferences set terminal-width 120 +``` + +### Priority + +Preferences provide defaults that can always be overridden: + +1. Command-line arguments (highest priority) +2. Environment variables +3. Preferences (`bl preferences set`) +4. Built-in defaults (lowest priority) + +See [PREFERENCES.md](PREFERENCES.md) for complete documentation. + ### Configuration file `bl configure` creates a configuration file containing the API token, and reads From 454ae33b730d1bc91e475d7cdd32057b53b41a1c Mon Sep 17 00:00:00 2001 From: Freewheelin Date: Sat, 11 Oct 2025 12:37:02 +1000 Subject: [PATCH 8/8] fix: properly inject boolean preferences as CLI flags - Fix preferences_show.py to display boolean flags in --flag/--no-flag format - Fix server_create.py to inject boolean preferences (backups, port-blocking) as CLI arguments instead of directly modifying request body - Ensures default-port-blocking=false actually disables port blocking on new servers --- .../console/commands/preferences_show.py | 10 ++++++++-- .../console/commands/server_create.py | 17 ++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/binarylane/console/commands/preferences_show.py b/src/binarylane/console/commands/preferences_show.py index 23774c20..e2a11934 100644 --- a/src/binarylane/console/commands/preferences_show.py +++ b/src/binarylane/console/commands/preferences_show.py @@ -348,9 +348,15 @@ def _show_example_command(self) -> None: for option, flag in preference_map: value = self._context.get_option(option) if value is not None: - # Handle boolean values + # Handle boolean values - convert to --flag or --no-flag format if option in (OptionName.DEFAULT_BACKUPS, OptionName.DEFAULT_PORT_BLOCKING): - args.append(f"{flag} {value}") + bool_value = value.lower() in ("true", "1", "yes", "on") + if bool_value: + args.append(flag) + else: + # Convert --backups to --no-backups, --port-blocking to --no-port-blocking + no_flag = flag.replace("--", "--no-", 1) + args.append(no_flag) else: args.append(f"{flag} {value}") diff --git a/src/binarylane/console/commands/server_create.py b/src/binarylane/console/commands/server_create.py index ad85d669..ac43e547 100644 --- a/src/binarylane/console/commands/server_create.py +++ b/src/binarylane/console/commands/server_create.py @@ -37,6 +37,15 @@ def run(self, args: List[str]) -> None: if "--region" not in modified_args and ctx.default_region: modified_args.extend(["--region", ctx.default_region]) + # Inject boolean flags if not already provided + if "--backups" not in modified_args and "--no-backups" not in modified_args: + if ctx.default_backups is not None: + modified_args.append("--backups" if ctx.default_backups else "--no-backups") + + if "--port-blocking" not in modified_args and "--no-port-blocking" not in modified_args: + if ctx.default_port_blocking is not None: + modified_args.append("--port-blocking" if ctx.default_port_blocking else "--no-port-blocking") + super().run(modified_args) def request( @@ -50,9 +59,7 @@ def request( ctx = self._context # Optional parameters with config defaults - if isinstance(getattr(body, "backups", Unset), type(Unset)): - if ctx.default_backups is not None: - body.backups = ctx.default_backups + # Note: backups and port_blocking are now handled as CLI args in run() if isinstance(getattr(body, "ssh_keys", Unset), type(Unset)): if ctx.default_ssh_keys: @@ -62,10 +69,6 @@ def request( if ctx.default_user_data: body.user_data = ctx.default_user_data - if isinstance(getattr(body, "port_blocking", Unset), type(Unset)): - if ctx.default_port_blocking is not None: - body.port_blocking = ctx.default_port_blocking - if isinstance(getattr(body, "password", Unset), type(Unset)): if ctx.default_password: body.password = ctx.default_password