This commit is contained in:
Qubasa
2023-10-23 22:34:43 +02:00
parent 8ff80b025c
commit 0be3dac289
19 changed files with 146 additions and 94 deletions

View File

@@ -70,7 +70,7 @@ pytest -n0 -s tests/test_secrets_cli.py::test_users
``` ```
## Debugging functions ## Debugging functions
Debugging functions can be found under `src/debug.py` Debugging functions can be found under `src/debug.py`
quite interesting is the function repro_env_break() which drops you into a shell quite interesting is the function repro_env_break() which drops you into a shell
with the test environment loaded. with the test environment loaded.

View File

@@ -1,23 +1,27 @@
from typing import Dict, Optional, Tuple, Callable, Any, Mapping, List import logging
from pathlib import Path import multiprocessing as mp
import ipdb
import os import os
import shlex
import stat import stat
import subprocess import subprocess
from .dirs import find_git_repo_root
import multiprocessing as mp
from .types import FlakeName
import logging
import sys import sys
import shlex from pathlib import Path
import time from typing import Any, Callable, Dict, List, Optional
import ipdb
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def command_exec(cmd: List[str], work_dir:Path, env: Dict[str, str]) -> None:
def command_exec(cmd: List[str], work_dir: Path, env: Dict[str, str]) -> None:
subprocess.run(cmd, check=True, env=env, cwd=work_dir.resolve()) subprocess.run(cmd, check=True, env=env, cwd=work_dir.resolve())
def repro_env_break(work_dir: Path, env: Optional[Dict[str, str]] = None, cmd: Optional[List[str]] = None) -> None:
def repro_env_break(
work_dir: Path,
env: Optional[Dict[str, str]] = None,
cmd: Optional[List[str]] = None,
) -> None:
if env is None: if env is None:
env = os.environ.copy() env = os.environ.copy()
else: else:
@@ -36,14 +40,16 @@ def repro_env_break(work_dir: Path, env: Optional[Dict[str, str]] = None, cmd: O
finally: finally:
proc.terminate() proc.terminate()
def write_command(command: str, loc:Path) -> None:
def write_command(command: str, loc: Path) -> None:
with open(loc, "w") as f: with open(loc, "w") as f:
f.write("#!/usr/bin/env bash\n") f.write("#!/usr/bin/env bash\n")
f.write(command) f.write(command)
st = os.stat(loc) st = os.stat(loc)
os.chmod(loc, st.st_mode | stat.S_IEXEC) os.chmod(loc, st.st_mode | stat.S_IEXEC)
def spawn_process(func: Callable, **kwargs:Any) -> mp.Process:
def spawn_process(func: Callable, **kwargs: Any) -> mp.Process:
if mp.get_start_method(allow_none=True) is None: if mp.get_start_method(allow_none=True) is None:
mp.set_start_method(method="spawn") mp.set_start_method(method="spawn")
@@ -57,7 +63,7 @@ def dump_env(env: Dict[str, str], loc: Path) -> None:
with open(loc, "w") as f: with open(loc, "w") as f:
f.write("#!/usr/bin/env bash\n") f.write("#!/usr/bin/env bash\n")
for k, v in cenv.items(): for k, v in cenv.items():
if v.count('\n') > 0 or v.count("\"") > 0 or v.count("'") > 0: if v.count("\n") > 0 or v.count('"') > 0 or v.count("'") > 0:
continue continue
f.write(f"export {k}='{v}'\n") f.write(f"export {k}='{v}'\n")
st = os.stat(loc) st = os.stat(loc)

View File

@@ -4,6 +4,7 @@ import argparse
from .create import register_create_parser from .create import register_create_parser
from .list import register_list_parser from .list import register_list_parser
# takes a (sub)parser and configures it # takes a (sub)parser and configures it
def register_parser(parser: argparse.ArgumentParser) -> None: def register_parser(parser: argparse.ArgumentParser) -> None:
subparser = parser.add_subparsers( subparser = parser.add_subparsers(

View File

@@ -1,3 +1,4 @@
import logging
import os import os
import shlex import shlex
import shutil import shutil
@@ -6,7 +7,6 @@ import sys
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Any from typing import Any
import logging
from clan_cli.nix import nix_shell from clan_cli.nix import nix_shell
@@ -20,6 +20,7 @@ from .sops import generate_private_key
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def generate_host_key(flake_name: FlakeName, machine_name: str) -> None: def generate_host_key(flake_name: FlakeName, machine_name: str) -> None:
if has_machine(flake_name, machine_name): if has_machine(flake_name, machine_name):
return return
@@ -97,7 +98,9 @@ def generate_secrets_from_nix(
) -> None: ) -> None:
generate_host_key(flake_name, machine_name) generate_host_key(flake_name, machine_name)
errors = {} errors = {}
log.debug("Generating secrets for machine %s and flake %s", machine_name, flake_name) log.debug(
"Generating secrets for machine %s and flake %s", machine_name, flake_name
)
with TemporaryDirectory() as d: with TemporaryDirectory() as d:
# if any of the secrets are missing, we regenerate all connected facts/secrets # if any of the secrets are missing, we regenerate all connected facts/secrets
for secret_group, secret_options in secret_submodules.items(): for secret_group, secret_options in secret_submodules.items():

View File

@@ -2,20 +2,19 @@ import argparse
import asyncio import asyncio
import json import json
import os import os
import re
import shlex import shlex
import sys import sys
import re
from pathlib import Path from pathlib import Path
from typing import Iterator, Dict from typing import Iterator
from uuid import UUID from uuid import UUID
from ..dirs import clan_flakes_dir, specific_flake_dir from ..dirs import clan_flakes_dir, specific_flake_dir
from ..errors import ClanError
from ..nix import nix_build, nix_config, nix_eval, nix_shell from ..nix import nix_build, nix_config, nix_eval, nix_shell
from ..task_manager import BaseTask, Command, create_task from ..task_manager import BaseTask, Command, create_task
from ..types import validate_path from ..types import validate_path
from .inspect import VmConfig, inspect_vm from .inspect import VmConfig, inspect_vm
from ..errors import ClanError
from ..debug import repro_env_break
def is_path_or_url(s: str) -> str | None: def is_path_or_url(s: str) -> str | None:
@@ -29,6 +28,7 @@ def is_path_or_url(s: str) -> str | None:
else: else:
return None return None
class BuildVmTask(BaseTask): class BuildVmTask(BaseTask):
def __init__(self, uuid: UUID, vm: VmConfig) -> None: def __init__(self, uuid: UUID, vm: VmConfig) -> None:
super().__init__(uuid, num_cmds=7) super().__init__(uuid, num_cmds=7)
@@ -95,7 +95,7 @@ class BuildVmTask(BaseTask):
raise ClanError( raise ClanError(
f"flake_url must be a valid path or URL, got {self.vm.flake_url}" f"flake_url must be a valid path or URL, got {self.vm.flake_url}"
) )
elif res == "path": # Only generate secrets for local clans elif res == "path": # Only generate secrets for local clans
cmd = next(cmds) cmd = next(cmds)
if Path(self.vm.flake_url).is_dir(): if Path(self.vm.flake_url).is_dir():
cmd.run( cmd.run(
@@ -105,7 +105,6 @@ class BuildVmTask(BaseTask):
else: else:
self.log.warning("won't generate secrets for non local clan") self.log.warning("won't generate secrets for non local clan")
cmd = next(cmds) cmd = next(cmds)
cmd.run( cmd.run(
[vm_config["uploadSecrets"], clan_name], [vm_config["uploadSecrets"], clan_name],

View File

@@ -3,7 +3,7 @@ import logging
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
from clan_cli.debug import repro_env_break
from ...config.machine import ( from ...config.machine import (
config_for_machine, config_for_machine,
schema_for_machine, schema_for_machine,

View File

@@ -13,6 +13,7 @@ from typing import Iterator
import uvicorn import uvicorn
from pydantic import AnyUrl, IPvAnyAddress from pydantic import AnyUrl, IPvAnyAddress
from pydantic.tools import parse_obj_as from pydantic.tools import parse_obj_as
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -25,8 +26,7 @@ def open_browser(base_url: AnyUrl, sub_url: str) -> None:
break break
except OSError: except OSError:
time.sleep(i) time.sleep(i)
url = parse_obj_as( url = parse_obj_as(AnyUrl, f"{base_url}/{sub_url.removeprefix('/')}")
AnyUrl,f"{base_url}/{sub_url.removeprefix('/')}")
_open_browser(url) _open_browser(url)

View File

@@ -41,6 +41,10 @@ ignore_missing_imports = true
module = "jsonschema.*" module = "jsonschema.*"
ignore_missing_imports = true ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "ipdb.*"
ignore_missing_imports = true
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = "pytest.*" module = "pytest.*"
ignore_missing_imports = true ignore_missing_imports = true
@@ -52,7 +56,7 @@ ignore_missing_imports = true
[tool.ruff] [tool.ruff]
line-length = 88 line-length = 88
select = [ "E", "F", "I", "U", "N"] select = [ "E", "F", "I", "N"]
ignore = [ "E501" ] ignore = [ "E501" ]
[tool.black] [tool.black]

View File

@@ -22,41 +22,41 @@ mkShell {
]; ];
shellHook = '' shellHook = ''
tmp_path=$(realpath ./.direnv) tmp_path=$(realpath ./.direnv)
repo_root=$(realpath .) repo_root=$(realpath .)
mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}" mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}"
# Install the package in editable mode # Install the package in editable mode
# This allows executing `clan` from within the dev-shell using the current # This allows executing `clan` from within the dev-shell using the current
# version of the code and its dependencies. # version of the code and its dependencies.
${pythonWithDeps.interpreter} -m pip install \ ${pythonWithDeps.interpreter} -m pip install \
--quiet \ --quiet \
--disable-pip-version-check \ --disable-pip-version-check \
--no-index \ --no-index \
--no-build-isolation \ --no-build-isolation \
--prefix "$tmp_path/python" \ --prefix "$tmp_path/python" \
--editable $repo_root --editable $repo_root
rm -f clan_cli/nixpkgs clan_cli/webui/assets rm -f clan_cli/nixpkgs clan_cli/webui/assets
ln -sf ${clan-cli.nixpkgs} clan_cli/nixpkgs ln -sf ${clan-cli.nixpkgs} clan_cli/nixpkgs
ln -sf ${ui-assets} clan_cli/webui/assets ln -sf ${ui-assets} clan_cli/webui/assets
export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH" export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH"
export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:" export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:"
export PYTHONBREAKPOINT=ipdb.set_trace export PYTHONBREAKPOINT=ipdb.set_trace
export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}"
mkdir -p \ mkdir -p \
$tmp_path/share/fish/vendor_completions.d \ $tmp_path/share/fish/vendor_completions.d \
$tmp_path/share/bash-completion/completions \ $tmp_path/share/bash-completion/completions \
$tmp_path/share/zsh/site-functions $tmp_path/share/zsh/site-functions
register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish
register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan
./bin/clan flakes create example_clan ./bin/clan flakes create example_clan
./bin/clan machines create example_machine example_clan ./bin/clan machines create example_machine example_clan
''; '';
} }

View File

@@ -112,5 +112,4 @@ def test_flake_with_core_and_pass(
temporary_home, temporary_home,
FlakeName("test_flake_with_core_and_pass"), FlakeName("test_flake_with_core_and_pass"),
CLAN_CORE, CLAN_CORE,
) )

View File

@@ -5,11 +5,11 @@ from typing import Any, Optional
import pytest import pytest
from cli import Cli from cli import Cli
from fixtures_flakes import FlakeForTest
from clan_cli import config from clan_cli import config
from clan_cli.config import parsing from clan_cli.config import parsing
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from fixtures_flakes import FlakeForTest
example_options = f"{Path(config.__file__).parent}/jsonschema/options.json" example_options = f"{Path(config.__file__).parent}/jsonschema/options.json"

View File

@@ -1,8 +1,8 @@
import json import json
from pathlib import Path
from fixtures_flakes import FlakeForTest
import pytest import pytest
from api import TestClient from api import TestClient
from fixtures_flakes import FlakeForTest
@pytest.mark.impure @pytest.mark.impure

View File

@@ -1,10 +1,11 @@
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from fixtures_flakes import FlakeForTest
from clan_cli.debug import repro_env_break
import pytest import pytest
from cli import Cli from cli import Cli
from fixtures_flakes import FlakeForTest
from clan_cli.debug import repro_env_break
if TYPE_CHECKING: if TYPE_CHECKING:
from age_keys import KeyPair from age_keys import KeyPair
@@ -20,7 +21,9 @@ def test_import_sops(
cli = Cli() cli = Cli()
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[1].privkey) monkeypatch.setenv("SOPS_AGE_KEY", age_keys[1].privkey)
cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey, test_flake.name]) cli.run(
["secrets", "machines", "add", "machine1", age_keys[0].pubkey, test_flake.name]
)
cli.run(["secrets", "users", "add", "user1", age_keys[1].pubkey, test_flake.name]) cli.run(["secrets", "users", "add", "user1", age_keys[1].pubkey, test_flake.name])
cli.run(["secrets", "users", "add", "user2", age_keys[2].pubkey, test_flake.name]) cli.run(["secrets", "users", "add", "user2", age_keys[2].pubkey, test_flake.name])
cli.run(["secrets", "groups", "add-user", "group1", "user1", test_flake.name]) cli.run(["secrets", "groups", "add-user", "group1", "user1", test_flake.name])
@@ -29,19 +32,17 @@ def test_import_sops(
# To edit: # To edit:
# SOPS_AGE_KEY=AGE-SECRET-KEY-1U5ENXZQAY62NC78Y2WC0SEGRRMAEEKH79EYY5TH4GPFWJKEAY0USZ6X7YQ sops --age age14tva0txcrl0zes05x7gkx56qd6wd9q3nwecjac74xxzz4l47r44sv3fz62 ./data/secrets.yaml # SOPS_AGE_KEY=AGE-SECRET-KEY-1U5ENXZQAY62NC78Y2WC0SEGRRMAEEKH79EYY5TH4GPFWJKEAY0USZ6X7YQ sops --age age14tva0txcrl0zes05x7gkx56qd6wd9q3nwecjac74xxzz4l47r44sv3fz62 ./data/secrets.yaml
cmd = [ cmd = [
"secrets", "secrets",
"import-sops", "import-sops",
"--group", "--group",
"group1", "group1",
"--machine", "--machine",
"machine1", "machine1",
str(test_root.joinpath("data", "secrets.yaml")), str(test_root.joinpath("data", "secrets.yaml")),
test_flake.name test_flake.name,
] ]
repro_env_break(work_dir=test_flake.path, cmd=cmd) repro_env_break(work_dir=test_flake.path, cmd=cmd)
cli.run( cli.run(cmd)
cmd
)
capsys.readouterr() capsys.readouterr()
cli.run(["secrets", "users", "list", test_flake.name]) cli.run(["secrets", "users", "list", test_flake.name])
users = sorted(capsys.readouterr().out.rstrip().split()) users = sorted(capsys.readouterr().out.rstrip().split())

View File

@@ -1,14 +1,13 @@
from pathlib import Path
from api import TestClient from api import TestClient
from fixtures_flakes import FlakeForTest from fixtures_flakes import FlakeForTest
from clan_cli.debug import repro_env_break
def test_machines(api: TestClient, test_flake: FlakeForTest) -> None: def test_machines(api: TestClient, test_flake: FlakeForTest) -> None:
response = api.get(f"/api/{test_flake.name}/machines") response = api.get(f"/api/{test_flake.name}/machines")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"machines": []} assert response.json() == {"machines": []}
# TODO: Fails because the test_flake fixture needs to init a git repo, which it currently does not
response = api.post(f"/api/{test_flake.name}/machines", json={"name": "test"}) response = api.post(f"/api/{test_flake.name}/machines", json={"name": "test"})
assert response.status_code == 201 assert response.status_code == 201
assert response.json() == {"machine": {"name": "test", "status": "unknown"}} assert response.json() == {"machine": {"name": "test", "status": "unknown"}}

