diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 110be3f1..8c5d519c 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -83,6 +83,7 @@ theme: features: - navigation.instant - navigation.tabs + - content.code.annotate - content.code.copy - content.tabs.link icon: diff --git a/docs/site/getting-started/machines.md b/docs/site/getting-started/machines.md index 80b7fb18..3dd7fda9 100644 --- a/docs/site/getting-started/machines.md +++ b/docs/site/getting-started/machines.md @@ -45,13 +45,98 @@ ssh root@ **Finally deployment time!** Use the following command to build and deploy the image via SSH onto your machine. -Replace `` with the **target computers' ip address**: +=== "**SSH access**" -```bash -clan machines install my-machine -``` -> Note: This may take a while for building and for the file transfer. + + Replace `` with the **target computers' ip address**: + + ```bash + clan machines install my-machine + ``` + + !!!note + Building and deploying time will depend on hardware and connection speed. + +=== "**Image Installer**" + + This method makes use of the image installers of [nixos-images](https://github.com/nix-community/nixos-images). + See how to prepare the installer for use [here](./installer.md). + + The installer will randomly generate a password and local addresses on boot, then run ssh with these preconfigured. + The installer shows it's deployment relevant information in two formats, a text form, as well as a QR code. + + ???example "An example view of a booted installer." + This is an example of the booted installer. + + ```{ .bash .annotate } + ┌─────────────────────────────────────────────────────────────────────────────────────┐ + │ ┌───────────────────────────┐ │ + │ │███████████████████████████│ # This is the QR Code (1) │ + │ │██ ▄▄▄▄▄ █▀▄█▀█▀▄█ ▄▄▄▄▄ ██│ │ + │ │██ █ █ █▀▄▄▄█ ▀█ █ █ ██│ │ + │ │██ █▄▄▄█ █▀▄ ▀▄▄▄█ █▄▄▄█ ██│ │ + │ │██▄▄▄▄▄▄▄█▄▀ ▀▄▀▄█▄▄▄▄▄▄▄██│ │ + │ │███▀▀▀ █▄▄█ ▀▄ ▄▀▄█ ███│ │ + │ │██▄██▄▄█▄▄▀▀██▄▀ ▄▄▄ ▄▀█▀██│ │ + │ │██ ▄▄▄▄▄ █▄▄▄▄ █ █▄█ █▀ ███│ │ + │ │██ █ █ █ █ █ ▄▄▄ ▄▀▀ ██│ │ + │ │██ █▄▄▄█ █ ▄ ▄ ▄ ▀█ ▄███│ │ + │ │██▄▄▄▄▄▄▄█▄▄▄▄▄▄█▄▄▄▄▄█▄███│ │ + │ │███████████████████████████│ │ + │ └───────────────────────────┘ │ + │ ┌─────────────────────────────────────────────────────────────────────────────────┐ │ + │ │Root password: cheesy-capital-unwell # password (2) │ │ + │ │Local network addresses: │ │ + │ │enp1s0 UP 192.168.178.169/24 metric 1024 fe80::21e:6ff:fe45:3c92/64 │ │ + │ │enp2s0 DOWN │ │ + │ │wlan0 DOWN # connect to wlan (3) │ │ + │ │Onion address: 6evxy5yhzytwpnhc2vpscrbti3iktxdhpnf6yim6bbs25p4v6beemzyd.onion │ │ + │ │Multicast DNS: nixos-installer.local │ │ + │ └─────────────────────────────────────────────────────────────────────────────────┘ │ + │ Press 'Ctrl-C' for console access │ + │ │ + └─────────────────────────────────────────────────────────────────────────────────────┘ + ``` + + 1. This is not an actual QR code, because it is displayed rather poorly on text sites. + This would be the actual content of this specific QR code prettified: + ```json + { + "pass": "cheesy-capital-unwell", + "tor": "6evxy5yhzytwpnhc2vpscrbti3iktxdhpnf6yim6bbs25p4v6beemzyd.onion", + "addrs": [ + "2001:9e8:347:ca00:21e:6ff:fe45:3c92" + ] + } + ``` + + To generate the actual QR code, that would be displayed use: + ```shellSession + echo '{"pass":"cheesy-capital-unwell","tor":"6evxy5yhzytwpnhc2vpscrbti3iktxdhpnf6yim6bbs25p4v6beemzyd.onion","addrs":["2001:9e8:347:ca00:21e:6ff:fe45:3c92"]}' | nix run nixpkgs#qrencode -- -s 2 -m 2 -t utf8 + ``` + 2. The root password for the installer medium. + This password is autogenerated and meant to be easily typeable. + 3. See how to connect the installer medium to wlan [here](./installer.md#optional-connect-to-wifi). + 4. :man_raising_hand: I'm a code annotation! I can contain `code`, __formatted + text__, images, ... basically anything that can be written in Markdown. + + + !!!tip + We recommend using KDE Connect for sharing the deployment information from the QR code with the deploying machine. + + + The QR code can be used to deploy either with an image, that is decoded on the fly, or it's contained json information. + + With the path to a `json` string, or the string itself: + ```terminal + clan machines install [MACHINE] --json [JSON] + ``` + With the path to an image containing the relevant QR code: + ```terminal + clan machines install [MACHINE] --png [PATH] + ``` + !!! success diff --git a/pkgs/clan-cli/clan_cli/machines/install.py b/pkgs/clan-cli/clan_cli/machines/install.py index 14644705..1d5dfdf0 100644 --- a/pkgs/clan-cli/clan_cli/machines/install.py +++ b/pkgs/clan-cli/clan_cli/machines/install.py @@ -1,6 +1,8 @@ import argparse import importlib +import json import logging +import os from dataclasses import dataclass from pathlib import Path from tempfile import TemporaryDirectory @@ -9,12 +11,20 @@ from ..cmd import Log, run from ..facts.generate import generate_facts from ..machines.machines import Machine from ..nix import nix_shell +from ..ssh.cli import is_ipv6, is_reachable, qrcode_scan log = logging.getLogger(__name__) +class ClanError(Exception): + pass + + def install_nixos( - machine: Machine, kexec: str | None = None, debug: bool = False + machine: Machine, + kexec: str | None = None, + debug: bool = False, + password: str | None = None, ) -> None: secret_facts_module = importlib.import_module(machine.secret_facts_module) log.info(f"installing {machine.name}") @@ -36,6 +46,9 @@ def install_nixos( upload_dir.mkdir(parents=True) secret_facts_store.upload(upload_dir) + if password: + os.environ["SSHPASS"] = password + cmd = [ "nixos-anywhere", "--flake", @@ -44,6 +57,14 @@ def install_nixos( "--extra-files", str(tmpdir), ] + + if password: + cmd += [ + "--env-password", + "--ssh-option", + "IdentitiesOnly=yes", + ] + if machine.target_host.port: cmd += ["--ssh-port", str(machine.target_host.port)] if kexec: @@ -69,16 +90,37 @@ class InstallOptions: kexec: str | None confirm: bool debug: bool + json_ssh_deploy: dict[str, str] | None def install_command(args: argparse.Namespace) -> None: + json_ssh_deploy = None + if args.json: + json_file = Path(args.json) + if json_file.is_file(): + json_ssh_deploy = json.loads(json_file.read_text()) + else: + json_ssh_deploy = json.loads(args.json) + elif args.png: + json_ssh_deploy = json.loads(qrcode_scan(args.png)) + + if not json_ssh_deploy and not args.target_host: + raise ClanError("No target host provided, please provide a target host.") + + if json_ssh_deploy: + target_host = f"root@{find_reachable_host_from_deploy_json(json_ssh_deploy)}" + password = json_ssh_deploy["pass"] + else: + target_host = args.target_host + opts = InstallOptions( flake=args.flake, machine=args.machine, - target_host=args.target_host, + target_host=target_host, kexec=args.kexec, confirm=not args.yes, debug=args.debug, + json_ssh_deploy=json_ssh_deploy, ) machine = Machine(opts.machine, flake=opts.flake) machine.target_host_address = opts.target_host @@ -88,7 +130,27 @@ def install_command(args: argparse.Namespace) -> None: if ask != "y": return - install_nixos(machine, kexec=opts.kexec, debug=opts.debug) + install_nixos(machine, kexec=opts.kexec, debug=opts.debug, password=password) + + +def find_reachable_host_from_deploy_json(deploy_json: dict[str, str]) -> str: + host = None + for addr in deploy_json["addrs"]: + if is_reachable(addr): + if is_ipv6(addr): + host = f"[{addr}]" + else: + host = addr + break + if not host: + raise ClanError( + f""" + Could not reach any of the host addresses provided in the json string. + Please doublecheck if they are reachable from your machine. + Try `ping [ADDR]` with one of the addresses: {deploy_json['addrs']} + """ + ) + return host def register_install_parser(parser: argparse.ArgumentParser) -> None: @@ -117,6 +179,18 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( "target_host", type=str, + nargs="?", help="ssh address to install to in the form of user@host:2222", ) + group = parser.add_mutually_exclusive_group(required=False) + group.add_argument( + "-j", + "--json", + help="specify the json file for ssh data (generated by starting the clan installer)", + ) + group.add_argument( + "-P", + "--png", + help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer)", + ) parser.set_defaults(func=install_command) diff --git a/pkgs/clan-cli/clan_cli/ssh/cli.py b/pkgs/clan-cli/clan_cli/ssh/cli.py index d793d806..97179132 100644 --- a/pkgs/clan-cli/clan_cli/ssh/cli.py +++ b/pkgs/clan-cli/clan_cli/ssh/cli.py @@ -1,4 +1,5 @@ import argparse +import ipaddress import json import logging import socket @@ -96,6 +97,13 @@ def connect_ssh_from_json(ssh_data: dict[str, str]) -> None: ssh(host=ssh_data["tor"], password=ssh_data["pass"], torify=True) +def is_ipv6(ip: str) -> bool: + try: + return isinstance(ipaddress.ip_address(ip), ipaddress.IPv6Address) + except ValueError: + return False + + def main(args: argparse.Namespace) -> None: if args.json: json_file = Path(args.json)