Compare commits
2 Commits
init/manpa
...
a-kenji-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7c400f51f | ||
|
|
ea6bd8f41d |
@@ -63,9 +63,7 @@
|
||||
};
|
||||
};
|
||||
nodes.client = {
|
||||
environment.systemPackages = [
|
||||
self.packages.${pkgs.system}.clan-cli
|
||||
] ++ self.packages.${pkgs.system}.clan-cli.runtimeDependencies;
|
||||
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
|
||||
environment.etc."install-closure".source = "${closureInfo}/store-paths";
|
||||
virtualisation.memorySize = 2048;
|
||||
nix.settings = {
|
||||
|
||||
@@ -10,19 +10,19 @@ validation:
|
||||
unrecognized_links: warn
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- attr_list
|
||||
- footnotes
|
||||
- meta
|
||||
- plantuml_markdown
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- footnotes
|
||||
- meta
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
- pymdownx.highlight:
|
||||
use_pygments: true
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
{ pkgs, module-docs, ... }:
|
||||
let
|
||||
uml-c4 = pkgs.python3Packages.plantuml-markdown.override { plantuml = pkgs.plantuml-c4; };
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
name = "clan-documentation";
|
||||
|
||||
src = ../.;
|
||||
|
||||
nativeBuildInputs =
|
||||
[
|
||||
pkgs.python3
|
||||
uml-c4
|
||||
]
|
||||
[ pkgs.python3 ]
|
||||
++ (with pkgs.python3Packages; [
|
||||
mkdocs
|
||||
mkdocs-material
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
## Secrets (CLI Reference)
|
||||
|
||||
#### Adding Secrets (set)
|
||||
|
||||
```bash
|
||||
clan secrets set mysecret
|
||||
> Paste your secret:
|
||||
```
|
||||
|
||||
!!! note
|
||||
As you type your secret won't be displayed. Press Enter to save the secret.
|
||||
|
||||
#### List all Secrets (list)
|
||||
|
||||
```bash
|
||||
clan secrets list
|
||||
```
|
||||
|
||||
#### Assigning Access (set)
|
||||
|
||||
By default, secrets are encrypted for your key. To specify which users and machines can access a secret:
|
||||
|
||||
```bash
|
||||
clan secrets set --machine <machine1> --machine <machine2> --user <user1> --user <user2> <secret_name>
|
||||
```
|
||||
|
||||
#### Displaying Secrets (get)
|
||||
|
||||
```bash
|
||||
clan secrets get mysecret
|
||||
```
|
||||
|
||||
#### Rename
|
||||
|
||||
TODO
|
||||
|
||||
#### Remove
|
||||
|
||||
TODO
|
||||
|
||||
#### import-sops
|
||||
|
||||
TODO
|
||||
|
||||
### Users (Reference)
|
||||
|
||||
Learn how to manage users and allowing access to existing secrets.
|
||||
|
||||
#### list user
|
||||
|
||||
Lists all added users
|
||||
|
||||
```bash
|
||||
clan secrets user list
|
||||
```
|
||||
|
||||
``` {.console, title="Example output", .no-copy}
|
||||
jon
|
||||
sara
|
||||
```
|
||||
|
||||
!!! Question "Who can execute this command?"
|
||||
Everyone - completely public.
|
||||
|
||||
#### add user
|
||||
|
||||
add a user
|
||||
|
||||
```bash
|
||||
clan secrets users add {username} {public-key}
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Changes can be trusted by maintainer review in version control.
|
||||
|
||||
#### get user
|
||||
|
||||
get a user public key
|
||||
|
||||
```bash
|
||||
clan secrets users get {username}
|
||||
```
|
||||
|
||||
``` {.console, title="Example output", .no-copy}
|
||||
age1zk8uzrte55wkg9lkqxu5x6twsj2ja4lehegks0cw4mkg6jv37d9qsjpt44
|
||||
```
|
||||
|
||||
#### remove user
|
||||
|
||||
remove a user
|
||||
|
||||
```bash
|
||||
clan secrets users remove {username}
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Changes can be trusted by maintainer review in version control.
|
||||
|
||||
#### add-secret user
|
||||
|
||||
Grants the user (`username`) access to the secret (`secret_name`)
|
||||
|
||||
```bash
|
||||
clan secrets users add-secret {username} {secret_name}
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Requires the executor of the command to have access to the secret (`secret_name`).
|
||||
|
||||
#### remove-secret user
|
||||
|
||||
remove the user (`username`) from accessing the secret (`secret_name`)
|
||||
|
||||
!!! Danger "Make sure at least one person has access."
|
||||
It might still be possible for the machine to access the secret. (See [machines](#machines))
|
||||
|
||||
We highly recommend to use version control such as `git` which allows you to rollback secrets in case anything gets messed up.
|
||||
|
||||
```bash
|
||||
clan secrets users remove-secret {username} {secret_name}
|
||||
```
|
||||
|
||||
!!! Question "Who can execute this command?"
|
||||
Requires the executor of the command to have access to the secret (`secret_name`).
|
||||
|
||||
### Machines (Reference)
|
||||
|
||||
- [list](): list machines
|
||||
- [add](): add a machine
|
||||
- [get](): get a machine public key
|
||||
- [remove](): remove a machine
|
||||
- [add-secret](): allow a machine to access a secret
|
||||
- [remove-secret](): remove a machine's access to a secret
|
||||
|
||||
#### List machine
|
||||
|
||||
New machines in Clan come with age keys stored in `./sops/machines/<machine_name>`. To list these machines:
|
||||
|
||||
```bash
|
||||
clan secrets machines list
|
||||
```
|
||||
|
||||
#### Add machine
|
||||
|
||||
For clan machines the machine key is generated automatically on demand if none exists.
|
||||
|
||||
```bash
|
||||
clan secrets machines add <machine_name> <age_key>
|
||||
```
|
||||
|
||||
If you already have a device key and want to add it manually, see: [How to obtain a remote key](#obtain-remote-keys-manually)
|
||||
|
||||
#### get machine
|
||||
|
||||
TODO
|
||||
|
||||
#### remove machine
|
||||
|
||||
TODO
|
||||
|
||||
#### add-secret machine
|
||||
|
||||
TODO
|
||||
|
||||
#### remove-secret machine
|
||||
|
||||
TODO
|
||||
|
||||
### Groups (Reference)
|
||||
|
||||
The Clan-CLI makes it easy to manage access by allowing you to create groups.
|
||||
|
||||
- [list](): list groups
|
||||
- [add-user](): add a user to group
|
||||
- [remove-user](): remove a user from group
|
||||
- [add-machine](): add a machine to group
|
||||
- [remove-machine](): remove a machine from group
|
||||
- [add-secret](): allow a user to access a secret
|
||||
- [remove-secret](): remove a group's access to a secret
|
||||
|
||||
#### List Groups
|
||||
|
||||
```bash
|
||||
clan secrets groups list
|
||||
```
|
||||
|
||||
#### add-user
|
||||
|
||||
Assign users to a new group, e.g., `admins`:
|
||||
|
||||
```bash
|
||||
clan secrets groups add-user admins <username>
|
||||
```
|
||||
|
||||
!!! info
|
||||
The group is created if no such group existed before.
|
||||
|
||||
The user must exist in beforehand (See: [users](#users-reference))
|
||||
|
||||
```{.console, .no-copy}
|
||||
.
|
||||
├── flake.nix
|
||||
. ...
|
||||
└── sops
|
||||
├── groups
|
||||
│ └── admins
|
||||
│ └── users
|
||||
│ └── <username> -> ../../../users/<username>
|
||||
```
|
||||
|
||||
#### remove-user
|
||||
|
||||
TODO
|
||||
|
||||
#### add-machine
|
||||
|
||||
TODO
|
||||
|
||||
#### remove-machine
|
||||
|
||||
TODO
|
||||
|
||||
#### add-secret
|
||||
|
||||
```bash
|
||||
clan secrets groups add-secret <group_name> <secret_name>
|
||||
```
|
||||
|
||||
#### remove-secret
|
||||
|
||||
TODO
|
||||
|
||||
### Key (Reference)
|
||||
|
||||
- [generate]() generate age key
|
||||
- [show]() show age public key
|
||||
- [update]() re-encrypt all secrets with current keys (useful when changing keys)
|
||||
|
||||
#### generate
|
||||
|
||||
TODO
|
||||
|
||||
#### show
|
||||
|
||||
TODO
|
||||
|
||||
#### update
|
||||
|
||||
TODO
|
||||
|
||||
## Further
|
||||
|
||||
Secrets in the repository follow this structure:
|
||||
|
||||
```{.console, .no-copy}
|
||||
sops/
|
||||
├── secrets/
|
||||
│ └── <secret_name>/
|
||||
│ ├── secret
|
||||
│ └── users/
|
||||
│ └── <your_username>/
|
||||
```
|
||||
|
||||
The content of the secret is stored encrypted inside the `secret` file under `mysecret`.
|
||||
|
||||
By default, secrets are encrypted with your key to ensure readability.
|
||||
|
||||
### Obtain remote keys manually
|
||||
|
||||
To fetch a **SSH host key** from a preinstalled system:
|
||||
|
||||
```bash
|
||||
ssh-keyscan <domain_name> | nix shell nixpkgs#ssh-to-age -c ssh-to-age
|
||||
```
|
||||
|
||||
!!! Success
|
||||
This command converts the SSH key into an age key on the fly. Since this is the format used by the clan secrets backend.
|
||||
|
||||
Once added the **SSH host key** enables seamless integration of existing machines with clan.
|
||||
|
||||
Then add the key by executing:
|
||||
|
||||
```bash
|
||||
clan secrets machines add <machine_name> <age_key>
|
||||
```
|
||||
|
||||
See also: [Machine reference](#machines-reference)
|
||||
|
||||
### NixOS integration
|
||||
|
||||
A NixOS machine will automatically import all secrets that are encrypted for the
|
||||
current machine. At runtime it will use the host key to decrypt all secrets into
|
||||
an in-memory, non-persistent filesystem using [sops-nix](https://github.com/Mic92/sops-nix).
|
||||
In your nixos configuration you can get a path to secrets like this `config.sops.secrets.<name>.path`. For example:
|
||||
|
||||
```nix
|
||||
{ config, ...}: {
|
||||
sops.secrets.my-password.neededForUsers = true;
|
||||
|
||||
users.users.mic92 = {
|
||||
isNormalUser = true;
|
||||
passwordFile = config.sops.secrets.my-password.path;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
See the [readme](https://github.com/Mic92/sops-nix) of sops-nix for more
|
||||
examples.
|
||||
|
||||
### Migration: Importing existing sops-based keys / sops-nix
|
||||
|
||||
`clan secrets` stores each secret in a single file, whereas [sops](https://github.com/Mic92/sops-nix) commonly allows to put all secrets in a yaml or json document.
|
||||
|
||||
If you already happened to use sops-nix, you can migrate by using the `clan secrets import-sops` command by importing these files:
|
||||
|
||||
```bash
|
||||
% clan secrets import-sops --prefix matchbox- --group admins --machine matchbox nixos/matchbox/secrets/secrets.yaml
|
||||
```
|
||||
|
||||
This will create secrets for each secret found in `nixos/matchbox/secrets/secrets.yaml` in a `./sops` folder of your repository.
|
||||
Each member of the group `admins` in this case will be able to decrypt the secrets with their respective key.
|
||||
|
||||
Since our clan secret module will auto-import secrets that are encrypted for a particular nixos machine,
|
||||
you can now remove `sops.secrets.<secrets> = { };` unless you need to specify more options for the secret like owner/group of the secret file.
|
||||
@@ -148,12 +148,3 @@ Adding or configuring a new machine requires two simple steps:
|
||||
**All facts are automatically initialized.**
|
||||
|
||||
If you need additional help see our [facts chapter](./secrets.md)
|
||||
|
||||
---
|
||||
|
||||
## Whats next?
|
||||
|
||||
- [Deploying](machines.md): Deploying a Machine configuration
|
||||
- [Secrets](secrets.md): Learn about secrets and facts
|
||||
|
||||
---
|
||||
|
||||
@@ -62,7 +62,10 @@ clan machines install my-machine <target_host>
|
||||
## What's next ?
|
||||
|
||||
- [**Update a Machine**](#update-your-machines): Learn how to update an existing machine?
|
||||
- [**Configure a Private Network**](./networking.md): Configuring a secure mesh network.
|
||||
|
||||
Coming Soon:
|
||||
|
||||
- **Join Your Machines in a Private Network:**: Stay tuned for steps on linking all your machines into a secure mesh network with Clan.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,28 +5,16 @@ This guide provides detailed instructions for configuring
|
||||
outlined steps to set up a machine as a VPN controller (`<CONTROLLER>`) and to
|
||||
include a new machine into the VPN.
|
||||
|
||||
## Concept
|
||||
|
||||
By default all machines within one clan are connected via a chosen network technology.
|
||||
|
||||
```
|
||||
Clan
|
||||
Node A
|
||||
<-> (zerotier / mycelium / ...)
|
||||
Node B
|
||||
```
|
||||
|
||||
If you select multiple network technologies at the same time. e.g. (zerotier + yggdrassil)
|
||||
You must choose one of them as primary network and the machines are always connected via the primary network.
|
||||
|
||||
## 1. Set-Up the VPN Controller
|
||||
## 1. Setting Up the VPN Controller
|
||||
|
||||
The VPN controller is initially essential for providing configuration to new
|
||||
peers. Once addresses are allocated, the controller's continuous operation is not essential.
|
||||
|
||||
### Instructions
|
||||
|
||||
1. **Designate a Machine**: Label a machine as the VPN controller in the clan,
|
||||
referred to as `<CONTROLLER>` henceforth in this guide.
|
||||
2. **Add Configuration**: Input the following configuration to the NixOS
|
||||
1. **Add Configuration**: Input the following configuration to the NixOS
|
||||
configuration of the controller machine:
|
||||
```nix
|
||||
clan.networking.zerotier.controller = {
|
||||
@@ -34,16 +22,18 @@ peers. Once addresses are allocated, the controller's continuous operation is no
|
||||
public = true;
|
||||
};
|
||||
```
|
||||
3. **Update the Controller Machine**: Execute the following:
|
||||
1. **Update the Controller Machine**: Execute the following:
|
||||
```bash
|
||||
$ clan machines update <CONTROLLER>
|
||||
```
|
||||
Your machine is now operational as the VPN controller.
|
||||
|
||||
## 2. Add Machines to the VPN
|
||||
## 2. Integrating a New Machine to the VPN
|
||||
|
||||
To introduce a new machine to the VPN, adhere to the following steps:
|
||||
|
||||
### Instructions:
|
||||
|
||||
1. **Update Configuration**: On the new machine, incorporate the following to its
|
||||
configuration, substituting `<CONTROLLER>` with the controller machine name:
|
||||
```nix
|
||||
@@ -56,25 +46,22 @@ To introduce a new machine to the VPN, adhere to the following steps:
|
||||
$ clan machines update <NEW_MACHINE>
|
||||
```
|
||||
Replace `<NEW_MACHINE>` with the designated new machine name.
|
||||
|
||||
!!! Note "For Private Networks"
|
||||
1. **Retrieve the ZeroTier ID**: On the `new_machine`, execute:
|
||||
```bash
|
||||
$ sudo zerotier-cli info
|
||||
```
|
||||
Example Output:
|
||||
```{.console, .no-copy}
|
||||
200 info d2c71971db 1.12.1 OFFLINE
|
||||
```
|
||||
, where `d2c71971db` is the ZeroTier ID.
|
||||
2. **Authorize the New Machine on the Controller**: On the controller machine,
|
||||
execute:
|
||||
```bash
|
||||
$ sudo zerotier-members allow <ID>
|
||||
```
|
||||
Substitute `<ID>` with the ZeroTier ID obtained previously.
|
||||
|
||||
2. **Verify Connection**: On the `new_machine`, re-execute:
|
||||
1. **Retrieve the ZeroTier ID**: On the `new_machine`, execute:
|
||||
```bash
|
||||
$ sudo zerotier-cli info
|
||||
```
|
||||
Example Output:
|
||||
```{.console, .no-copy}
|
||||
200 info d2c71971db 1.12.1 OFFLINE
|
||||
```
|
||||
, where `d2c71971db` is the ZeroTier ID.
|
||||
1. **Authorize the New Machine on the Controller**: On the controller machine,
|
||||
execute:
|
||||
```bash
|
||||
$ sudo zerotier-members allow <ID>
|
||||
```
|
||||
Substitute `<ID>` with the ZeroTier ID obtained previously.
|
||||
1. **Verify Connection**: On the `new_machine`, re-execute:
|
||||
```bash
|
||||
$ sudo zerotier-cli info
|
||||
```
|
||||
@@ -87,11 +74,22 @@ To introduce a new machine to the VPN, adhere to the following steps:
|
||||
The new machine is now part of the VPN, and the ZeroTier
|
||||
configuration on NixOS within the Clan project is complete.
|
||||
|
||||
## Further
|
||||
## Decision
|
||||
|
||||
We chose zerotier because in our tests it was the easiest solution to bootstrap. You can selfhost a controller and the controller doesn't need to be globally reachable.
|
||||
|
||||
Currently you can only use **Zerotier** as networking technology because this is the first network stack we aim to support.
|
||||
In the future we plan to add additional network technologies like tinc, head/tailscale, yggdrassil and mycelium.
|
||||
|
||||
We chose zerotier because in our tests it was a straight forwards solution to bootstrap.
|
||||
It allows you to selfhost a controller and the controller doesn't need to be globally reachable.
|
||||
Which made it a good fit for starting the project.
|
||||
## Specification
|
||||
|
||||
By default all machines within one clan are connected via the chosen network technology.
|
||||
|
||||
```
|
||||
Clan
|
||||
Node A
|
||||
<-> (zerotier / mycelium / ...)
|
||||
Node B
|
||||
```
|
||||
|
||||
If you select multiple network technologies at the same time. e.g. (zerotier + yggdrassil)
|
||||
One of them is the primary network and the above statement holds for the primary network.
|
||||
|
||||
@@ -6,7 +6,7 @@ Clan utilizes the [sops](https://github.com/getsops/sops) format and integrates
|
||||
|
||||
This documentation will guide you through managing secrets with the Clan CLI
|
||||
|
||||
## Initializing Secrets (Quickstart)
|
||||
## 1. Initializing Secrets
|
||||
|
||||
### Create Your Master Keypair
|
||||
|
||||
@@ -28,11 +28,11 @@ Generated age private key at '/home/joerg/.config/sops/age/keys.txt' for your us
|
||||
Also add your age public key to the repository with 'clan secrets users add YOUR_USER age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7' (replace YOUR_USER with your actual username)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
!!! warning
|
||||
Make sure to keep a safe backup of the private key you've just created.
|
||||
If it's lost, you won't be able to get to your secrets anymore because they all need the master key to be unlocked.
|
||||
|
||||
!!! note
|
||||
!!! note
|
||||
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
|
||||
|
||||
### Add Your Public Key
|
||||
@@ -41,7 +41,7 @@ Also add your age public key to the repository with 'clan secrets users add YOUR
|
||||
clan secrets users add <your_username> <your_public_key>
|
||||
```
|
||||
|
||||
!!! note
|
||||
!!! note
|
||||
Choose the same username as on your Setup/Source Machine that you use to control the deployment with.
|
||||
|
||||
Once run this will create the following files:
|
||||
@@ -53,137 +53,6 @@ sops/
|
||||
└── key.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> If you followed the quickstart tutorial all necessary secrets are initialized at this point.
|
||||
|
||||
- Continue with [deploying machines](./machines.md)
|
||||
- Learn about the [basics concept](#concept) of clan secrets
|
||||
|
||||
---
|
||||
|
||||
## Concept
|
||||
|
||||
The secrets system conceptually knows two different entities:
|
||||
|
||||
- **Machine**: consumes secrets
|
||||
- **User**: manages access to secrets
|
||||
|
||||
**A Users** Can add or revoke machines' access to secrets.
|
||||
|
||||
**A machine** Can decrypt secrets that where encrypted specifically for that machine.
|
||||
|
||||
!!! Danger
|
||||
**Always make sure at least one _User_ has access to a secret**. Otherwise you could lock yourself out from accessing the secret.
|
||||
|
||||
### Inherited implications
|
||||
|
||||
By default clan uses [sops](https://github.com/getsops/sops) through [sops-nix](https://github.com/Mic92/sops-nix) for managing its secrets which inherits some implications that are important to understand:
|
||||
|
||||
- **Public/Private keys**: Entities are identified via their public keys. Each Entity can use their respective private key to decrypt a secret.
|
||||
- **Public keys are stored**: All Public keys are stored inside the repository
|
||||
- **Secrets are stored Encrypted**: secrets are stored inside the repository encrypted with the respective public keys
|
||||
- **Secrets are deployed encrypted**: Fully encrypted secrets are deployed to machines at deployment time.
|
||||
- **Secrets are decrypted by sops on-demand**: Each machine decrypts its secrets at runtime and stores them at an ephemeral location.
|
||||
- **Machine key-pairs are auto-generated**: When a machine is created **no user-interaction is required** to setup public/private key-pairs.
|
||||
- **secrets are re-encrypted**: In case machines, users or groups are modified secrets get re-encrypted on demand.
|
||||
|
||||
!!! Important
|
||||
After revoking access to a secret you should also change the underlying secret. i.e. change the API key, or the password.
|
||||
|
||||
---
|
||||
|
||||
### Machine and user keys
|
||||
|
||||
The following diagrams illustrates how a user can provide a secret (i.e. a Password).
|
||||
|
||||
- By using the **Clan CLI** a user encrypts the password with both the **User public-key** and the **machine's public-key**
|
||||
|
||||
- The *Machine* can decrypt the password with its private-key on demand.
|
||||
|
||||
- The *User* is able to decrypt the password to make changes to it.
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
!include C4_Container.puml
|
||||
|
||||
Person(user, "User", "Someone who manages secrets")
|
||||
ContainerDb(secret, "Secret")
|
||||
Container(machine, "Machine", "A Machine. i.e. Needs the Secret for a given Service." )
|
||||
|
||||
Rel_R(user, secret, "Encrypt", "", "Pubkeys: User, Machine")
|
||||
Rel_L(secret, user, "Decrypt", "", "user privkey")
|
||||
Rel_R(secret, machine, "Decrypt", "", "machine privkey" )
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
### Groups
|
||||
|
||||
It is possible to create semantic groups to make access control more convenient.
|
||||
|
||||
#### User groups
|
||||
|
||||
Here we illustrate how machine groups work.
|
||||
|
||||
Common use cases:
|
||||
|
||||
- **Shared Management**: Access among multiple users. I.e. a subset of secrets/machines that have two admins
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
!include C4_Container.puml
|
||||
|
||||
System_Boundary(c1, "Group") {
|
||||
Person(user1, "User A", "has access")
|
||||
Person(user2, "User B", "has access")
|
||||
}
|
||||
|
||||
ContainerDb(secret, "Secret")
|
||||
Container(machine, "Machine", "A Machine. i.e. Needs the Secret for a given Service." )
|
||||
|
||||
Rel_R(c1, secret, "Encrypt", "", "Pubkeys: User A, User B, Machine")
|
||||
Rel_R(secret, machine, "Decrypt", "", "machine privkey" )
|
||||
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
<!-- TODO: See also [Groups Reference](#groups-reference) -->
|
||||
|
||||
---
|
||||
|
||||
#### Machine groups
|
||||
|
||||
Here we illustrate how machine groups work.
|
||||
|
||||
Common use cases:
|
||||
|
||||
- **Shared secrets**: Among multiple machines such as Wifi passwords
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
!include C4_Container.puml
|
||||
!include C4_Deployment.puml
|
||||
|
||||
Person(user, "User", "Someone who manages secrets")
|
||||
ContainerDb(secret, "Secret")
|
||||
System_Boundary(c1, "Group") {
|
||||
Container(machine1, "Machine A", "Both machines need the same secret" )
|
||||
Container(machine2, "Machine B", "Both machines need the same secret" )
|
||||
}
|
||||
|
||||
Rel_R(user, secret, "Encrypt", "", "Pubkeys: machine A, machine B, User")
|
||||
Rel(secret, c1, "Decrypt", "", "Both machine A or B can decrypt using their private key" )
|
||||
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
<!-- TODO: See also [Groups Reference](#groups-reference) -->
|
||||
|
||||
---
|
||||
|
||||
## 2. Adding Machine Keys
|
||||
|
||||
New machines in Clan come with age keys stored in `./sops/machines/<machine_name>`. To list these machines:
|
||||
|
||||
@@ -16,7 +16,6 @@ class SecretStore(SecretStoreBase):
|
||||
# no need to generate keys if we don't manage secrets
|
||||
if not hasattr(self.machine, "facts_data"):
|
||||
return
|
||||
|
||||
if not self.machine.facts_data:
|
||||
return
|
||||
|
||||
|
||||
@@ -29,8 +29,6 @@ def commit_files(
|
||||
repo_dir: Path,
|
||||
commit_message: str | None = None,
|
||||
) -> None:
|
||||
if not file_paths:
|
||||
return
|
||||
# check that the file is in the git repository
|
||||
for file_path in file_paths:
|
||||
if not Path(file_path).resolve().is_relative_to(repo_dir.resolve()):
|
||||
|
||||
@@ -29,13 +29,5 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
list_parser = subparser.add_parser("list", help="List machines")
|
||||
register_list_parser(list_parser)
|
||||
|
||||
install_parser = subparser.add_parser(
|
||||
"install",
|
||||
help="Install a machine",
|
||||
description="""
|
||||
Install a configured machine over the network.
|
||||
The target must be a Linux based system reachable via SSH.
|
||||
Installing a machine means overwriting the target's disk.
|
||||
""",
|
||||
)
|
||||
install_parser = subparser.add_parser("install", help="Install a machine")
|
||||
register_install_parser(install_parser)
|
||||
|
||||
@@ -38,6 +38,8 @@ def install_nixos(
|
||||
|
||||
cmd = [
|
||||
"nixos-anywhere",
|
||||
"--debug",
|
||||
"--copy-password",
|
||||
"--flake",
|
||||
f"{machine.flake}#{machine.name}",
|
||||
"--no-reboot",
|
||||
@@ -54,7 +56,8 @@ def install_nixos(
|
||||
|
||||
run(
|
||||
nix_shell(
|
||||
["nixpkgs#nixos-anywhere"],
|
||||
# ["nixpkgs#sshpass", "/home/kenji/git/nix-projects/nixos-anywhere"],
|
||||
["nixpkgs#sshpass", "nixpkgs#nixos-anywhere"],
|
||||
cmd,
|
||||
),
|
||||
log=Log.BOTH,
|
||||
@@ -117,6 +120,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)
|
||||
|
||||
@@ -33,13 +33,10 @@ def list_objects(path: Path, is_valid: Callable[[str], bool]) -> list[str]:
|
||||
return objs
|
||||
|
||||
|
||||
def remove_object(path: Path, name: str) -> list[Path]:
|
||||
paths_to_commit = []
|
||||
def remove_object(path: Path, name: str) -> None:
|
||||
try:
|
||||
shutil.rmtree(path / name)
|
||||
paths_to_commit.append(path / name)
|
||||
except FileNotFoundError:
|
||||
raise ClanError(f"{name} not found in {path}")
|
||||
if not os.listdir(path):
|
||||
os.rmdir(path)
|
||||
return paths_to_commit
|
||||
|
||||
@@ -2,8 +2,6 @@ import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.git import commit_files
|
||||
|
||||
from ..errors import ClanError
|
||||
from ..machines.types import machine_name_type, validate_hostname
|
||||
from . import secrets
|
||||
@@ -89,21 +87,19 @@ def list_directory(directory: Path) -> str:
|
||||
return msg
|
||||
|
||||
|
||||
def update_group_keys(flake_dir: Path, group: str) -> list[Path]:
|
||||
updated_paths = []
|
||||
def update_group_keys(flake_dir: Path, group: str) -> None:
|
||||
for secret_ in secrets.list_secrets(flake_dir):
|
||||
secret = sops_secrets_folder(flake_dir) / secret_
|
||||
if (secret / "groups" / group).is_symlink():
|
||||
updated_paths += update_keys(
|
||||
update_keys(
|
||||
secret,
|
||||
list(sorted(secrets.collect_keys_for_path(secret))),
|
||||
)
|
||||
return updated_paths
|
||||
|
||||
|
||||
def add_member(
|
||||
flake_dir: Path, group_folder: Path, source_folder: Path, name: str
|
||||
) -> list[Path]:
|
||||
) -> None:
|
||||
source = source_folder / name
|
||||
if not source.exists():
|
||||
msg = f"{name} does not exist in {source_folder}: "
|
||||
@@ -118,7 +114,7 @@ def add_member(
|
||||
)
|
||||
os.remove(user_target)
|
||||
user_target.symlink_to(os.path.relpath(source, user_target.parent))
|
||||
return update_group_keys(flake_dir, group_folder.parent.name)
|
||||
update_group_keys(flake_dir, group_folder.parent.name)
|
||||
|
||||
|
||||
def remove_member(flake_dir: Path, group_folder: Path, name: str) -> None:
|
||||
@@ -140,14 +136,9 @@ def remove_member(flake_dir: Path, group_folder: Path, name: str) -> None:
|
||||
|
||||
|
||||
def add_user(flake_dir: Path, group: str, name: str) -> None:
|
||||
updated_files = add_member(
|
||||
add_member(
|
||||
flake_dir, users_folder(flake_dir, group), sops_users_folder(flake_dir), name
|
||||
)
|
||||
commit_files(
|
||||
updated_files,
|
||||
flake_dir,
|
||||
f"Add user {name} to group {group}",
|
||||
)
|
||||
|
||||
|
||||
def add_user_command(args: argparse.Namespace) -> None:
|
||||
@@ -163,17 +154,12 @@ def remove_user_command(args: argparse.Namespace) -> None:
|
||||
|
||||
|
||||
def add_machine(flake_dir: Path, group: str, name: str) -> None:
|
||||
updated_files = add_member(
|
||||
add_member(
|
||||
flake_dir,
|
||||
machines_folder(flake_dir, group),
|
||||
sops_machines_folder(flake_dir),
|
||||
name,
|
||||
)
|
||||
commit_files(
|
||||
updated_files,
|
||||
flake_dir,
|
||||
f"Add machine {name} to group {group}",
|
||||
)
|
||||
|
||||
|
||||
def add_machine_command(args: argparse.Namespace) -> None:
|
||||
@@ -203,14 +189,7 @@ def add_secret_command(args: argparse.Namespace) -> None:
|
||||
|
||||
|
||||
def remove_secret(flake_dir: Path, group: str, name: str) -> None:
|
||||
updated_paths = secrets.disallow_member(
|
||||
secrets.groups_folder(flake_dir, name), group
|
||||
)
|
||||
commit_files(
|
||||
updated_paths,
|
||||
flake_dir,
|
||||
f"Remove group {group} from secret {name}",
|
||||
)
|
||||
secrets.disallow_member(secrets.groups_folder(flake_dir, name), group)
|
||||
|
||||
|
||||
def remove_secret_command(args: argparse.Namespace) -> None:
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.git import commit_files
|
||||
|
||||
from .. import tty
|
||||
from ..errors import ClanError
|
||||
from .secrets import update_secrets
|
||||
@@ -13,7 +11,9 @@ def generate_key() -> str:
|
||||
path = default_sops_key_path()
|
||||
if path.exists():
|
||||
raise ClanError(f"Key already exists at {path}")
|
||||
priv_key, pub_key = generate_private_key(out_file=path)
|
||||
priv_key, pub_key = generate_private_key()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(priv_key)
|
||||
return pub_key
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def show_command(args: argparse.Namespace) -> None:
|
||||
|
||||
def update_command(args: argparse.Namespace) -> None:
|
||||
flake_dir = Path(args.flake)
|
||||
commit_files(update_secrets(flake_dir), flake_dir, "Updated secrets with new keys.")
|
||||
update_secrets(flake_dir)
|
||||
|
||||
|
||||
def register_key_parser(parser: argparse.ArgumentParser) -> None:
|
||||
|
||||
@@ -21,19 +21,14 @@ def add_machine(flake_dir: Path, name: str, key: str, force: bool) -> None:
|
||||
|
||||
paths.extend(update_secrets(flake_dir, filter_secrets=filter_machine_secrets))
|
||||
commit_files(
|
||||
paths,
|
||||
[path],
|
||||
flake_dir,
|
||||
f"Add machine {name} to secrets",
|
||||
)
|
||||
|
||||
|
||||
def remove_machine(flake_dir: Path, name: str) -> None:
|
||||
removed_paths = remove_object(sops_machines_folder(flake_dir), name)
|
||||
commit_files(
|
||||
removed_paths,
|
||||
flake_dir,
|
||||
f"Remove machine {name}",
|
||||
)
|
||||
remove_object(sops_machines_folder(flake_dir), name)
|
||||
|
||||
|
||||
def get_machine(flake_dir: Path, name: str) -> str:
|
||||
@@ -54,27 +49,20 @@ def list_machines(flake_dir: Path) -> list[str]:
|
||||
|
||||
|
||||
def add_secret(flake_dir: Path, machine: str, secret: str) -> None:
|
||||
paths = secrets.allow_member(
|
||||
path = secrets.allow_member(
|
||||
secrets.machines_folder(flake_dir, secret),
|
||||
sops_machines_folder(flake_dir),
|
||||
machine,
|
||||
)
|
||||
commit_files(
|
||||
paths,
|
||||
[path],
|
||||
flake_dir,
|
||||
f"Add {machine} to secret",
|
||||
)
|
||||
|
||||
|
||||
def remove_secret(flake_dir: Path, machine: str, secret: str) -> None:
|
||||
updated_paths = secrets.disallow_member(
|
||||
secrets.machines_folder(flake_dir, secret), machine
|
||||
)
|
||||
commit_files(
|
||||
updated_paths,
|
||||
flake_dir,
|
||||
f"Remove {machine} from secret {secret}",
|
||||
)
|
||||
secrets.disallow_member(secrets.machines_folder(flake_dir, secret), machine)
|
||||
|
||||
|
||||
def list_command(args: argparse.Namespace) -> None:
|
||||
|
||||
@@ -85,7 +85,7 @@ def encrypt_secret(
|
||||
|
||||
files_to_commit = []
|
||||
for user in add_users:
|
||||
files_to_commit.extend(
|
||||
files_to_commit.append(
|
||||
allow_member(
|
||||
users_folder(flake_dir, secret.name),
|
||||
sops_users_folder(flake_dir),
|
||||
@@ -95,7 +95,7 @@ def encrypt_secret(
|
||||
)
|
||||
|
||||
for machine in add_machines:
|
||||
files_to_commit.extend(
|
||||
files_to_commit.append(
|
||||
allow_member(
|
||||
machines_folder(flake_dir, secret.name),
|
||||
sops_machines_folder(flake_dir),
|
||||
@@ -105,7 +105,7 @@ def encrypt_secret(
|
||||
)
|
||||
|
||||
for group in add_groups:
|
||||
files_to_commit.extend(
|
||||
files_to_commit.append(
|
||||
allow_member(
|
||||
groups_folder(flake_dir, secret.name),
|
||||
sops_groups_folder(flake_dir),
|
||||
@@ -118,7 +118,7 @@ def encrypt_secret(
|
||||
|
||||
if key.pubkey not in keys:
|
||||
keys.add(key.pubkey)
|
||||
files_to_commit.extend(
|
||||
files_to_commit.append(
|
||||
allow_member(
|
||||
users_folder(flake_dir, secret.name),
|
||||
sops_users_folder(flake_dir),
|
||||
@@ -180,7 +180,7 @@ def list_directory(directory: Path) -> str:
|
||||
|
||||
def allow_member(
|
||||
group_folder: Path, source_folder: Path, name: str, do_update_keys: bool = True
|
||||
) -> list[Path]:
|
||||
) -> Path:
|
||||
source = source_folder / name
|
||||
if not source.exists():
|
||||
msg = f"Cannot encrypt {group_folder.parent.name} for '{name}' group. '{name}' group does not exist in {source_folder}: "
|
||||
@@ -196,18 +196,15 @@ def allow_member(
|
||||
os.remove(user_target)
|
||||
|
||||
user_target.symlink_to(os.path.relpath(source, user_target.parent))
|
||||
changed = [user_target]
|
||||
if do_update_keys:
|
||||
changed.extend(
|
||||
update_keys(
|
||||
group_folder.parent,
|
||||
list(sorted(collect_keys_for_path(group_folder.parent))),
|
||||
)
|
||||
update_keys(
|
||||
group_folder.parent,
|
||||
list(sorted(collect_keys_for_path(group_folder.parent))),
|
||||
)
|
||||
return changed
|
||||
return user_target
|
||||
|
||||
|
||||
def disallow_member(group_folder: Path, name: str) -> list[Path]:
|
||||
def disallow_member(group_folder: Path, name: str) -> None:
|
||||
target = group_folder / name
|
||||
if not target.exists():
|
||||
msg = f"{name} does not exist in group in {group_folder}: "
|
||||
@@ -228,7 +225,7 @@ def disallow_member(group_folder: Path, name: str) -> list[Path]:
|
||||
if len(os.listdir(group_folder.parent)) == 0:
|
||||
os.rmdir(group_folder.parent)
|
||||
|
||||
return update_keys(
|
||||
update_keys(
|
||||
target.parent.parent, list(sorted(collect_keys_for_path(group_folder.parent)))
|
||||
)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def get_public_key(privkey: str) -> str:
|
||||
return res.stdout.strip()
|
||||
|
||||
|
||||
def generate_private_key(out_file: Path | None = None) -> tuple[str, str]:
|
||||
def generate_private_key() -> tuple[str, str]:
|
||||
cmd = nix_shell(["nixpkgs#age"], ["age-keygen"])
|
||||
try:
|
||||
proc = run(cmd)
|
||||
@@ -50,9 +50,6 @@ def generate_private_key(out_file: Path | None = None) -> tuple[str, str]:
|
||||
raise ClanError("Could not find public key in age-keygen output")
|
||||
if not private_key:
|
||||
raise ClanError("Could not find private key in age-keygen output")
|
||||
if out_file:
|
||||
out_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_file.write_text(res)
|
||||
return private_key, pubkey
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise ClanError("Failed to generate private sops key") from e
|
||||
|
||||
@@ -32,12 +32,7 @@ def add_user(flake_dir: Path, name: str, key: str, force: bool) -> None:
|
||||
|
||||
|
||||
def remove_user(flake_dir: Path, name: str) -> None:
|
||||
removed_paths = remove_object(sops_users_folder(flake_dir), name)
|
||||
commit_files(
|
||||
removed_paths,
|
||||
flake_dir,
|
||||
f"Remove user {name}",
|
||||
)
|
||||
remove_object(sops_users_folder(flake_dir), name)
|
||||
|
||||
|
||||
def get_user(flake_dir: Path, name: str) -> str:
|
||||
@@ -57,25 +52,13 @@ def list_users(flake_dir: Path) -> list[str]:
|
||||
|
||||
|
||||
def add_secret(flake_dir: Path, user: str, secret: str) -> None:
|
||||
updated_paths = secrets.allow_member(
|
||||
secrets.allow_member(
|
||||
secrets.users_folder(flake_dir, secret), sops_users_folder(flake_dir), user
|
||||
)
|
||||
commit_files(
|
||||
updated_paths,
|
||||
flake_dir,
|
||||
f"Add {user} to secret",
|
||||
)
|
||||
|
||||
|
||||
def remove_secret(flake_dir: Path, user: str, secret: str) -> None:
|
||||
updated_paths = secrets.disallow_member(
|
||||
secrets.users_folder(flake_dir, secret), user
|
||||
)
|
||||
commit_files(
|
||||
updated_paths,
|
||||
flake_dir,
|
||||
f"Remove {user} from secret",
|
||||
)
|
||||
secrets.disallow_member(secrets.users_folder(flake_dir, secret), user)
|
||||
|
||||
|
||||
def list_command(args: argparse.Namespace) -> None:
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
qemu,
|
||||
gnupg,
|
||||
e2fsprogs,
|
||||
mandown,
|
||||
mypy,
|
||||
clan-core-path,
|
||||
}:
|
||||
@@ -121,7 +120,6 @@ python3.pkgs.buildPythonApplication {
|
||||
nativeBuildInputs = [
|
||||
setuptools
|
||||
installShellFiles
|
||||
mandown
|
||||
];
|
||||
|
||||
propagatedBuildInputs = pythonDependencies;
|
||||
@@ -129,7 +127,7 @@ python3.pkgs.buildPythonApplication {
|
||||
# Define and expose the tests and checks to run in CI
|
||||
passthru.tests =
|
||||
(lib.mapAttrs' (n: lib.nameValuePair "clan-dep-${n}") runtimeDependenciesAsSet)
|
||||
// {
|
||||
// rec {
|
||||
clan-pytest-without-core =
|
||||
runCommand "clan-pytest-without-core"
|
||||
{ nativeBuildInputs = [ pythonWithTestDeps ] ++ testDependencies; }
|
||||
@@ -180,8 +178,6 @@ python3.pkgs.buildPythonApplication {
|
||||
<(${argcomplete}/bin/register-python-argcomplete --shell bash clan)
|
||||
installShellCompletion --fish --name clan.fish \
|
||||
<(${argcomplete}/bin/register-python-argcomplete --shell fish clan)
|
||||
mandown $src/man/clan.md > ./clan.1
|
||||
installManPage ./clan.1
|
||||
'';
|
||||
|
||||
# Clean up after the package to avoid leaking python packages into a devshell
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
packages = {
|
||||
clan-cli = pkgs.python3.pkgs.callPackage ./default.nix {
|
||||
inherit (inputs) nixpkgs;
|
||||
mandown = pkgs.mandown;
|
||||
clan-core-path = clanCoreWithVendoredDeps;
|
||||
};
|
||||
default = self'.packages.clan-cli;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# Manpages for the clan cli
|
||||
|
||||
Run:
|
||||
```
|
||||
nix run nixpkgs#manpage [.md] > [manpage.1]
|
||||
```
|
||||
to convert a markdown page into a man page.
|
||||
@@ -1,51 +0,0 @@
|
||||
## NAME
|
||||
clan - the clan cli tool
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
clan [OPTION...] [SUBCOMMAND]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
**clan** is the cli for managing and deploying various nixos configurations in a unified coherent way.
|
||||
Secrets and passwords can be provisioned, set and retrieved.
|
||||
Machines can be remotely installed through the **install** subcommand.
|
||||
|
||||
For more overview please refer to the reference material at docs.clan.lol.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
### -h, --help
|
||||
Output a help message and exit.
|
||||
|
||||
### --debug
|
||||
Enable debug logging.
|
||||
|
||||
### --option name value
|
||||
Nix option to set.
|
||||
|
||||
### --flake=FLAKE
|
||||
Path to the flake where the clan resides in, can be a remote flake, or local.
|
||||
|
||||
## SUBCOMMANDS
|
||||
|
||||
### clan-backups(1)
|
||||
Manage backups of clan machines
|
||||
### clan-flakes(1)
|
||||
Create a clan flake inside the current directory
|
||||
### clan-config(1)
|
||||
Set nixos-configuration
|
||||
### clan-ssh(1)
|
||||
SSH to a remote machine
|
||||
### clan-secrets(1)
|
||||
Manage secrets
|
||||
### clan-facts(1)
|
||||
Manage facts
|
||||
### clan-machines(1)
|
||||
Manage machines and their configuration
|
||||
### clan-vms(1)
|
||||
Manage virtual machines
|
||||
### clan-history(1)
|
||||
Manage history
|
||||
### clan-flash(1)
|
||||
Flash machines to usb sticks or into isos
|
||||
@@ -175,18 +175,6 @@ def test_flake(
|
||||
monkeypatch: pytest.MonkeyPatch, temporary_home: Path
|
||||
) -> Iterator[FlakeForTest]:
|
||||
yield from create_flake(monkeypatch, temporary_home, "test_flake")
|
||||
# check that git diff on ./sops is empty
|
||||
if (temporary_home / "test_flake" / "sops").exists():
|
||||
git_proc = sp.run(
|
||||
["git", "diff", "--exit-code", "./sops"],
|
||||
cwd=temporary_home / "test_flake",
|
||||
stderr=sp.PIPE,
|
||||
)
|
||||
if git_proc.returncode != 0:
|
||||
log.error(git_proc.stderr.decode())
|
||||
raise Exception(
|
||||
"git diff on ./sops is not empty. This should not happen as all changes should be committed"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Generic, TypeVar
|
||||
from typing import Any, ClassVar, Generic, TypeVar
|
||||
|
||||
import gi
|
||||
|
||||
@@ -24,6 +24,10 @@ class GKVStore(GObject.GObject, Gio.ListModel, Generic[K, V]):
|
||||
This class could be optimized by having the objects remember their position in the list.
|
||||
"""
|
||||
|
||||
__gsignals__: ClassVar = {
|
||||
"is_ready": (GObject.SignalFlags.RUN_FIRST, None, []),
|
||||
}
|
||||
|
||||
def __init__(self, gtype: type[V], key_gen: Callable[[V], K]) -> None:
|
||||
super().__init__()
|
||||
self.gtype = gtype
|
||||
|
||||
@@ -43,11 +43,8 @@ class EmptySplash(Gtk.Box):
|
||||
join_button = Gtk.Button(label="Join")
|
||||
join_button.connect("clicked", self._on_join, join_entry)
|
||||
|
||||
join_entry.connect("activate", lambda e: self._on_join(join_button, e))
|
||||
|
||||
clamp = Adw.Clamp()
|
||||
clamp.set_maximum_size(400)
|
||||
clamp.set_margin_bottom(40)
|
||||
vbox.append(empty_label)
|
||||
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
hbox.append(join_entry)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar
|
||||
from typing import Any
|
||||
|
||||
import gi
|
||||
from clan_cli.clan_uri import ClanURI
|
||||
@@ -15,7 +15,7 @@ from clan_vm_manager.views.logs import Logs
|
||||
|
||||
gi.require_version("GObject", "2.0")
|
||||
gi.require_version("Gtk", "4.0")
|
||||
from gi.repository import Gio, GLib, GObject
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,18 +25,10 @@ class VMStore(GKVStore):
|
||||
super().__init__(VMObject, lambda vm: vm.data.flake.flake_attr)
|
||||
|
||||
|
||||
class Emitter(GObject.GObject):
|
||||
__gsignals__: ClassVar = {
|
||||
"is_ready": (GObject.SignalFlags.RUN_FIRST, None, []),
|
||||
}
|
||||
|
||||
|
||||
class ClanStore:
|
||||
_instance: "None | ClanStore" = None
|
||||
_clan_store: GKVStore[str, VMStore]
|
||||
|
||||
_emitter: Emitter
|
||||
|
||||
# set the vm that is outputting logs
|
||||
# build logs are automatically streamed to the logs-view
|
||||
_logging_vm: VMObject | None = None
|
||||
@@ -52,16 +44,9 @@ class ClanStore:
|
||||
cls._clan_store = GKVStore(
|
||||
VMStore, lambda store: store.first().data.flake.flake_url
|
||||
)
|
||||
cls._emitter = Emitter()
|
||||
|
||||
return cls._instance
|
||||
|
||||
def emit(self, signal: str) -> None:
|
||||
self._emitter.emit(signal)
|
||||
|
||||
def connect(self, signal: str, cb: Callable[(...), Any]) -> None:
|
||||
self._emitter.connect(signal, cb)
|
||||
|
||||
def set_logging_vm(self, ident: str) -> VMObject | None:
|
||||
vm = self.get_vm(ClanURI(f"clan://{ident}"))
|
||||
if vm is not None:
|
||||
|
||||
@@ -74,11 +74,11 @@ class ClanList(Gtk.Box):
|
||||
self.join_boxed_list.add_css_class("join-list")
|
||||
self.append(self.join_boxed_list)
|
||||
|
||||
clan_store = ClanStore.use()
|
||||
clan_store = ClanStore.use().clan_store
|
||||
clan_store.connect("is_ready", self.display_splash)
|
||||
|
||||
self.group_list = create_boxed_list(
|
||||
model=clan_store.clan_store, render_row=self.render_group_row
|
||||
model=clan_store, render_row=self.render_group_row
|
||||
)
|
||||
self.group_list.add_css_class("group-list")
|
||||
self.append(self.group_list)
|
||||
@@ -338,7 +338,7 @@ class ClanList(Gtk.Box):
|
||||
|
||||
def on_after_join(self, source: JoinValue) -> None:
|
||||
ToastOverlay.use().add_toast_unique(
|
||||
SuccessToast(f"Updated {source.url.machine.name}").toast,
|
||||
SuccessToast(f"Added/updated {source.url.machine.name}").toast,
|
||||
"success.join",
|
||||
)
|
||||
# If the join request list is empty disable the shadow artefact
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
|
||||
import gi
|
||||
from clan_cli.history.list import list_history
|
||||
@@ -41,7 +42,9 @@ class MainWindow(Adw.ApplicationWindow):
|
||||
self.tray_icon: TrayIcon = TrayIcon(app)
|
||||
|
||||
# Initialize all ClanStore
|
||||
threading.Thread(target=self._populate_vms).start()
|
||||
threading.Thread(
|
||||
target=self._populate_vms, args=[self._set_clan_store_ready]
|
||||
).start()
|
||||
|
||||
# Initialize all views
|
||||
stack_view = ViewStack.use().view
|
||||
@@ -65,17 +68,16 @@ class MainWindow(Adw.ApplicationWindow):
|
||||
|
||||
self.connect("destroy", self.on_destroy)
|
||||
|
||||
def _set_clan_store_ready(self) -> bool:
|
||||
ClanStore.use().emit("is_ready")
|
||||
return GLib.SOURCE_REMOVE
|
||||
def _set_clan_store_ready(self) -> None:
|
||||
ClanStore.use().clan_store.emit("is_ready")
|
||||
|
||||
def _populate_vms(self) -> None:
|
||||
def _populate_vms(self, done: Callable[[], None]) -> None:
|
||||
# Execute `clan flakes add <path>` to democlan for this to work
|
||||
# TODO: Make list_history a generator function
|
||||
for entry in list_history():
|
||||
GLib.idle_add(ClanStore.use().create_vm_task, entry)
|
||||
|
||||
GLib.idle_add(self._set_clan_store_ready)
|
||||
GLib.idle_add(done)
|
||||
|
||||
def kill_vms(self) -> None:
|
||||
log.debug("Killing all VMs")
|
||||
|
||||
Reference in New Issue
Block a user