View File

@@ -21,7 +21,16 @@ def test_generate_secret(
monkeypatch.chdir(test_flake_with_core.path) monkeypatch.chdir(test_flake_with_core.path)
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
cli = Cli() cli = Cli()
cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake_with_core.name]) cli.run(
[
"secrets",
"users",
"add",
"user1",
age_keys[0].pubkey,
test_flake_with_core.name,
]
)
cli.run(["secrets", "generate", "vm1", test_flake_with_core.name]) cli.run(["secrets", "generate", "vm1", test_flake_with_core.name])
has_secret(test_flake_with_core.name, "vm1-age.key") has_secret(test_flake_with_core.name, "vm1-age.key")
has_secret(test_flake_with_core.name, "vm1-zerotier-identity-secret") has_secret(test_flake_with_core.name, "vm1-zerotier-identity-secret")

View File

@@ -1,9 +1,9 @@
from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import pytest import pytest
from cli import Cli from cli import Cli
from fixtures_flakes import FlakeForTest from fixtures_flakes import FlakeForTest
from clan_cli.ssh import HostGroup from clan_cli.ssh import HostGroup
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -21,9 +21,27 @@ def test_secrets_upload(
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
cli = Cli() cli = Cli()
cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake_with_core.name]) cli.run(
[
"secrets",
"users",
"add",
"user1",
age_keys[0].pubkey,
test_flake_with_core.name,
]
)
cli.run(["secrets", "machines", "add", "vm1", age_keys[1].pubkey, test_flake_with_core.name]) cli.run(
[
"secrets",
"machines",
"add",
"vm1",
age_keys[1].pubkey,
test_flake_with_core.name,
]
)
monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey) monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey)
cli.run(["secrets", "set", "vm1-age.key", test_flake_with_core.name]) cli.run(["secrets", "set", "vm1-age.key", test_flake_with_core.name])

