Compare commits
1 Commits
update-tem
...
feat/clan-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6cec3521a |
@@ -83,6 +83,7 @@ theme:
|
|||||||
features:
|
features:
|
||||||
- navigation.instant
|
- navigation.instant
|
||||||
- navigation.tabs
|
- navigation.tabs
|
||||||
|
- content.code.annotate
|
||||||
- content.code.copy
|
- content.code.copy
|
||||||
- content.tabs.link
|
- content.tabs.link
|
||||||
icon:
|
icon:
|
||||||
|
|||||||
@@ -45,13 +45,98 @@ ssh root@<your_target_machine_ip>
|
|||||||
|
|
||||||
**Finally deployment time!** Use the following command to build and deploy the image via SSH onto your machine.
|
**Finally deployment time!** Use the following command to build and deploy the image via SSH onto your machine.
|
||||||
|
|
||||||
|
=== "**SSH access**"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Replace `<target_host>` with the **target computers' ip address**:
|
Replace `<target_host>` with the **target computers' ip address**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clan machines install my-machine <target_host>
|
clan machines install my-machine <target_host>
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note: This may take a while for building and for the file transfer.
|
!!!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
|
!!! success
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import importlib
|
import importlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
@@ -9,12 +11,20 @@ from ..cmd import Log, run
|
|||||||
from ..facts.generate import generate_facts
|
from ..facts.generate import generate_facts
|
||||||
from ..machines.machines import Machine
|
from ..machines.machines import Machine
|
||||||
from ..nix import nix_shell
|
from ..nix import nix_shell
|
||||||
|
from ..ssh.cli import is_ipv6, is_reachable, qrcode_scan
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ClanError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def install_nixos(
|
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:
|
) -> None:
|
||||||
secret_facts_module = importlib.import_module(machine.secret_facts_module)
|
secret_facts_module = importlib.import_module(machine.secret_facts_module)
|
||||||
log.info(f"installing {machine.name}")
|
log.info(f"installing {machine.name}")
|
||||||
@@ -36,6 +46,9 @@ def install_nixos(
|
|||||||
upload_dir.mkdir(parents=True)
|
upload_dir.mkdir(parents=True)
|
||||||
secret_facts_store.upload(upload_dir)
|
secret_facts_store.upload(upload_dir)
|
||||||
|
|
||||||
|
if password:
|
||||||
|
os.environ["SSHPASS"] = password
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"nixos-anywhere",
|
"nixos-anywhere",
|
||||||
"--flake",
|
"--flake",
|
||||||
@@ -44,6 +57,14 @@ def install_nixos(
|
|||||||
"--extra-files",
|
"--extra-files",
|
||||||
str(tmpdir),
|
str(tmpdir),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if password:
|
||||||
|
cmd += [
|
||||||
|
"--env-password",
|
||||||
|
"--ssh-option",
|
||||||
|
"IdentitiesOnly=yes",
|
||||||
|
]
|
||||||
|
|
||||||
if machine.target_host.port:
|
if machine.target_host.port:
|
||||||
cmd += ["--ssh-port", str(machine.target_host.port)]
|
cmd += ["--ssh-port", str(machine.target_host.port)]
|
||||||
if kexec:
|
if kexec:
|
||||||
@@ -69,16 +90,37 @@ class InstallOptions:
|
|||||||
kexec: str | None
|
kexec: str | None
|
||||||
confirm: bool
|
confirm: bool
|
||||||
debug: bool
|
debug: bool
|
||||||
|
json_ssh_deploy: dict[str, str] | None
|
||||||
|
|
||||||
|
|
||||||
def install_command(args: argparse.Namespace) -> 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(
|
opts = InstallOptions(
|
||||||
flake=args.flake,
|
flake=args.flake,
|
||||||
machine=args.machine,
|
machine=args.machine,
|
||||||
target_host=args.target_host,
|
target_host=target_host,
|
||||||
kexec=args.kexec,
|
kexec=args.kexec,
|
||||||
confirm=not args.yes,
|
confirm=not args.yes,
|
||||||
debug=args.debug,
|
debug=args.debug,
|
||||||
|
json_ssh_deploy=json_ssh_deploy,
|
||||||
)
|
)
|
||||||
machine = Machine(opts.machine, flake=opts.flake)
|
machine = Machine(opts.machine, flake=opts.flake)
|
||||||
machine.target_host_address = opts.target_host
|
machine.target_host_address = opts.target_host
|
||||||
@@ -88,7 +130,27 @@ def install_command(args: argparse.Namespace) -> None:
|
|||||||
if ask != "y":
|
if ask != "y":
|
||||||
return
|
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:
|
def register_install_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
@@ -117,6 +179,18 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"target_host",
|
"target_host",
|
||||||
type=str,
|
type=str,
|
||||||
|
nargs="?",
|
||||||
help="ssh address to install to in the form of user@host:2222",
|
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)
|
parser.set_defaults(func=install_command)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import socket
|
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)
|
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:
|
def main(args: argparse.Namespace) -> None:
|
||||||
if args.json:
|
if args.json:
|
||||||
json_file = Path(args.json)
|
json_file = Path(args.json)
|
||||||
|
|||||||
Reference in New Issue
Block a user