Merge pull request 'rework-installation' (#1804) from rework-installation into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/1804
Reviewed-by: kenji <aks.kenji@protonmail.com>
This commit is contained in:
Mic92
2024-08-21 13:28:54 +00:00
26 changed files with 341 additions and 332 deletions

View File

@@ -32,7 +32,7 @@
secret.user-password = { }; secret.user-password = { };
secret.user-password-hash = { }; secret.user-password-hash = { };
generator.prompt = ( generator.prompt = (
lib.mkIf config.clan.user-password.prompt "Set the password for your $user: ${config.clan.user-password.user}. lib.mkIf config.clan.user-password.prompt "Set the password for your user '${config.clan.user-password.user}'.
You can autogenerate a password, if you leave this prompt blank." You can autogenerate a password, if you leave this prompt blank."
); );
generator.path = with pkgs; [ generator.path = with pkgs; [

View File

@@ -32,9 +32,9 @@ In the `flake.nix` file:
- [x] set a unique `name`. - [x] set a unique `name`.
=== "**buildClan**" === "**normal flake template**"
```nix title="clan-core.lib.buildClan" ```nix title="flake.nix" hl_lines="3"
buildClan { buildClan {
# Set a unique name # Set a unique name
meta.name = "Lobsters"; meta.name = "Lobsters";
@@ -50,11 +50,11 @@ In the `flake.nix` file:
} }
``` ```
=== "**flakeParts**" === "**template using flake-parts**"
!!! info "See [Clan with flake-parts](./flake-parts.md) for help migrating to flake-parts." !!! info "See [Clan with flake-parts](./flake-parts.md) for help migrating to flake-parts."
```nix title="clan-core.flakeModules.default" ```nix title="flake.nix" hl_lines="3"
clan = { clan = {
# Set a unique name # Set a unique name
meta.name = "Lobsters"; meta.name = "Lobsters";
@@ -77,11 +77,11 @@ Adding or configuring a new machine requires two simple steps:
1. Find the remote disk id by executing: 1. Find the remote disk id by executing:
```bash title="setup computer" ```bash title="setup computer"
ssh root@flash-installer.local lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT ssh root@<IP> lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
``` ```
!!! Note !!! Note
Replace `flash-installer.local` with the IP address of the machine if you don't have the avahi service running which resolves mDNS local domains. Replace `<IP>` with the IP address of the machine if you don't have the avahi service running which resolves mDNS local domains.
Which should show something like: Which should show something like:
@@ -97,75 +97,43 @@ Adding or configuring a new machine requires two simple steps:
└─nvme0n1p3 nvme-eui.e8238fa6bf530001001b448b4aec2929-part3 swap 16.8G └─nvme0n1p3 nvme-eui.e8238fa6bf530001001b448b4aec2929-part3 swap 16.8G
``` ```
1. Edit the following fields inside the `flake.nix` 1. Edit the following fields inside the `./machines/jon/configuration.nix` and/or `./machines/sara/configuration.nix`
=== "**buildClan**" ```nix title="./machines/<machine>/configuration.nix" hl_lines="13 18 23 27"
{
imports = [
./hardware-configuration.nix
# contains your disk format and partitioning configuration.
../../modules/disko.nix
# this file is shared among all machines
../../modules/shared.nix
# enables GNOME desktop (optional)
../../modules/gnome.nix
];
```nix title="clan-core.lib.buildClan" hl_lines="18 23" # Put your username here for login
buildClan { users.users.user.username = "__YOUR_USERNAME__";
# ...
machines = {
"jon" = {
imports = [
# ...
./modules/disko.nix
./machines/jon/configuration.nix
];
# ...
# Change this to the correct ip-address or hostname # Set this for clan commands use ssh i.e. `clan machines update`
# The hostname is the machine name by default # If you change the hostname, you need to update this line to root@<new-hostname>
clan.core.networking.targetHost = pkgs.lib.mkDefault "root@jon" # This only works however if you have avahi running on your admin machine else use IP
clan.core.networking.targetHost = "root@__IP__";
# Change this to the ID-LINK of the desired disk shown by 'lsblk' # You can get your disk id by running the following command on the installer:
disko.devices.disk.main = { # Replace <IP> with the IP of the installer printed on the screen or by running the `ip addr` command.
device = "/dev/disk/by-id/__CHANGE_ME__"; # ssh root@<IP> lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
} disko.devices.disk.main.device = "/dev/disk/by-id/__CHANGE_ME__";
# e.g. > cat ~/.ssh/id_ed25519.pub # IMPORTANT! Add your SSH key here
users.users.root.openssh.authorizedKeys.keys = [ # e.g. > cat ~/.ssh/id_ed25519.pub
"<YOUR SSH_KEY>" users.users.root.openssh.authorizedKeys.keys = "__YOUR_SSH_KEY__";
];
# ...
};
};
}
```
=== "**flakeParts**"
```nix title="clan-core.flakeModules.default" hl_lines="18 23"
clan = {
# ...
machines = {
"jon" = {
imports = [
# ...
./modules/disko.nix
./machines/jon/configuration.nix
];
# ...
# Change this to the correct ip-address or hostname
# The hostname is the machine name by default
clan.core.networking.targetHost = pkgs.lib.mkDefault "root@jon"
# Change this to the ID-LINK of the desired disk shown by 'lsblk'
disko.devices.disk.main = {
device = "/dev/disk/by-id/__CHANGE_ME__";
}
# e.g. > cat ~/.ssh/id_ed25519.pub
users.users.root.openssh.authorizedKeys.keys = [
"__YOUR_SSH_KEY__"
];
# ...
};
};
};
```
# ...
}
```
!!! Info "Replace `__YOUR_USERNAME__` with the ip of your machine, if you use avahi you can also use your hostname"
!!! Info "Replace `__IP__` with the ip of your machine, if you use avahi you can also use your hostname"
!!! Info "Replace `__CHANGE_ME__` with the appropriate identifier, such as `nvme-eui.e8238fa6bf530001001b448b4aec2929`" !!! Info "Replace `__CHANGE_ME__` with the appropriate identifier, such as `nvme-eui.e8238fa6bf530001001b448b4aec2929`"
!!! Info "Replace `__YOUR_SSH_KEY__` with your personal key, like `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILoMI0NC5eT9pHlQExrvR5ASV3iW9+BXwhfchq0smXUJ jon@jon-desktop`" !!! Info "Replace `__YOUR_SSH_KEY__` with your personal key, like `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILoMI0NC5eT9pHlQExrvR5ASV3iW9+BXwhfchq0smXUJ jon@jon-desktop`"
@@ -176,25 +144,60 @@ These steps will allow you to update your machine later.
Generate the `hardware-configuration.nix` file for your machine by executing the following command: Generate the `hardware-configuration.nix` file for your machine by executing the following command:
```bash ```bash
clan machines hw-generate [MACHINE_NAME] [HOSTNAME] clan machines hw-generate [MACHINE_NAME]
``` ```
replace `[MACHINE_NAME]` with the name of the machine i.e. `jon` and `[HOSTNAME]` with the `ip_adress` or `hostname` of the machine within the network. i.e. `flash-installer.local` replace `[MACHINE_NAME]` with the name of the machine i.e. `jon` and `[HOSTNAME]` with the `ip_adress` or `hostname` of the machine within the network. i.e. `<IP>`
!!! Example !!! Example
```bash ```bash
clan machines hw-generate jon flash-installer.local clan machines hw-generate jon
``` ```
This command connects to `flash-installer.local` as `root`, runs `nixos-generate-config` to detect hardware configurations (excluding filesystems), and writes them to `machines/jon/hardware-configuration.nix`. This command connects to the ip configured in the previous step, runs `nixos-generate-config` to detect hardware configurations (excluding filesystems), and writes them to `machines/jon/hardware-configuration.nix`.
### Step 3: Custom Disk Formatting ### Step 3: Custom Disk Formatting
In `./modules/disko.nix`, a simple `ext4` disk partitioning scheme is defined for the Disko module. For more complex disk partitioning setups, refer to the [Disko examples](https://github.com/nix-community/disko/tree/master/example). In `./modules/disko.nix`, a simple `ext4` disk partitioning scheme is defined for the Disko module. For more complex disk partitioning setups,
refer to the [Disko templates](https://github.com/nix-community/disko-templates) or [Disko examples](https://github.com/nix-community/disko/tree/master/example).
### Step 4: Custom Configuration ### Step 4: Custom Configuration
Modify `./machines/jon/configuration.nix` to personalize the system settings according to your requirements. Modify `./machines/jon/configuration.nix` to personalize the system settings according to your requirements.
If you wish to name your machine to something else, do the following steps:
```
mv ./machines/jon/configuration.nix ./machines/newname/configuration.nix
```
Than rename `jon` to your preferred name in `machines` in `flake.nix` as well as the import line:
```diff
- imports = [ ./machines/jon/configuration.nix ];
+ imports = [ ./machines/__NEW_NAME__/configuration.nix ];
```
!!! Info "Replace `__NEW_NAME__` with the name of the machine"
Note that our clan lives inside a git repository.
Only files that have been added with `git add` are recognized by `nix`.
So for every file that you add or rename you also need to run:
```
git add ./path/to/my/file
```
For renaming jon to your own machine name, you can use the following command:
```
git mv ./machines/jon ./machines/newname
```
If you only want to setup a single machine at this point, you can delete `sara` from flake.nix as well as from the machines directory:
```
git rm ./machines/sara
```
### Step 5: Check Configuration ### Step 5: Check Configuration
@@ -206,9 +209,9 @@ nix flake check
This command helps ensure that your system configuration is correct and free from errors. This command helps ensure that your system configuration is correct and free from errors.
!!! Note !!! Tip
Integrate this step into your [Continuous Integration](https://en.wikipedia.org/wiki/Continuous_integration) workflow to ensure that only valid Nix configurations are merged into your codebase. This practice helps maintain system stability and reduces integration issues. You can integrate this step into your [Continuous Integration](https://en.wikipedia.org/wiki/Continuous_integration) workflow to ensure that only valid Nix configurations are merged into your codebase.
--- ---

View File

@@ -112,7 +112,7 @@ This process involves preparing a suitable hardware and disk partitioning config
1. **SSH with Password Authentication** 1. **SSH with Password Authentication**
Run the following command to install using SSH: Run the following command to install using SSH:
```bash ```bash
clan machines install [MACHINE] flash-installer.local clan machines install [MACHINE] <IP>
``` ```
2. **Scanning a QR Code for Installation Details** 2. **Scanning a QR Code for Installation Details**
@@ -150,23 +150,17 @@ Clan CLI enables you to remotely update your machines over SSH. This requires se
### Setting the Target Host ### Setting the Target Host
Replace `root@jon` with the actual hostname or IP address of your target machine: Replace `root@jon` with the actual hostname or IP address of your target machine in the `configuration.nix` of the machine:
```{.nix hl_lines="9" .no-copy} ```{.nix hl_lines="9" .no-copy}
buildClan { {
# ... # ...
machines = { # Set this for clan commands use ssh i.e. `clan machines update`
# "jon" will be the hostname of the machine # If you change the hostname, you need to update this line to root@<new-hostname>
"jon" = { # This only works however if you have avahi running on your admin machine else use IP
# Set this for clan commands use ssh i.e. `clan machines update` clan.core.networking.targetHost = "root@jon";
# If you change the hostname, you need to update this line to root@<new-hostname>
# This only works however if you have avahi running on your admin machine else use IP
clan.core.networking.targetHost = pkgs.lib.mkDefault "root@jon";
};
};
}; };
``` ```
!!! warning !!! warning
The use of `root@` in the target address implies SSH access as the `root` user. The use of `root@` in the target address implies SSH access as the `root` user.
Ensure that the root login is secured and only used when necessary. Ensure that the root login is secured and only used when necessary.

View File

@@ -84,7 +84,7 @@ This should yield the following:
└── modules └── modules
└── shared.nix └── shared.nix
5 directories, 6 files 5 directories, 9 files
``` ```
```bash ```bash

View File

@@ -21,7 +21,6 @@ let
++ (with python3.pkgs; [ ++ (with python3.pkgs; [
rope rope
mypy mypy
ipdb
setuptools setuptools
wheel wheel
pip pip
@@ -48,8 +47,6 @@ mkShell {
desktop-file-utils # verify desktop files desktop-file-utils # verify desktop files
]); ]);
PYTHONBREAKPOINT = "ipdb.set_trace";
shellHook = '' shellHook = ''
export GIT_ROOT=$(git rev-parse --show-toplevel) export GIT_ROOT=$(git rev-parse --show-toplevel)
export PKG_ROOT=$GIT_ROOT/pkgs/clan-app export PKG_ROOT=$GIT_ROOT/pkgs/clan-app

View File

@@ -8,28 +8,27 @@ from clan_cli.api import API
from clan_cli.inventory import Inventory, init_inventory from clan_cli.inventory import Inventory, init_inventory
from ..cmd import CmdOut, run from ..cmd import CmdOut, run
from ..dirs import clan_templates
from ..errors import ClanError from ..errors import ClanError
from ..nix import nix_command, nix_shell from ..nix import nix_command, nix_shell
default_template_url: str = "git+https://git.clan.lol/clan/clan-core"
minimal_template_url: str = "git+https://git.clan.lol/clan/clan-core#templates.minimal"
@dataclass @dataclass
class CreateClanResponse: class CreateClanResponse:
flake_init: CmdOut flake_init: CmdOut
git_init: CmdOut | None
git_add: CmdOut
git_config_username: CmdOut | None
git_config_email: CmdOut | None
flake_update: CmdOut flake_update: CmdOut
git_init: CmdOut | None = None
git_add: CmdOut | None = None
git_config_username: CmdOut | None = None
git_config_email: CmdOut | None = None
@dataclass @dataclass
class CreateOptions: class CreateOptions:
directory: Path | str directory: Path | str
# URL to the template to use. Defaults to the "minimal" template # URL to the template to use. Defaults to the "minimal" template
template_url: str = minimal_template_url template: str = "minimal"
setup_git: bool = True
initial: Inventory | None = None initial: Inventory | None = None
@@ -40,7 +39,7 @@ def git_command(directory: Path, *args: str) -> list[str]:
@API.register @API.register
def create_clan(options: CreateOptions) -> CreateClanResponse: def create_clan(options: CreateOptions) -> CreateClanResponse:
directory = Path(options.directory).resolve() directory = Path(options.directory).resolve()
template_url = options.template_url template_url = f"{clan_templates()}#{options.template}"
if not directory.exists(): if not directory.exists():
directory.mkdir() directory.mkdir()
else: else:
@@ -65,26 +64,6 @@ def create_clan(options: CreateOptions) -> CreateClanResponse:
) )
flake_init = run(command, cwd=directory) flake_init = run(command, cwd=directory)
git_init = None
if not directory.joinpath(".git").exists():
git_init = run(git_command(directory, "init"))
git_add = run(git_command(directory, "add", "."))
# check if username is set
has_username = run(git_command(directory, "config", "user.name"), check=False)
git_config_username = None
if has_username.returncode != 0:
git_config_username = run(
git_command(directory, "config", "user.name", "clan-tool")
)
has_username = run(git_command(directory, "config", "user.email"), check=False)
git_config_email = None
if has_username.returncode != 0:
git_config_email = run(
git_command(directory, "config", "user.email", "clan@example.com")
)
flake_update = run( flake_update = run(
nix_shell(["nixpkgs#nix"], ["nix", "flake", "update"]), cwd=directory nix_shell(["nixpkgs#nix"], ["nix", "flake", "update"]), cwd=directory
) )
@@ -94,21 +73,45 @@ def create_clan(options: CreateOptions) -> CreateClanResponse:
response = CreateClanResponse( response = CreateClanResponse(
flake_init=flake_init, flake_init=flake_init,
git_init=git_init,
git_add=git_add,
git_config_username=git_config_username,
git_config_email=git_config_email,
flake_update=flake_update, flake_update=flake_update,
) )
if not options.setup_git:
return response
response.git_init = run(git_command(directory, "init"))
response.git_add = run(git_command(directory, "add", "."))
# check if username is set
has_username = run(git_command(directory, "config", "user.name"), check=False)
response.git_config_username = None
if has_username.returncode != 0:
response.git_config_username = run(
git_command(directory, "config", "user.name", "clan-tool")
)
has_username = run(git_command(directory, "config", "user.email"), check=False)
if has_username.returncode != 0:
response.git_config_email = run(
git_command(directory, "config", "user.email", "clan@example.com")
)
return response return response
def register_create_parser(parser: argparse.ArgumentParser) -> None: def register_create_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
"--url", "--template",
type=str, type=str,
help="url to the clan template", choices=["default", "minimal"],
default=default_template_url, help="Clan template name",
default="default",
)
parser.add_argument(
"--no-git",
help="Do not setup git",
action="store_true",
default=False,
) )
parser.add_argument( parser.add_argument(
@@ -119,7 +122,8 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
create_clan( create_clan(
CreateOptions( CreateOptions(
directory=args.path, directory=args.path,
template_url=args.url, template=args.template,
setup_git=not args.no_git,
) )
) )

View File

@@ -4,6 +4,8 @@ import sys
import urllib import urllib
from pathlib import Path from pathlib import Path
from .errors import ClanError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -41,6 +43,18 @@ def find_toplevel(top_level_files: list[str]) -> Path | None:
return None return None
def clan_templates() -> Path:
template_path = module_root().parent.parent.parent / "templates"
if template_path.exists():
return template_path
else:
template_path = module_root() / "templates"
if not template_path.exists():
msg = f"BUG! clan core not found at {template_path}. This is an issue with packaging the cli"
raise ClanError(msg)
return template_path
def user_config_dir() -> Path: def user_config_dir() -> Path:
if sys.platform == "win32": if sys.platform == "win32":
return Path(os.getenv("APPDATA", os.path.expanduser("~\\AppData\\Roaming\\"))) return Path(os.getenv("APPDATA", os.path.expanduser("~\\AppData\\Roaming\\")))

View File

@@ -5,10 +5,12 @@ import logging
from pathlib import Path from pathlib import Path
from clan_cli.api import API from clan_cli.api import API
from clan_cli.clan_uri import FlakeId
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from ..cmd import run, run_no_stdout from ..cmd import run, run_no_stdout
from ..completions import add_dynamic_completer, complete_machines from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine
from ..nix import nix_config, nix_eval, nix_shell from ..nix import nix_config, nix_eval, nix_shell
from .types import machine_name_type from .types import machine_name_type
@@ -88,9 +90,9 @@ def show_machine_hardware_platform(
@API.register @API.register
def generate_machine_hardware_info( def generate_machine_hardware_info(
clan_dir: str | Path, clan_dir: FlakeId,
machine_name: str, machine_name: str,
hostname: str, hostname: str | None = None,
password: str | None = None, password: str | None = None,
keyfile: str | None = None, keyfile: str | None = None,
force: bool | None = False, force: bool | None = False,
@@ -99,6 +101,13 @@ def generate_machine_hardware_info(
Generate hardware information for a machine Generate hardware information for a machine
and place the resulting *.nix file in the machine's directory. and place the resulting *.nix file in the machine's directory.
""" """
machine = Machine(machine_name, flake=clan_dir)
if hostname is not None:
machine.target_host_address = hostname
host = machine.target_host
target_host = f"{host.user or 'root'}@{host.host}"
cmd = nix_shell( cmd = nix_shell(
[ [
"nixpkgs#openssh", "nixpkgs#openssh",
@@ -110,9 +119,12 @@ def generate_machine_hardware_info(
*(["sshpass", "-p", f"{password}"] if password else []), *(["sshpass", "-p", f"{password}"] if password else []),
"ssh", "ssh",
*(["-i", f"{keyfile}"] if keyfile else []), *(["-i", f"{keyfile}"] if keyfile else []),
# Disable strict host key checking
"-o StrictHostKeyChecking=no",
# Disable known hosts file # Disable known hosts file
"-o",
"UserKnownHostsFile=/dev/null",
"-p",
str(machine.target_host.port),
target_host,
"-o UserKnownHostsFile=/dev/null", "-o UserKnownHostsFile=/dev/null",
f"{hostname}", f"{hostname}",
"nixos-generate-config", "nixos-generate-config",
@@ -149,9 +161,8 @@ def generate_machine_hardware_info(
def hw_generate_command(args: argparse.Namespace) -> None: def hw_generate_command(args: argparse.Namespace) -> None:
flake_path = args.flake.path
hw_info = generate_machine_hardware_info( hw_info = generate_machine_hardware_info(
flake_path, args.machine, args.hostname, args.password, args.force args.flake, args.machine, args.hostname, args.password, args.force
) )
print("----") print("----")
print("Successfully generated hardware information.") print("Successfully generated hardware information.")
@@ -168,9 +179,10 @@ def register_hw_generate(parser: argparse.ArgumentParser) -> None:
type=machine_name_type, type=machine_name_type,
) )
machine_parser = parser.add_argument( machine_parser = parser.add_argument(
"hostname", "target_host",
help="hostname of the machine",
type=str, type=str,
nargs="?",
help="ssh address to install to in the form of user@host:2222",
) )
machine_parser = parser.add_argument( machine_parser = parser.add_argument(
"--password", "--password",

View File

@@ -138,5 +138,7 @@ class SecretStore(SecretStoreBase):
# else: # else:
# # TODO: drop old format soon # # TODO: drop old format soon
# secret_name = secret # secret_name = secret
# (output_dir / secret_name).write_bytes(self.get(service, secret_name)) # with (output_dir / secret_name).open("wb") as f:
# f.chmod(0o600)
# f.write(self.get(service, secret_name))
# (output_dir / ".pass_info").write_bytes(self.generate_hash()) # (output_dir / ".pass_info").write_bytes(self.generate_hash())

View File

@@ -61,6 +61,7 @@ let
cp -r ${./.} $out cp -r ${./.} $out
chmod -R +w $out chmod -R +w $out
ln -sf ${nixpkgs'} $out/clan_cli/nixpkgs ln -sf ${nixpkgs'} $out/clan_cli/nixpkgs
cp -r ${../../templates} $out/clan_cli/templates
${classgen}/bin/classgen ${inventory-schema}/schema.json $out/clan_cli/inventory/classes.py ${classgen}/bin/classgen ${inventory-schema}/schema.json $out/clan_cli/inventory/classes.py
''; '';

View File

@@ -18,7 +18,14 @@ Repository = "https://git.clan.lol/clan/clan-core"
exclude = ["clan_cli.nixpkgs*", "result"] exclude = ["clan_cli.nixpkgs*", "result"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
clan_cli = ["py.typed", "config/jsonschema/*", "webui/assets/**/*", "vms/mimetypes/**/*", "**/allowed-programs.json"] clan_cli = [
"**/allowed-programs.json",
"config/jsonschema/*",
"py.typed",
"templates/**/*",
"vms/mimetypes/**/*",
"webui/assets/**/*",
]
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = "tests" testpaths = "tests"

View File

@@ -15,7 +15,6 @@ let
rope rope
setuptools setuptools
wheel wheel
ipdb
pip pip
]); ]);
in in
@@ -27,8 +26,6 @@ mkShell {
inputsFrom = [ self'.devShells.default ]; inputsFrom = [ self'.devShells.default ];
PYTHONBREAKPOINT = "ipdb.set_trace";
CLAN_STATIC_PROGRAMS = lib.concatStringsSep ":" ( CLAN_STATIC_PROGRAMS = lib.concatStringsSep ":" (
lib.attrNames clan-cli-full.passthru.runtimeDependenciesAsSet lib.attrNames clan-cli-full.passthru.runtimeDependenciesAsSet
); );

View File

@@ -7,8 +7,11 @@ from pytest import CaptureFixture
class CaptureOutput: class CaptureOutput:
def __init__(self, capsys: CaptureFixture) -> None: def __init__(self, capsys: CaptureFixture) -> None:
self.capsys = capsys self.capsys = capsys
self.capsys_disabled = capsys.disabled()
self.capsys_disabled.__enter__()
def __enter__(self) -> "CaptureOutput": def __enter__(self) -> "CaptureOutput":
self.capsys_disabled.__exit__(None, None, None)
self.capsys.readouterr() self.capsys.readouterr()
return self return self
@@ -17,6 +20,11 @@ class CaptureOutput:
self.out = res.out self.out = res.out
self.err = res.err self.err = res.err
# Disable capsys again
self.capsys_disabled = self.capsys.disabled()
self.capsys_disabled.__enter__()
return False
@pytest.fixture @pytest.fixture
def capture_output(capsys: CaptureFixture) -> CaptureOutput: def capture_output(capsys: CaptureFixture) -> CaptureOutput:

View File

@@ -17,8 +17,7 @@ def test_create_flake(
) -> None: ) -> None:
flake_dir = temporary_home / "test-flake" flake_dir = temporary_home / "test-flake"
url = f"{clan_core}#default" cli.run(["flakes", "create", str(flake_dir), "--template=default"])
cli.run(["flakes", "create", str(flake_dir), f"--url={url}"])
assert (flake_dir / ".clan-flake").exists() assert (flake_dir / ".clan-flake").exists()
# Replace the inputs.clan.url in the template flake.nix # Replace the inputs.clan.url in the template flake.nix
@@ -63,8 +62,7 @@ def test_ui_template(
capture_output: CaptureOutput, capture_output: CaptureOutput,
) -> None: ) -> None:
flake_dir = temporary_home / "test-flake" flake_dir = temporary_home / "test-flake"
url = f"{clan_core}#minimal" cli.run(["flakes", "create", str(flake_dir), "--template=minimal"])
cli.run(["flakes", "create", str(flake_dir), f"--url={url}"])
# Replace the inputs.clan.url in the template flake.nix # Replace the inputs.clan.url in the template flake.nix
substitute( substitute(

View File

@@ -19,7 +19,6 @@ let
++ (with python3.pkgs; [ ++ (with python3.pkgs; [
rope rope
mypy mypy
ipdb
setuptools setuptools
wheel wheel
pip pip
@@ -41,8 +40,6 @@ mkShell {
desktop-file-utils # verify desktop files desktop-file-utils # verify desktop files
]); ]);
PYTHONBREAKPOINT = "ipdb.set_trace";
shellHook = '' shellHook = ''
export GIT_ROOT=$(git rev-parse --show-toplevel) export GIT_ROOT=$(git rev-parse --show-toplevel)
export PKG_ROOT=$GIT_ROOT/pkgs/clan-vm-manager export PKG_ROOT=$GIT_ROOT/pkgs/clan-vm-manager

View File

@@ -10,7 +10,7 @@ import toast from "solid-toast";
import { setActiveURI } from "@/src/App"; import { setActiveURI } from "@/src/App";
type CreateForm = Meta & { type CreateForm = Meta & {
template_url: string; template: string;
}; };
export const ClanForm = () => { export const ClanForm = () => {
@@ -18,12 +18,12 @@ export const ClanForm = () => {
initialValues: { initialValues: {
name: "", name: "",
description: "", description: "",
template_url: "git+https://git.clan.lol/clan/clan-core#templates.minimal", template: "minimal",
}, },
}); });
const handleSubmit: SubmitHandler<CreateForm> = async (values, event) => { const handleSubmit: SubmitHandler<CreateForm> = async (values, event) => {
const { template_url, ...meta } = values; const { template, ...meta } = values;
const response = await callApi("open_file", { const response = await callApi("open_file", {
file_request: { mode: "save" }, file_request: { mode: "save" },
@@ -44,7 +44,7 @@ export const ClanForm = () => {
await callApi("create_clan", { await callApi("create_clan", {
options: { options: {
directory: target_dir[0], directory: target_dir[0],
template_url, template,
initial: { initial: {
meta, meta,
services: {}, services: {},
@@ -146,7 +146,7 @@ export const ClanForm = () => {
</label> </label>
)} )}
</Field> </Field>
<Field name="template_url" validate={[required("This is required")]}> <Field name="template" validate={[required("This is required")]}>
{(field, props) => ( {(field, props) => (
<div class="collapse collapse-arrow" tabindex="0"> <div class="collapse collapse-arrow" tabindex="0">
<input type="checkbox" /> <input type="checkbox" />

View File

@@ -161,7 +161,7 @@ export const MachineDetails = () => {
"generate_machine_hardware_info", "generate_machine_hardware_info",
{ {
machine_name: params.id, machine_name: params.id,
clan_dir: curr_uri, clan_dir: { loc: curr_uri },
hostname: query.data.machine.deploy.targetHost, hostname: query.data.machine.deploy.targetHost,
}, },
); );

View File

@@ -1,37 +1,28 @@
{ self, inputs, ... }: { self, inputs, ... }:
{ {
flake.templates = { flake = (import ./flake.nix).outputs { } // {
new-clan = { checks.x86_64-linux.template-minimal =
description = "Initialize a new clan flake"; let
path = ./new-clan; path = self.templates.minimal.path;
}; initialized = inputs.nixpkgs.legacyPackages.x86_64-linux.runCommand "minimal-clan-flake" { } ''
default = self.templates.new-clan; mkdir $out
minimal = { cp -r ${path}/* $out
description = "for clans managed via (G)UI"; mkdir -p $out/machines/foo
path = ./minimal;
};
};
flake.checks.x86_64-linux.template-minimal =
let
path = self.templates.minimal.path;
initialized = inputs.nixpkgs.legacyPackages.x86_64-linux.runCommand "minimal-clan-flake" { } ''
mkdir $out
cp -r ${path}/* $out
rm $out/inventory.json
# TODO: Instead create a machine by calling the API, this wont break in future tests and is much closer to what the user performs # TODO: Instead create a machine by calling the API, this wont break in future tests and is much closer to what the user performs
echo '{ "machines": { "foo": { "name": "foo" } } }' > $out/inventory.json echo '{ "nixpkgs": { "hostPlatform": "x86_64-linux" } }' > $out/machines/foo/settings.json
''; '';
evaled = (import "${initialized}/flake.nix").outputs { evaled = (import "${initialized}/flake.nix").outputs {
self = evaled // { self = evaled // {
outPath = initialized; outPath = initialized;
};
clan-core = self;
}; };
clan-core = self; in
{
type = "derivation";
name = "minimal-clan-flake-check";
inherit (evaled.nixosConfigurations.foo.config.system.build.vm) drvPath outPath;
}; };
in };
{
type = "derivation";
name = "minimal-clan-flake-check";
inherit (evaled.nixosConfigurations.foo.config.system.build.vm) drvPath outPath;
};
} }

16
templates/flake.nix Normal file
View File

@@ -0,0 +1,16 @@
{
outputs =
{ ... }:
{
templates = {
default = {
description = "Initialize a new clan flake";
path = ./new-clan;
};
minimal = {
description = "for clans managed via (G)UI";
path = ./minimal;
};
};
};
}

View File

@@ -0,0 +1,2 @@
# shellcheck shell=bash
use flake

View File

@@ -6,94 +6,24 @@
outputs = outputs =
{ self, clan-core, ... }: { self, clan-core, ... }:
let let
system = "x86_64-linux";
pkgs = clan-core.inputs.nixpkgs.legacyPackages.${system};
# Usage see: https://docs.clan.lol # Usage see: https://docs.clan.lol
clan = clan-core.lib.buildClan { clan = clan-core.lib.buildClan {
directory = self; directory = self;
meta.name = "__CHANGE_ME__"; # Ensure this is unique among all clans you want to use. # Ensure this is unique among all clans you want to use.
meta.name = "__CHANGE_ME__";
# Distributed services, uncomment to enable.
# inventory = {
# services = {
# # This example configures a BorgBackup service
# # Check: https://docs.clan.lol/reference/clanModules which ones are available in Inventory
# borgbackup.instance_1 = {
# roles.server.machines = [ "jon" ];
# roles.client.machines = [ "sara" ];
# };
# };
# };
# Prerequisite: boot into the installer # Prerequisite: boot into the installer
# See: https://docs.clan.lol/getting-started/installer # See: https://docs.clan.lol/getting-started/installer
# local> mkdir -p ./machines/machine1 # local> mkdir -p ./machines/machine1
# local> Edit ./machines/machine1/configuration.nix to your liking # local> Edit ./machines/<machine>/configuration.nix to your liking
machines = { machines = {
# "jon" will be the hostname of the machine # "jon" will be the hostname of the machine
jon = { jon = {
imports = [ imports = [ ./machines/jon/configuration.nix ];
./modules/shared.nix
./modules/disko.nix
./machines/jon/configuration.nix
];
nixpkgs.hostPlatform = system;
# Set this for clan commands use ssh i.e. `clan machines update`
# If you change the hostname, you need to update this line to root@<new-hostname>
# This only works however if you have avahi running on your admin machine else use IP
clan.core.networking.targetHost = pkgs.lib.mkDefault "root@jon";
# You can get your disk id by running the following command on the installer:
# Replace <IP> with the IP of the installer printed on the screen or by running the `ip addr` command.
# ssh root@<IP> lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
disko.devices.disk.main.device = "/dev/disk/by-id/__CHANGE_ME__";
# IMPORTANT! Add your SSH key here
# e.g. > cat ~/.ssh/id_ed25519.pub
users.users.root.openssh.authorizedKeys.keys = throw ''
Don't forget to add your SSH key here!
users.users.root.openssh.authorizedKeys.keys = [ "<YOUR SSH_KEY>" ]
'';
# Zerotier needs one controller to accept new nodes. Once accepted
# the controller can be offline and routing still works.
clan.core.networking.zerotier.controller.enable = true;
}; };
# "sara" will be the hostname of the machine # "sara" will be the hostname of the machine
sara = { sara = {
imports = [ imports = [ ./machines/sara/configuration.nix ];
./modules/shared.nix
./modules/disko.nix
./machines/sara/configuration.nix
];
nixpkgs.hostPlatform = system;
# Set this for clan commands use ssh i.e. `clan machines update`
# If you change the hostname, you need to update this line to root@<new-hostname>
# This only works however if you have avahi running on your admin machine else use IP
clan.core.networking.targetHost = pkgs.lib.mkDefault "root@sara";
# You can get your disk id by running the following command on the installer:
# Replace <IP> with the IP of the installer printed on the screen or by running the `ip addr` command.
# ssh root@<IP> lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
disko.devices.disk.main.device = "/dev/disk/by-id/__CHANGE_ME__";
# IMPORTANT! Add your SSH key here
# e.g. > cat ~/.ssh/id_ed25519.pub
users.users.root.openssh.authorizedKeys.keys = throw ''
Don't forget to add your SSH key here!
users.users.root.openssh.authorizedKeys.keys = [ "<YOUR SSH_KEY>" ]
'';
/*
After jon is deployed, uncomment the following line
This will allow sara to share the VPN overlay network with jon
The networkId is generated by the first deployment of jon
*/
# clan.core.networking.zerotier.networkId = builtins.readFile ../jon/facts/zerotier-network-id;
}; };
}; };
}; };
@@ -102,8 +32,19 @@
# all machines managed by Clan # all machines managed by Clan
inherit (clan) nixosConfigurations clanInternals; inherit (clan) nixosConfigurations clanInternals;
# add the Clan cli tool to the dev shell # add the Clan cli tool to the dev shell
devShells.${system}.default = pkgs.mkShell { # use the "nix develop" command to enter the dev shell
packages = [ clan-core.packages.${system}.clan-cli ]; devShells =
}; clan-core.inputs.nixpkgs.lib.genAttrs
[
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
"x86_64-darwin"
]
(system: {
default = clan-core.inputs.nixpkgs.legacyPackages.${system}.mkShell {
packages = [ clan-core.packages.${system}.clan-cli ];
};
});
}; };
} }

View File

@@ -1,38 +1,36 @@
{ config, ... }:
let
username = config.networking.hostName;
in
{ {
imports = [ ./hardware-configuration.nix ]; imports = [
./hardware-configuration.nix
# contains your disk format and partitioning configuration.
../../modules/disko.nix
# this file is shared among all machines
../../modules/shared.nix
# enables GNOME desktop (optional)
../../modules/gnome.nix
];
# Locale service discovery and mDNS # This is your user login name.
services.avahi.enable = true; users.users.user.name = "<your-username>";
services.xserver.enable = true; # Set this for clan commands use ssh i.e. `clan machines update`
services.xserver.desktopManager.gnome.enable = true; # If you change the hostname, you need to update this line to root@<new-hostname>
services.xserver.displayManager.gdm.enable = true; # This only works however if you have avahi running on your admin machine else use IP
# Disable the default gnome apps to speed up deployment clan.core.networking.targetHost = "root@<IP>";
services.gnome.core-utilities.enable = false;
# Enable automatic login for the user. # You can get your disk id by running the following command on the installer:
services.displayManager.autoLogin = { # Replace <IP> with the IP of the installer printed on the screen or by running the `ip addr` command.
enable = true; # ssh root@<IP> lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
user = username; disko.devices.disk.main.device = "/dev/disk/by-id/__CHANGE_ME__";
};
users.users.${username} = { # IMPORTANT! Add your SSH key here
initialPassword = username; # e.g. > cat ~/.ssh/id_ed25519.pub
isNormalUser = true; users.users.root.openssh.authorizedKeys.keys = [
extraGroups = [ ''
"wheel" __YOUR_SSH_KEY__
"networkmanager" ''
"video" ];
"audio"
"input" # Zerotier needs one controller to accept new nodes. Once accepted
"dialout" # the controller can be offline and routing still works.
"disk" clan.core.networking.zerotier.controller.enable = true;
];
uid = 1000;
openssh.authorizedKeys.keys = config.users.users.root.openssh.authorizedKeys.keys;
};
} }

View File

@@ -1,39 +1,35 @@
{ config, ... }:
let
username = config.networking.hostName;
in
{ {
imports = [ ./hardware-configuration.nix ]; imports = [
./hardware-configuration.nix
../../modules/disko.nix
../../modules/shared.nix
# enables GNOME desktop (optional)
../../modules/gnome.nix
];
# Put your username here for login
users.users.user.name = "<your-username>";
# Locale service discovery and mDNS # Set this for clan commands use ssh i.e. `clan machines update`
services.avahi.enable = true; # If you change the hostname, you need to update this line to root@<new-hostname>
# This only works however if you have avahi running on your admin machine else use IP
clan.core.networking.targetHost = "root@<IP>";
services.xserver.enable = true; # You can get your disk id by running the following command on the installer:
services.xserver.desktopManager.gnome.enable = true; # Replace <IP> with the IP of the installer printed on the screen or by running the `ip addr` command.
services.xserver.displayManager.gdm.enable = true; # ssh root@<IP> lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
# Disable the default gnome apps to speed up deployment disko.devices.disk.main.device = "/dev/disk/by-id/__CHANGE_ME__";
services.gnome.core-utilities.enable = false;
# Enable automatic login for the user. # IMPORTANT! Add your SSH key here
services.displayManager.autoLogin = { # e.g. > cat ~/.ssh/id_ed25519.pub
enable = true; users.users.root.openssh.authorizedKeys.keys = [
user = username; ''
}; __YOUR_SSH_KEY__
''
users.users.${username} = { ];
initialPassword = username; /*
isNormalUser = true; After jon is deployed, uncomment the following line
extraGroups = [ This will allow sara to share the VPN overlay network with jon
"wheel" The networkId is generated by the first deployment of jon
"networkmanager" */
"video" # clan.core.networking.zerotier.networkId = builtins.readFile ../jon/facts/zerotier-network-id;
"audio"
"input"
"dialout"
"disk"
];
uid = 1000;
openssh.authorizedKeys.keys = config.users.users.root.openssh.authorizedKeys.keys;
};
} }

View File

@@ -1,5 +1,7 @@
{ lib, ... }: { lib, ... }:
{ {
# TO NOT EDIT THIS FILE AFTER INSTALLATION of a machine
# Otherwise your system might not boot because of missing partitions / filesystems
boot.loader.grub.efiSupport = lib.mkDefault true; boot.loader.grub.efiSupport = lib.mkDefault true;
boot.loader.grub.efiInstallAsRemovable = lib.mkDefault true; boot.loader.grub.efiInstallAsRemovable = lib.mkDefault true;
disko.devices = { disko.devices = {
@@ -23,6 +25,7 @@
type = "filesystem"; type = "filesystem";
format = "vfat"; format = "vfat";
mountpoint = "/boot"; mountpoint = "/boot";
mountOptions = [ "nofail" ];
}; };
}; };
root = { root = {
@@ -30,6 +33,8 @@
content = { content = {
type = "filesystem"; type = "filesystem";
format = "ext4"; format = "ext4";
# format = "btrfs";
# format = "bcachefs";
mountpoint = "/"; mountpoint = "/";
}; };
}; };

View File

@@ -0,0 +1,5 @@
{
services.xserver.enable = true;
services.xserver.desktopManager.gnome.enable = true;
services.xserver.displayManager.gdm.enable = true;
}

View File

@@ -1,7 +1,28 @@
{ clan-core, ... }: { config, clan-core, ... }:
{ {
imports = [ imports = [
# Enables the OpenSSH server for remote access
clan-core.clanModules.sshd clan-core.clanModules.sshd
# Set a root password
clan-core.clanModules.root-password clan-core.clanModules.root-password
clan-core.clanModules.user-password
]; ];
# Locale service discovery and mDNS
services.avahi.enable = true;
# generate a random password for our user below
# can be read using `clan secrets get <machine-name>-user-password` command
clan.user-password.user = "user";
users.users.user = {
isNormalUser = true;
extraGroups = [
"wheel"
"networkmanager"
"video"
"input"
];
uid = 1000;
openssh.authorizedKeys.keys = config.users.users.root.openssh.authorizedKeys.keys;
};
} }