View File

@@ -1,9 +1,8 @@
from pathlib import Path
import pytest import pytest
from api import TestClient from api import TestClient
from fixtures_flakes import FlakeForTest from fixtures_flakes import FlakeForTest
@pytest.mark.impure @pytest.mark.impure
def test_inspect(api: TestClient, test_flake_with_core: FlakeForTest) -> None: def test_inspect(api: TestClient, test_flake_with_core: FlakeForTest) -> None:
response = api.post( response = api.post(

View File

@@ -9,7 +9,6 @@ from fixtures_flakes import FlakeForTest, create_flake
from httpx import SyncByteStream from httpx import SyncByteStream
from root import CLAN_CORE from root import CLAN_CORE
from clan_cli.debug import repro_env_break
from clan_cli.types import FlakeName from clan_cli.types import FlakeName
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -18,8 +17,7 @@ if TYPE_CHECKING:
@pytest.fixture @pytest.fixture
def flake_with_vm_with_secrets( def flake_with_vm_with_secrets(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch, temporary_home: Path
temporary_home: Path
) -> Iterator[FlakeForTest]: ) -> Iterator[FlakeForTest]:
yield from create_flake( yield from create_flake(
monkeypatch, monkeypatch,
@@ -44,8 +42,6 @@ def remote_flake_with_vm_without_secrets(
) )
def generic_create_vm_test(api: TestClient, flake: Path, vm: str) -> None: def generic_create_vm_test(api: TestClient, flake: Path, vm: str) -> None:
print(f"flake_url: {flake} ") print(f"flake_url: {flake} ")
response = api.post( response = api.post(
@@ -94,7 +90,14 @@ def test_create_local(
) -> None: ) -> None:
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
cli = Cli() cli = Cli()
cmd = ["secrets", "users", "add", "user1", age_keys[0].pubkey, flake_with_vm_with_secrets.name] cmd = [
"secrets",
"users",
"add",
"user1",
age_keys[0].pubkey,
flake_with_vm_with_secrets.name,
]
cli.run(cmd) cli.run(cmd)
generic_create_vm_test(api, flake_with_vm_with_secrets.path, "vm_with_secrets") generic_create_vm_test(api, flake_with_vm_with_secrets.path, "vm_with_secrets")

View File

@@ -1,9 +1,9 @@
import os import os
from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from fixtures_flakes import FlakeForTest
import pytest import pytest
from cli import Cli from cli import Cli
from fixtures_flakes import FlakeForTest
if TYPE_CHECKING: if TYPE_CHECKING:
from age_keys import KeyPair from age_keys import KeyPair
@@ -12,7 +12,9 @@ no_kvm = not os.path.exists("/dev/kvm")
@pytest.mark.impure @pytest.mark.impure
def test_inspect(test_flake_with_core: FlakeForTest, capsys: pytest.CaptureFixture) -> None: def test_inspect(
test_flake_with_core: FlakeForTest, capsys: pytest.CaptureFixture
) -> None:
cli = Cli() cli = Cli()
cli.run(["vms", "inspect", "vm1", test_flake_with_core.name]) cli.run(["vms", "inspect", "vm1", test_flake_with_core.name])
out = capsys.readouterr() # empty the buffer out = capsys.readouterr() # empty the buffer
@@ -29,5 +31,14 @@ def test_create(
monkeypatch.chdir(test_flake_with_core.path) monkeypatch.chdir(test_flake_with_core.path)
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
cli = Cli() cli = Cli()
cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake_with_core.name]) cli.run(
[
"secrets",
"users",
"add",
"user1",
age_keys[0].pubkey,
test_flake_with_core.name,
]
)
cli.run(["vms", "create", "vm1", test_flake_with_core.name]) cli.run(["vms", "create", "vm1", test_flake_with_core.name])