diff --git a/README.rst b/README.rst index 5974baa..ce22b85 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,7 @@ View the information about Pis in your account from the command line: .. code-block:: console - $ hostedpi list + $ hostedpi ls pi123 pi234 pi345 @@ -56,23 +56,22 @@ View the information about Pis in your account from the command line: └───────┴───────┴────────┴───────────┘ $ hostedpi table pi345 --full - ┏━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Name ┃ Model ┃ Memory ┃ CPU Speed ┃ NIC Speed ┃ Disk size ┃ Status ┃ Initialised keys ┃ IPv4 SSH port ┃ - ┡━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ pi345 │ 4B │ 8 GB │ 2.0 GHz │ 1 Gbps │ 50 GB │ Powered on │ Yes │ 5387 │ - └───────┴───────┴────────┴───────────┴───────────┴───────────┴────────────┴──────────────────┴───────────────┘ + ┏━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓ + ┃ Name ┃ Model ┃ Memory ┃ CPU Speed ┃ NIC Speed ┃ Disk size ┃ Status ┃ Initialised keys ┃ IPv4 SSH port ┃ IPv6 Address ┃ + ┡━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩ + │ pi345 │ 4B │ 8 GB │ 2.0 GHz │ 1 Gbps │ 50 GB │ Powered on │ Yes │ 5381 │ 2a00:1098:8:17d::1 │ + └───────┴───────┴────────┴───────────┴───────────┴───────────┴────────────┴──────────────────┴───────────────┴────────────────────┘ Provision a new Pi with your public key and SSH into it: .. code-block:: console $ hostedpi create mypi --model 3 --ssh-key-path ~/.ssh/id_rsa.pub --wait - Server provisioned - ┏━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ - ┃ Name ┃ Model ┃ Memory ┃ CPU Speed ┃ - ┡━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ - │ mypi | 3 │ 1 GB │ 1.2 GHz │ - └──────┴───────┴────────┴───────────┘ + ┏━━━━━━┳━━━━━━━━━━━━━┓ + ┃ Name ┃ Status ┃ + ┡━━━━━━╇━━━━━━━━━━━━━┩ + │ mypi │ Provisioned │ + └──────┴─────────────┘ $ hostedpi ssh command mypi ssh -p 5063 root@ssh.mypi.hostedpi.com $ ssh -p 5063 root@ssh.mypi.hostedpi.com diff --git a/docs/changelog.rst b/docs/changelog.rst index eabcdb8..578a3d0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,12 +8,31 @@ Changelog Once the library reaches v1.0, it will be considered stable. Please consider giving feedback to help stabilise the API. +Release 0.4.6 (2025-10-18) +========================== + +- Allowed ``user`` to be ``None`` in :meth:`~hostedpi.pi.Pi.get_ipv4_ssh_command()`, + :meth:`~hostedpi.pi.Pi.get_ipv6_ssh_command()`, :meth:`~hostedpi.pi.Pi.get_ipv4_ssh_config()` and + :meth:`~hostedpi.pi.Pi.get_ipv6_ssh_config()` methods to :class:`~hostedpi.pi.Pi` +- Allowed ``user`` to be ``None`` in :meth:`~hostedpi.picloud.PiCloud.get_ipv4_ssh_config()` and + :meth:`~hostedpi.picloud.PiCloud.get_ipv6_ssh_config()` method to + :class:`~hostedpi.picloud.PiCloud` +- Removed ``--full`` option from :doc:`cli/create` command, and simplified the output to a single + table + +Release 0.4.5 (2025-10-11) +========================== + +- Fixup release + Release 0.4.4 (2025-10-10) ========================== -- Added :meth:`~hostedpi.pi.Pi.get_ipv6_ssh_command()` and +- Added :meth:`~hostedpi.pi.Pi.get_ipv4_ssh_command()`, + :meth:`~hostedpi.pi.Pi.get_ipv6_ssh_command()`, :meth:`~hostedpi.pi.Pi.get_ipv4_ssh_config()` and :meth:`~hostedpi.pi.Pi.get_ipv6_ssh_config()` methods to :class:`~hostedpi.pi.Pi` -- Added :meth:`~hostedpi.picloud.PiCloud.get_ipv6_ssh_config()` method to +- Added :meth:`~hostedpi.picloud.PiCloud.get_ipv4_ssh_config()` and + :meth:`~hostedpi.picloud.PiCloud.get_ipv6_ssh_config()` method to :class:`~hostedpi.picloud.PiCloud` Release 0.4.3 (2025-07-25) diff --git a/docs/cli/create.rst b/docs/cli/create.rst index 8e222cf..ead6837 100644 --- a/docs/cli/create.rst +++ b/docs/cli/create.rst @@ -17,8 +17,8 @@ Arguments Names of the Raspberry Pi servers to provision - If no names are provided, a generated name will be generated. Use in combination with - :option:`--number` to create multiple servers with generated names. + If no names are provided, a name will be generated. Use in combination with :option:`--number` + to create multiple servers with generated names. Options ======= @@ -58,9 +58,6 @@ Options Wait and poll for status to be available before returning - Supply with :option:`--full` to show the full table of Raspberry Pi server info when the server - is provisioned - .. option:: --ssh-key-path [path] Path to the SSH key to install on the Raspberry Pi server @@ -77,12 +74,6 @@ Options Can be provided multiple times -.. option:: --full - - Show full table of Raspberry Pi server info when the server is provisioned - - Can only provided along with :option:`--wait` - .. option:: --help Show this message and exit @@ -95,30 +86,23 @@ Provision a new Pi 3 using the default Pi 3 spec, and wait for it to be provisio .. code-block:: console $ hostedpi create mypi --model 3 --wait - Server provisioned - ┏━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ - ┃ Name ┃ Model ┃ Memory ┃ CPU Speed ┃ - ┡━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ - │ mypi │ 3 │ 1 GB │ 1.2 GHz │ - └───────┴───────┴────────┴───────────┘ + ┏━━━━━━┳━━━━━━━━━━━━━┓ + ┃ Name ┃ Status ┃ + ┡━━━━━━╇━━━━━━━━━━━━━┩ + │ mypi │ Provisioned │ + └──────┴─────────────┘ Provision two new Pi 4 servers with generated names, using the default Pi 4 spec: .. code-block:: console $ hostedpi create --model 4 --number 2 --wait - Server provisioned - ┏━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ - ┃ Name ┃ Model ┃ Memory ┃ CPU Speed ┃ - ┡━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ - │ c8046pxjf │ 4 │ 4 GB │ 1.5 GHz │ - └───────────┴───────┴────────┴───────────┘ - Server provisioned - ┏━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ - ┃ Name ┃ Model ┃ Memory ┃ CPU Speed ┃ - ┡━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ - │ c8046pg5e │ 4 │ 4 GB │ 1.5 GHz │ - └───────────┴───────┴────────┴───────────┘ + ┏━━━━━━━━━━━┳━━━━━━━━━━━━━┓ + ┃ Name ┃ Status ┃ + ┡━━━━━━━━━━━╇━━━━━━━━━━━━━┩ + │ c8046pxjf │ Provisioned │ + │ c8046pg5e │ Provisioned │ + └───────────┴─────────────┘ .. warning:: If no :option:`names` are provided, and :option:`--wait` is not provided, the command will return @@ -129,12 +113,11 @@ Provision a new Pi 4 using custom settings: .. code-block:: console $ hostedpi create mypi4 --model 4 --memory 8192 --cpu-speed 2000 --disk 60 --os-image rpi-jammy-arm64 --ssh-key-path ~/.ssh/id_rsa.pub --wait - Server provisioned - ┏━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ - ┃ Name ┃ Model ┃ Memory ┃ CPU Speed ┃ - ┡━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ - │ mypi4 │ 4 │ 8 GB │ 2.0 GHz │ - └───────┴───────┴────────┴───────────┘ + ┏━━━━━━━┳━━━━━━━━━━━━━┓ + ┃ Name ┃ Status ┃ + ┡━━━━━━━╇━━━━━━━━━━━━━┩ + │ mypi4 │ Provisioned │ + └───────┴─────────────┘ .. note:: diff --git a/docs/cli/ssh/command.rst b/docs/cli/ssh/command.rst index 32cb26d..b1f9854 100644 --- a/docs/cli/ssh/command.rst +++ b/docs/cli/ssh/command.rst @@ -32,6 +32,10 @@ Options The username to use when connecting +.. option:: --no-user + + Don't include a username in SSH command + .. option:: --help Show this message and exit @@ -39,7 +43,7 @@ Options Usage ===== -Output the IPv4 SSH command for a Pi: +Output the default IPv4 SSH command for a Pi: .. code-block:: console @@ -53,6 +57,13 @@ Output the IPv4 SSH command for a Pi, with a custom username: $ hostedpi ssh command mypi --username pi ssh -p 5091 pi@ssh.mypi.hostedpi.com +Output the IPv4 SSH command for a Pi, with no specified username: + +.. code-block:: console + + $ hostedpi ssh command mypi --no-user + ssh -p 5091 ssh.mypi.hostedpi.com + Output the IPv6 SSH command for a Pi: .. code-block:: console @@ -60,6 +71,10 @@ Output the IPv6 SSH command for a Pi: $ hostedpi ssh command mypi --ipv6 ssh root@mypi.hostedpi.com +.. warning:: + + You will need IPv6 connectivity to connect using the IPv6 address. + Output the IPv6 SSH command for a Pi, with a custom username and numeric address: .. code-block:: console diff --git a/docs/cli/ssh/config.rst b/docs/cli/ssh/config.rst index e921cb4..d069858 100644 --- a/docs/cli/ssh/config.rst +++ b/docs/cli/ssh/config.rst @@ -36,6 +36,10 @@ Options The username to use when connecting +.. option:: --no-user + + Don't include a username in SSH command + .. option:: --help Show this message and exit @@ -71,6 +75,10 @@ Output the IPv6 SSH config for a Pi: user root hostname mypi.hostedpi.com +.. warning:: + + You will need IPv6 connectivity to connect using the IPv6 address. + Output the IPv6 SSH config for a Pi, using a custom username and numeric address: .. code-block:: console @@ -80,6 +88,14 @@ Output the IPv6 SSH config for a Pi, using a custom username and numeric address user pi hostname 2a00:1098:8:14b::1 +Output the IPv6 SSH config for a Pi, using the numeric address and specifying no username: + +.. code-block:: console + + $ hostedpi ssh config mypi --ipv6 --numeric --no-user + Host mypi + hostname 2a00:1098:8:14b::1 + Output the IPv4 SSH config for multiple Pis: .. code-block:: console diff --git a/docs/conf.py b/docs/conf.py index 7443b08..01a3e32 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,7 +7,7 @@ import sphinx_rtd_theme -hostedpi_version = "0.4.5" +hostedpi_version = "0.4.6" # -- General configuration ------------------------------------------------ diff --git a/docs/index.rst b/docs/index.rst index bfd5a0e..cbbcbf4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -34,7 +34,7 @@ View the information about Pis in your account from the command line: .. code-block:: console - $ hostedpi list + $ hostedpi ls pi123 pi234 pi345 @@ -56,23 +56,22 @@ View the information about Pis in your account from the command line: └───────┴───────┴────────┴───────────┘ $ hostedpi table pi345 --full - ┏━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ - ┃ Name ┃ Model ┃ Memory ┃ CPU Speed ┃ NIC Speed ┃ Disk size ┃ Status ┃ Initialised keys ┃ IPv4 SSH port ┃ - ┡━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ - │ pi345 │ 4B │ 8 GB │ 2.0 GHz │ 1 Gbps │ 50 GB │ Powered on │ Yes │ 5387 │ - └───────┴───────┴────────┴───────────┴───────────┴───────────┴────────────┴──────────────────┴───────────────┘ + ┏━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓ + ┃ Name ┃ Model ┃ Memory ┃ CPU Speed ┃ NIC Speed ┃ Disk size ┃ Status ┃ Initialised keys ┃ IPv4 SSH port ┃ IPv6 Address ┃ + ┡━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩ + │ pi345 │ 4B │ 8 GB │ 2.0 GHz │ 1 Gbps │ 50 GB │ Powered on │ Yes │ 5381 │ 2a00:1098:8:17d::1 │ + └───────┴───────┴────────┴───────────┴───────────┴───────────┴────────────┴──────────────────┴───────────────┴────────────────────┘ Provision a new Pi with your public key and SSH into it: .. code-block:: console $ hostedpi create mypi --model 3 --ssh-key-path ~/.ssh/id_rsa.pub --wait - Server provisioned - ┏━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ - ┃ Name ┃ Model ┃ Memory ┃ CPU Speed ┃ - ┡━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ - │ mypi | 3 │ 1 GB │ 1.2 GHz │ - └──────┴───────┴────────┴───────────┘ + ┏━━━━━━┳━━━━━━━━━━━━━┓ + ┃ Name ┃ Status ┃ + ┡━━━━━━╇━━━━━━━━━━━━━┩ + │ mypi │ Provisioned │ + └──────┴─────────────┘ $ hostedpi ssh command mypi ssh -p 5063 root@ssh.mypi.hostedpi.com $ ssh -p 5063 root@ssh.mypi.hostedpi.com diff --git a/hostedpi/cli/__init__.py b/hostedpi/cli/__init__.py index e92a792..02149a8 100644 --- a/hostedpi/cli/__init__.py +++ b/hostedpi/cli/__init__.py @@ -89,7 +89,6 @@ def do_create( ssh_key_path: options.ssh_key_path = None, ssh_import_github: options.ssh_import_github = None, ssh_import_launchpad: options.ssh_import_launchpad = None, - full: options.full_table = False, ): """ Provision one or more new Raspberry Pi servers @@ -99,9 +98,6 @@ def do_create( raise Exit(1) if not names and not number: number = 1 - if full and not wait: - utils.print_error("You can't use --full without --wait") - raise Exit(1) if ssh_key_path is not None: if not ssh_key_path.exists(): utils.print_error(f"SSH key file not found: {ssh_key_path}") @@ -109,44 +105,46 @@ def do_create( ssh_import_gh_set = set(ssh_import_github) if ssh_import_github is not None else None ssh_import_lp_set = set(ssh_import_launchpad) if ssh_import_launchpad is not None else None - if names: - for name in names: - try: - utils.create_pi( - name=name, - model=model, - disk=disk, - memory_gb=memory, - cpu_speed=cpu_speed, - os_image=os_image, - wait=wait, - ssh_key_path=ssh_key_path, - github_usernames=ssh_import_gh_set, - launchpad_usernames=ssh_import_lp_set, - full=full, - ) - except HostedPiException as exc: - utils.print_exc(exc) - continue - - if number: - for n in range(number): - try: - utils.create_pi( - model=model, - disk=disk, - memory_gb=memory, - cpu_speed=cpu_speed, - os_image=os_image, - wait=wait, - ssh_key_path=ssh_key_path, - github_usernames=ssh_import_gh_set, - launchpad_usernames=ssh_import_lp_set, - full=full, - ) - except HostedPiException as exc: - utils.print_exc(exc) - continue + table = utils.make_table("Name", "Status") + with Live(table, console=console, refresh_per_second=4): + if names: + for name in names: + try: + utils.create_pi( + name=name, + model=model, + disk=disk, + memory_gb=memory, + cpu_speed=cpu_speed, + os_image=os_image, + wait=wait, + ssh_key_path=ssh_key_path, + github_usernames=ssh_import_gh_set, + launchpad_usernames=ssh_import_lp_set, + ) + table.add_row(name, "Provisioned") + except HostedPiException as exc: + table.add_row(name, f"Error: {exc}") + continue + + if number: + for n in range(number): + try: + pi = utils.create_pi( + model=model, + disk=disk, + memory_gb=memory, + cpu_speed=cpu_speed, + os_image=os_image, + wait=wait, + ssh_key_path=ssh_key_path, + github_usernames=ssh_import_gh_set, + launchpad_usernames=ssh_import_lp_set, + ) + table.add_row(pi.name, "Provisioned") + except HostedPiException as exc: + table.add_row(str(n + 1), f"Error: {exc}") + continue @app.command("status") diff --git a/hostedpi/cli/options.py b/hostedpi/cli/options.py index 0c79c3c..0b2c4b8 100644 --- a/hostedpi/cli/options.py +++ b/hostedpi/cli/options.py @@ -25,6 +25,7 @@ ipv6 = Annotated[bool, Option(help="Use the IPv6 connection method")] numeric = Annotated[bool, Option("--numeric", "-n", help="Use numeric IPv6 address in SSH command")] user = Annotated[str, Option("--username", "-u", help="Username for SSH connection")] +no_user = Annotated[bool, Option("--no-user", help="Don't include a username in SSH command")] yes = Annotated[bool, Option("--yes", "-y", help="Proceed without confirmation")] number = Annotated[Union[int, None], Option(help="Number of Raspberry Pi servers to create", min=1)] full_table = Annotated[bool, Option(help="Show full table of Raspberry Pi server info")] diff --git a/hostedpi/cli/ssh.py b/hostedpi/cli/ssh.py index aa114c5..2a99e8b 100644 --- a/hostedpi/cli/ssh.py +++ b/hostedpi/cli/ssh.py @@ -16,6 +16,7 @@ def do_command( ipv6: options.ipv6 = False, numeric: options.numeric = False, user: options.user = "root", + no_user: options.no_user = False, ): """ Get the SSH command to connect to a Raspberry Pi server @@ -24,6 +25,9 @@ def do_command( print_error("--numeric is only supported with --ipv6") raise Exit(1) + if no_user: + user = None + pi = get_pi(name) if pi is None: print_error(f"Pi '{name}' not found") @@ -45,6 +49,7 @@ def do_config( ipv6: options.ipv6 = False, numeric: options.numeric = False, user: options.user = "root", + no_user: options.no_user = False, ): """ Get the SSH config to connect to one or more Raspberry Pi servers @@ -53,6 +58,9 @@ def do_config( print_error("--numeric is only supported with --ipv6") raise Exit(1) + if no_user: + user = None + pis = get_pis(names, filter) for pi in pis: try: diff --git a/hostedpi/cli/utils.py b/hostedpi/cli/utils.py index 52f44d3..f00372d 100644 --- a/hostedpi/cli/utils.py +++ b/hostedpi/cli/utils.py @@ -120,9 +120,8 @@ def create_pi( ssh_key_path: Union[Path, None], github_usernames: Union[set[str], None], launchpad_usernames: Union[set[str], None], - full: bool, name: Union[str, None] = None, -): +) -> Pi: data = { "disk": disk, "memory_gb": memory_gb, @@ -143,16 +142,7 @@ def create_pi( raise HostedPiValidationError(f"Invalid server spec: {exc}") from exc cloud = get_picloud() - pi = cloud.create_pi(name=name, spec=spec, ssh_keys=ssh_keys, wait=wait) - - if full: - print_success("Server provisioned") - full_pis_table([pi]) - elif wait: - print_success("Server provisioned") - short_pis_table([pi]) - else: - print_success("Server provision request accepted") + return cloud.create_pi(name=name, spec=spec, ssh_keys=ssh_keys, wait=wait) def print_exc(exc: Exception): diff --git a/hostedpi/pi.py b/hostedpi/pi.py index a9110b3..d04e92c 100644 --- a/hostedpi/pi.py +++ b/hostedpi/pi.py @@ -497,36 +497,44 @@ def cancel(self): self._cancelled = True - def get_ipv4_ssh_command(self, user: str = "root") -> str: + def get_ipv4_ssh_command(self, user: Union[str, None] = "root") -> str: """ Construct an SSH command required to connect to the Pi using SSH over IPv4 """ + if user is None: + return f"ssh -p {self.ipv4_ssh_port} {self.ipv4_ssh_hostname}" return f"ssh -p {self.ipv4_ssh_port} {user}@{self.ipv4_ssh_hostname}" - def get_ipv6_ssh_command(self, *, user: str = "root", numeric: bool = False) -> str: + def get_ipv6_ssh_command( + self, *, user: Union[str, None] = "root", numeric: bool = False + ) -> str: """ Construct an SSH command required to connect to the Pi using SSH over IPv6 """ if numeric: + if user is None: + return f"ssh [{self.ipv6_address.compressed}]" return f"ssh {user}@[{self.ipv6_address.compressed}]" else: + if user is None: + return f"ssh {self.ipv6_ssh_hostname}" return f"ssh {user}@{self.ipv6_ssh_hostname}" - def get_ipv4_ssh_config(self, user: str = "root") -> str: + def get_ipv4_ssh_config(self, user: Union[str, None] = "root") -> str: """ Construct a string containing the IPv4 SSH config for the Pi. The contents could be added to an SSH config file for easy access to the Pi. """ - return f"""Host {self.name} - user {user} - port {self.ipv4_ssh_port} - hostname {self.ipv4_ssh_hostname} - """.strip() + host_line = f"Host {self.name}\n" + user_line = f" user {user}\n" if user else "" + port_line = f" port {self.ipv4_ssh_port}\n" + hostname_line = f" hostname {self.ipv4_ssh_hostname}" + return host_line + user_line + port_line + hostname_line def get_ipv6_ssh_config( self, *, - user: str = "root", + user: Union[str, None] = "root", numeric: bool = False, ) -> str: """ @@ -534,10 +542,10 @@ def get_ipv6_ssh_config( SSH config file for easy access to the Pi. """ hostname = self.ipv6_address.compressed if numeric else self.ipv6_ssh_hostname - return f"""Host {self.name} - user {user} - hostname {hostname} - """.strip() + host_line = f"Host {self.name}\n" + user_line = f" user {user}\n" if user else "" + hostname_line = f" hostname {hostname}" + return host_line + user_line + hostname_line def add_ssh_keys(self, ssh_keys: SSHKeySources) -> set[str]: """ diff --git a/hostedpi/picloud.py b/hostedpi/picloud.py index 5263dd9..5d48a43 100644 --- a/hostedpi/picloud.py +++ b/hostedpi/picloud.py @@ -109,14 +109,14 @@ def ipv6_ssh_config(self) -> str: """ return self.get_ipv6_ssh_config() - def get_ipv4_ssh_config(self, user: str = "root") -> str: + def get_ipv4_ssh_config(self, user: Union[str, None] = "root") -> str: """ Construct a string containing the IPv4 SSH config for all Pis within the account. The contents could be added to an SSH config file for easy access to the Pis in the account. """ return "\n".join(pi.get_ipv4_ssh_config(user=user) for pi in self.pis.values()) - def get_ipv6_ssh_config(self, user: str = "root", numeric: bool = False) -> str: + def get_ipv6_ssh_config(self, user: Union[str, None] = "root", numeric: bool = False) -> str: """ Construct a string containing the IPv6 SSH config for all Pis within the account. The contents could be added to an SSH config file for easy access to the Pis in the account. diff --git a/pyproject.toml b/pyproject.toml index 4898cac..0ecd539 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "hostedpi" -version = "0.4.5" +version = "0.4.6" description = "Python interface to the Mythic Beasts Hosted Pi API" authors = [ {name = "Ben Nuttall", email = "ben@bennuttall.com"} diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 2474101..6c0c244 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -48,6 +48,13 @@ def mock_get_pis_one(mock_pi): yield get_pis +@pytest.fixture(autouse=True) +def mock_create_pi(mock_pi): + with patch("hostedpi.cli.utils.create_pi") as create_pi: + create_pi.return_value = mock_pi + yield create_pi + + @pytest.fixture() def usage_text() -> str: return "Usage: hostedpi" diff --git a/tests/test_pi.py b/tests/test_pi.py index fac92fa..94337b1 100644 --- a/tests/test_pi.py +++ b/tests/test_pi.py @@ -714,6 +714,7 @@ def test_pi_get_ipv4_ssh_command(pi_name, pi_info_basic, auth, pi_info_response) auth._api_session.get.return_value = pi_info_response assert pi.get_ipv4_ssh_command() == "ssh -p 5100 root@ssh.test-pi.hostedpi.com" assert pi.get_ipv4_ssh_command(user="pi") == "ssh -p 5100 pi@ssh.test-pi.hostedpi.com" + assert pi.get_ipv4_ssh_command(user=None) == "ssh -p 5100 ssh.test-pi.hostedpi.com" def test_pi_get_ipv6_ssh_command(pi_name, pi_info_basic, auth, pi_info_response): @@ -721,8 +722,10 @@ def test_pi_get_ipv6_ssh_command(pi_name, pi_info_basic, auth, pi_info_response) auth._api_session.get.return_value = pi_info_response assert pi.get_ipv6_ssh_command() == "ssh root@test-pi.hostedpi.com" assert pi.get_ipv6_ssh_command(user="pi") == "ssh pi@test-pi.hostedpi.com" + assert pi.get_ipv6_ssh_command(user=None) == "ssh test-pi.hostedpi.com" assert pi.get_ipv6_ssh_command(numeric=True) == "ssh root@[2a00:1098:8:64::1]" assert pi.get_ipv6_ssh_command(user="pi", numeric=True) == "ssh pi@[2a00:1098:8:64::1]" + assert pi.get_ipv6_ssh_command(user=None, numeric=True) == "ssh [2a00:1098:8:64::1]" def test_pi_get_ipv4_ssh_config(pi_name, pi_info_basic, auth, pi_info_response): @@ -741,6 +744,12 @@ def test_pi_get_ipv4_ssh_config(pi_name, pi_info_basic, auth, pi_info_response): assert "port 5100" in config assert "hostname ssh.test-pi.hostedpi.com" in config + config = pi.get_ipv4_ssh_config(user=None) + assert "Host test-pi" in config + assert "user" not in config + assert "port 5100" in config + assert "hostname ssh.test-pi.hostedpi.com" in config + def test_pi_get_ipv6_ssh_config(pi_name, pi_info_basic, auth, pi_info_response): pi = Pi(name=pi_name, info=pi_info_basic, auth=auth) @@ -756,6 +765,11 @@ def test_pi_get_ipv6_ssh_config(pi_name, pi_info_basic, auth, pi_info_response): assert "user pi" in config assert "hostname test-pi.hostedpi.com" in config + config = pi.get_ipv6_ssh_config(user=None) + assert "Host test-pi" in config + assert "user" not in config + assert "hostname test-pi.hostedpi.com" in config + config = pi.get_ipv6_ssh_config(numeric=True) assert "Host test-pi" in config assert "user root" in config diff --git a/tests/test_picloud.py b/tests/test_picloud.py index 8986e96..9853dc8 100644 --- a/tests/test_picloud.py +++ b/tests/test_picloud.py @@ -523,8 +523,8 @@ def test_get_ipv4_ssh_config(auth, pis_response, pi_info_response, pi_info_respo assert auth._api_session.get.call_args_list[0][0][0] == cloud._api_url + "servers" assert auth._api_session.get.call_args_list[1][0][0] == cloud._api_url + "servers/pi1" assert auth._api_session.get.call_args_list[2][0][0] == cloud._api_url + "servers/pi2" - assert ipv4_config.count("\n") == 7 lines = ipv4_config.splitlines() + assert len(lines) == 8 assert lines[0] == "Host pi1" assert lines[1] == " user root" assert lines[2] == " port 5100" @@ -535,6 +535,52 @@ def test_get_ipv4_ssh_config(auth, pis_response, pi_info_response, pi_info_respo assert lines[7] == " hostname ssh.pi2.hostedpi.com" +def test_get_ipv4_ssh_config_pi_user(auth, pis_response, pi_info_response, pi_info_response_2): + cloud = PiCloud(auth=auth) + auth._api_session.get.side_effect = [ + pis_response, + pi_info_response, + pi_info_response_2, + ] + ipv4_config = cloud.get_ipv4_ssh_config(user="pi") + assert auth._api_session.get.call_count == 3 + assert auth._api_session.get.call_args_list[0][0][0] == cloud._api_url + "servers" + assert auth._api_session.get.call_args_list[1][0][0] == cloud._api_url + "servers/pi1" + assert auth._api_session.get.call_args_list[2][0][0] == cloud._api_url + "servers/pi2" + lines = ipv4_config.splitlines() + assert len(lines) == 8 + assert lines[0] == "Host pi1" + assert lines[1] == " user pi" + assert lines[2] == " port 5100" + assert lines[3] == " hostname ssh.pi1.hostedpi.com" + assert lines[4] == "Host pi2" + assert lines[5] == " user pi" + assert lines[6] == " port 5123" + assert lines[7] == " hostname ssh.pi2.hostedpi.com" + + +def test_get_ipv4_ssh_config_no_user(auth, pis_response, pi_info_response, pi_info_response_2): + cloud = PiCloud(auth=auth) + auth._api_session.get.side_effect = [ + pis_response, + pi_info_response, + pi_info_response_2, + ] + ipv4_config = cloud.get_ipv4_ssh_config(user=None) + assert auth._api_session.get.call_count == 3 + assert auth._api_session.get.call_args_list[0][0][0] == cloud._api_url + "servers" + assert auth._api_session.get.call_args_list[1][0][0] == cloud._api_url + "servers/pi1" + assert auth._api_session.get.call_args_list[2][0][0] == cloud._api_url + "servers/pi2" + lines = ipv4_config.splitlines() + assert len(lines) == 6 + assert lines[0] == "Host pi1" + assert lines[1] == " port 5100" + assert lines[2] == " hostname ssh.pi1.hostedpi.com" + assert lines[3] == "Host pi2" + assert lines[4] == " port 5123" + assert lines[5] == " hostname ssh.pi2.hostedpi.com" + + def test_get_ipv6_ssh_config(auth, pis_response, pi_info_response, pi_info_response_2): cloud = PiCloud(auth=auth) auth._api_session.get.side_effect = [ @@ -545,11 +591,49 @@ def test_get_ipv6_ssh_config(auth, pis_response, pi_info_response, pi_info_respo ipv6_config = cloud.ipv6_ssh_config assert auth._api_session.get.call_count == 1 assert auth._api_session.get.call_args_list[0][0][0] == cloud._api_url + "servers" - assert ipv6_config.count("\n") == 5 lines = ipv6_config.splitlines() + assert len(lines) == 6 assert lines[0] == "Host pi1" assert lines[1] == " user root" assert lines[2] == " hostname pi1.hostedpi.com" assert lines[3] == "Host pi2" assert lines[4] == " user root" assert lines[5] == " hostname pi2.hostedpi.com" + + +def test_get_ipv6_ssh_config_pi_user(auth, pis_response, pi_info_response, pi_info_response_2): + cloud = PiCloud(auth=auth) + auth._api_session.get.side_effect = [ + pis_response, + pi_info_response, + pi_info_response_2, + ] + ipv6_config = cloud.get_ipv6_ssh_config(user="pi") + assert auth._api_session.get.call_count == 1 + assert auth._api_session.get.call_args_list[0][0][0] == cloud._api_url + "servers" + lines = ipv6_config.splitlines() + assert len(lines) == 6 + assert lines[0] == "Host pi1" + assert lines[1] == " user pi" + assert lines[2] == " hostname pi1.hostedpi.com" + assert lines[3] == "Host pi2" + assert lines[4] == " user pi" + assert lines[5] == " hostname pi2.hostedpi.com" + + +def test_get_ipv6_ssh_config_no_user(auth, pis_response, pi_info_response, pi_info_response_2): + cloud = PiCloud(auth=auth) + auth._api_session.get.side_effect = [ + pis_response, + pi_info_response, + pi_info_response_2, + ] + ipv6_config = cloud.get_ipv6_ssh_config(user=None) + assert auth._api_session.get.call_count == 1 + assert auth._api_session.get.call_args_list[0][0][0] == cloud._api_url + "servers" + lines = ipv6_config.splitlines() + assert len(lines) == 4 + assert lines[0] == "Host pi1" + assert lines[1] == " hostname pi1.hostedpi.com" + assert lines[2] == "Host pi2" + assert lines[3] == " hostname pi2.hostedpi.com"