switch to deploykit from deploy_nixos
This commit is contained in:
parent
2387d97c5b
commit
d30e84aad5
5 changed files with 129 additions and 308 deletions
253
deploy_nixos.py
253
deploy_nixos.py
|
@ -1,253 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import os
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from typing import List, Dict, Tuple, IO, Iterator, Optional, Callable, Any, Union, Text
|
|
||||||
from threading import Thread
|
|
||||||
import subprocess
|
|
||||||
import fcntl
|
|
||||||
import select
|
|
||||||
from contextlib import ExitStack
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def pipe() -> Iterator[Tuple[IO[str], IO[str]]]:
|
|
||||||
(pipe_r, pipe_w) = os.pipe()
|
|
||||||
read_end = os.fdopen(pipe_r, "r")
|
|
||||||
write_end = os.fdopen(pipe_w, "w")
|
|
||||||
|
|
||||||
try:
|
|
||||||
fl = fcntl.fcntl(read_end, fcntl.F_GETFL)
|
|
||||||
fcntl.fcntl(read_end, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
|
||||||
|
|
||||||
yield (read_end, write_end)
|
|
||||||
finally:
|
|
||||||
read_end.close()
|
|
||||||
write_end.close()
|
|
||||||
|
|
||||||
|
|
||||||
FILE = Union[None, int]
|
|
||||||
|
|
||||||
|
|
||||||
class HostKeyCheck(Enum):
|
|
||||||
STRICT = 0
|
|
||||||
# trust-on-first-use
|
|
||||||
TOFU = 1
|
|
||||||
NONE = 2
|
|
||||||
|
|
||||||
|
|
||||||
class DeployHost:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
host: str,
|
|
||||||
user: str = "root",
|
|
||||||
port: int = 22,
|
|
||||||
forward_agent: bool = False,
|
|
||||||
command_prefix: Optional[str] = None,
|
|
||||||
host_key_check: HostKeyCheck = HostKeyCheck.STRICT,
|
|
||||||
meta: Dict[str, Any] = {},
|
|
||||||
) -> None:
|
|
||||||
self.host = host
|
|
||||||
self.user = user
|
|
||||||
self.port = port
|
|
||||||
if command_prefix:
|
|
||||||
self.command_prefix = command_prefix
|
|
||||||
else:
|
|
||||||
self.command_prefix = host
|
|
||||||
self.forward_agent = forward_agent
|
|
||||||
self.host_key_check = host_key_check
|
|
||||||
self.meta = meta
|
|
||||||
|
|
||||||
def _prefix_output(
|
|
||||||
self, print_fd: IO[str], stdout: Optional[IO[str]], stderr: Optional[IO[str]]
|
|
||||||
) -> Tuple[str, str]:
|
|
||||||
rlist = [print_fd]
|
|
||||||
if stdout is not None:
|
|
||||||
rlist.append(stdout)
|
|
||||||
|
|
||||||
if stderr is not None:
|
|
||||||
rlist.append(stderr)
|
|
||||||
|
|
||||||
print_buf = ""
|
|
||||||
stdout_buf = ""
|
|
||||||
stderr_buf = ""
|
|
||||||
|
|
||||||
while len(rlist) != 0:
|
|
||||||
r, _, _ = select.select(rlist, [], [])
|
|
||||||
|
|
||||||
if print_fd in r:
|
|
||||||
read = os.read(print_fd.fileno(), 4096)
|
|
||||||
if len(read) == 0:
|
|
||||||
rlist.remove(print_fd)
|
|
||||||
print_buf += read.decode("utf-8")
|
|
||||||
if read == "" or "\n" in print_buf:
|
|
||||||
lines = print_buf.rstrip("\n").split("\n")
|
|
||||||
for line in lines:
|
|
||||||
print(f"[{self.command_prefix}] {line}")
|
|
||||||
print_buf = ""
|
|
||||||
|
|
||||||
def handle_fd(fd: Optional[IO[Any]]) -> str:
|
|
||||||
if fd and fd in r:
|
|
||||||
read = os.read(fd.fileno(), 4096)
|
|
||||||
if len(read) == 0:
|
|
||||||
rlist.remove(fd)
|
|
||||||
else:
|
|
||||||
return read.decode("utf-8")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
stdout_buf += handle_fd(stdout)
|
|
||||||
stderr_buf += handle_fd(stderr)
|
|
||||||
return stdout_buf, stderr_buf
|
|
||||||
|
|
||||||
def _run(
|
|
||||||
self, cmd: List[str], shell: bool, stdout: FILE = None, stderr: FILE = None
|
|
||||||
) -> subprocess.CompletedProcess[Text]:
|
|
||||||
with ExitStack() as stack:
|
|
||||||
if stdout is None or stderr is None:
|
|
||||||
read_fd, write_fd = stack.enter_context(pipe())
|
|
||||||
|
|
||||||
if stdout is None:
|
|
||||||
stdout_read = None
|
|
||||||
stdout_write = write_fd
|
|
||||||
elif stdout == subprocess.PIPE:
|
|
||||||
stdout_read, stdout_write = stack.enter_context(pipe())
|
|
||||||
|
|
||||||
if stderr is None:
|
|
||||||
stderr_read = None
|
|
||||||
stderr_write = write_fd
|
|
||||||
elif stderr == subprocess.PIPE:
|
|
||||||
stderr_read, stderr_write = stack.enter_context(pipe())
|
|
||||||
|
|
||||||
with subprocess.Popen(
|
|
||||||
cmd, text=True, shell=shell, stdout=stdout_write, stderr=stderr_write
|
|
||||||
) as p:
|
|
||||||
write_fd.close()
|
|
||||||
if stdout == subprocess.PIPE:
|
|
||||||
stdout_write.close()
|
|
||||||
if stderr == subprocess.PIPE:
|
|
||||||
stderr_write.close()
|
|
||||||
stdout_data, stderr_data = self._prefix_output(read_fd, stdout_read, stderr_read)
|
|
||||||
ret = p.wait()
|
|
||||||
return subprocess.CompletedProcess(
|
|
||||||
cmd, ret, stdout=stdout_data, stderr=stderr_data
|
|
||||||
)
|
|
||||||
raise RuntimeError("unreachable")
|
|
||||||
|
|
||||||
def run_local(
|
|
||||||
self, cmd: str, stdout: FILE = None, stderr: FILE = None
|
|
||||||
) -> subprocess.CompletedProcess:
|
|
||||||
print(f"[{self.command_prefix}] {cmd}")
|
|
||||||
return self._run([cmd], shell=True, stdout=stdout, stderr=stderr)
|
|
||||||
|
|
||||||
def run(
|
|
||||||
self, cmd: str, stdout: FILE = None, stderr: FILE = None
|
|
||||||
) -> subprocess.CompletedProcess:
|
|
||||||
print(f"[{self.command_prefix}] {cmd}")
|
|
||||||
ssh_opts = ["-A"] if self.forward_agent else []
|
|
||||||
|
|
||||||
if self.host_key_check != HostKeyCheck.STRICT:
|
|
||||||
ssh_opts.extend(["-o", "StrictHostKeyChecking=no"])
|
|
||||||
if self.host_key_check == HostKeyCheck.NONE:
|
|
||||||
ssh_opts.extend(["-o", "UserKnownHostsFile=/dev/null"])
|
|
||||||
|
|
||||||
ssh_cmd = (
|
|
||||||
["ssh", f"{self.user}@{self.host}", "-p", str(self.port)]
|
|
||||||
+ ssh_opts
|
|
||||||
+ ["--", cmd]
|
|
||||||
)
|
|
||||||
return self._run(ssh_cmd, shell=False, stdout=stdout, stderr=stderr)
|
|
||||||
|
|
||||||
|
|
||||||
DeployResults = List[Tuple[DeployHost, subprocess.CompletedProcess[Text]]]
|
|
||||||
|
|
||||||
|
|
||||||
class DeployGroup:
|
|
||||||
def __init__(self, hosts: List[DeployHost]) -> None:
|
|
||||||
self.hosts = hosts
|
|
||||||
|
|
||||||
def _run_local(
|
|
||||||
self,
|
|
||||||
cmd: str,
|
|
||||||
host: DeployHost,
|
|
||||||
results: DeployResults,
|
|
||||||
stdout: FILE = None,
|
|
||||||
stderr: FILE = None,
|
|
||||||
) -> None:
|
|
||||||
results.append((host, host.run_local(cmd, stdout=stdout, stderr=stderr)))
|
|
||||||
|
|
||||||
def _run_remote(
|
|
||||||
self,
|
|
||||||
cmd: str,
|
|
||||||
host: DeployHost,
|
|
||||||
results: DeployResults,
|
|
||||||
stdout: FILE = None,
|
|
||||||
stderr: FILE = None,
|
|
||||||
) -> None:
|
|
||||||
results.append((host, host.run(cmd, stdout=stdout, stderr=stderr)))
|
|
||||||
|
|
||||||
def _run(
|
|
||||||
self, cmd: str, local: bool = False, stdout: FILE = None, stderr: FILE = None
|
|
||||||
) -> DeployResults:
|
|
||||||
results: DeployResults = []
|
|
||||||
threads = []
|
|
||||||
for host in self.hosts:
|
|
||||||
fn = self._run_local if local else self._run_remote
|
|
||||||
thread = Thread(
|
|
||||||
target=fn,
|
|
||||||
kwargs=dict(
|
|
||||||
results=results, cmd=cmd, host=host, stdout=stdout, stderr=stderr
|
|
||||||
),
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
threads.append(thread)
|
|
||||||
|
|
||||||
for thread in threads:
|
|
||||||
thread.join()
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
def run(self, cmd: str, stdout: FILE = None, stderr: FILE = None) -> DeployResults:
|
|
||||||
return self._run(cmd, stdout=stdout, stderr=stderr)
|
|
||||||
|
|
||||||
def run_local(
|
|
||||||
self, cmd: str, stdout: FILE = None, stderr: FILE = None
|
|
||||||
) -> DeployResults:
|
|
||||||
return self._run(cmd, local=True, stdout=stdout, stderr=stderr)
|
|
||||||
|
|
||||||
def run_function(self, func: Callable) -> None:
|
|
||||||
threads = []
|
|
||||||
for host in self.hosts:
|
|
||||||
thread = Thread(
|
|
||||||
target=func,
|
|
||||||
args=(host,),
|
|
||||||
)
|
|
||||||
threads.append(thread)
|
|
||||||
|
|
||||||
for thread in threads:
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
for thread in threads:
|
|
||||||
thread.join()
|
|
||||||
|
|
||||||
|
|
||||||
def parse_hosts(
|
|
||||||
hosts: str,
|
|
||||||
host_key_check: HostKeyCheck = HostKeyCheck.STRICT,
|
|
||||||
forward_agent: bool = False,
|
|
||||||
) -> DeployGroup:
|
|
||||||
deploy_hosts = []
|
|
||||||
for h in hosts.split(","):
|
|
||||||
parts = h.split("@")
|
|
||||||
if len(parts) > 1:
|
|
||||||
user = parts[0]
|
|
||||||
hostname = parts[1]
|
|
||||||
else:
|
|
||||||
user = "root"
|
|
||||||
hostname = parts[0]
|
|
||||||
deploy_hosts.append(
|
|
||||||
DeployHost(
|
|
||||||
hostname, user=user, host_key_check=host_key_check, forward_agent=forward_agent
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return DeployGroup(deploy_hosts)
|
|
45
flake.lock
generated
45
flake.lock
generated
|
@ -1,5 +1,28 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"deploykit": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-parts": [
|
||||||
|
"flake-parts"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1661774097,
|
||||||
|
"narHash": "sha256-dvWpOU3dU9TfSnK7I+CSDrCaASr+uR5VepG/SQs/RIg=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "deploykit",
|
||||||
|
"rev": "c8cd9ddcd647e3116b07e920e53aebf047cbeac5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "deploykit",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
|
@ -16,6 +39,26 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1661009076,
|
||||||
|
"narHash": "sha256-phAE40gctVygRq3G3B6LhvD7u2qdQT21xsz8DdRDYFo=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "850d8a76026127ef02f040fb0dcfdb8b749dd9d9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"hercules-ci-effects": {
|
"hercules-ci-effects": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
|
@ -310,6 +353,8 @@
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"deploykit": "deploykit",
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
"hercules-ci-effects": "hercules-ci-effects",
|
"hercules-ci-effects": "hercules-ci-effects",
|
||||||
"hydra": "hydra",
|
"hydra": "hydra",
|
||||||
"nixpkgs": "nixpkgs_3",
|
"nixpkgs": "nixpkgs_3",
|
||||||
|
|
133
flake.nix
133
flake.nix
|
@ -21,63 +21,90 @@
|
||||||
hercules-ci-effects.url = "github:hercules-ci/hercules-ci-effects";
|
hercules-ci-effects.url = "github:hercules-ci/hercules-ci-effects";
|
||||||
hydra.url = "github:NixOS/hydra";
|
hydra.url = "github:NixOS/hydra";
|
||||||
hydra.inputs.nixpkgs.follows = "nixpkgs";
|
hydra.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
|
||||||
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
|
flake-parts.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
|
||||||
|
deploykit.url = "github:numtide/deploykit";
|
||||||
|
deploykit.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
deploykit.inputs.flake-parts.follows = "flake-parts";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self
|
outputs = {
|
||||||
, nixpkgs
|
self,
|
||||||
, nixpkgs-update
|
flake-parts,
|
||||||
, nixpkgs-update-github-releases
|
...
|
||||||
, nixpkgs-update-pypi-releases
|
}:
|
||||||
, sops-nix
|
(flake-parts.lib.evalFlakeModule
|
||||||
, hercules-ci-effects
|
{inherit self;}
|
||||||
, hydra
|
{
|
||||||
}: {
|
systems = ["x86_64-linux" "aarch64-linux" "x86_64-linux" "aarch64-darwin"];
|
||||||
devShell.x86_64-linux = let
|
|
||||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
|
||||||
in pkgs.callPackage ./shell.nix {
|
|
||||||
inherit (sops-nix.packages.x86_64-linux) sops-import-keys-hook;
|
|
||||||
};
|
|
||||||
nixosConfigurations = let
|
|
||||||
common = [
|
|
||||||
sops-nix.nixosModules.sops
|
|
||||||
];
|
|
||||||
in {
|
|
||||||
"build01.nix-community.org" = nixpkgs.lib.nixosSystem {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
modules = common ++ [
|
|
||||||
./build01/configuration.nix
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
"build02.nix-community.org" = nixpkgs.lib.nixosSystem {
|
perSystem = {
|
||||||
system = "x86_64-linux";
|
inputs',
|
||||||
modules = common ++ [
|
pkgs,
|
||||||
(import ./build02/nixpkgs-update.nix {
|
...
|
||||||
inherit nixpkgs-update
|
}: {
|
||||||
nixpkgs-update-github-releases
|
devShells.default = pkgs.callPackage ./shell.nix {
|
||||||
nixpkgs-update-pypi-releases;
|
inherit (inputs'.sops-nix.packages) sops-import-keys-hook;
|
||||||
})
|
inherit (inputs'.deploykit.packages) deploykit;
|
||||||
./build02/configuration.nix
|
};
|
||||||
];
|
};
|
||||||
};
|
flake.nixosConfigurations = let
|
||||||
|
inherit (self.inputs.nixpkgs.lib) nixosSystem;
|
||||||
|
common = [
|
||||||
|
self.inputs.sops-nix.nixosModules.sops
|
||||||
|
];
|
||||||
|
in {
|
||||||
|
"build01.nix-community.org" = nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules =
|
||||||
|
common
|
||||||
|
++ [
|
||||||
|
./build01/configuration.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
"build03.nix-community.org" = nixpkgs.lib.nixosSystem {
|
"build02.nix-community.org" = nixosSystem {
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
modules = common ++ [
|
modules =
|
||||||
(import ./services/hydra {
|
common
|
||||||
inherit hydra;
|
++ [
|
||||||
})
|
(import ./build02/nixpkgs-update.nix {
|
||||||
|
inherit
|
||||||
|
(self.inputs)
|
||||||
|
nixpkgs-update
|
||||||
|
nixpkgs-update-github-releases
|
||||||
|
nixpkgs-update-pypi-releases
|
||||||
|
;
|
||||||
|
})
|
||||||
|
./build02/configuration.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
./build03/configuration.nix
|
"build03.nix-community.org" = nixosSystem {
|
||||||
];
|
system = "x86_64-linux";
|
||||||
};
|
modules =
|
||||||
|
common
|
||||||
|
++ [
|
||||||
|
(import ./services/hydra {
|
||||||
|
inherit (self.inputs) hydra;
|
||||||
|
})
|
||||||
|
|
||||||
"build04.nix-community.org" = nixpkgs.lib.nixosSystem {
|
./build03/configuration.nix
|
||||||
system = "aarch64-linux";
|
];
|
||||||
modules = common ++ [
|
};
|
||||||
./build04/configuration.nix
|
|
||||||
];
|
"build04.nix-community.org" = nixosSystem {
|
||||||
};
|
system = "aarch64-linux";
|
||||||
};
|
modules =
|
||||||
};
|
common
|
||||||
|
++ [
|
||||||
|
./build04/configuration.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.config
|
||||||
|
.flake;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
{ pkgs ? import <nixpkgs> {}
|
{ pkgs ? import <nixpkgs> {}
|
||||||
, sops-import-keys-hook
|
, sops-import-keys-hook
|
||||||
|
, deploykit
|
||||||
}:
|
}:
|
||||||
|
|
||||||
with pkgs;
|
with pkgs;
|
||||||
mkShell {
|
mkShellNoCC {
|
||||||
sopsPGPKeyDirs = [
|
sopsPGPKeyDirs = [
|
||||||
"./keys"
|
"./keys"
|
||||||
];
|
];
|
||||||
|
@ -23,5 +24,6 @@ mkShell {
|
||||||
rsync
|
rsync
|
||||||
|
|
||||||
sops-import-keys-hook
|
sops-import-keys-hook
|
||||||
|
deploykit
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
2
tasks.py
2
tasks.py
|
@ -4,7 +4,7 @@ from invoke import task
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from typing import List, Any
|
from typing import List, Any
|
||||||
from deploy_nixos import DeployHost, DeployGroup
|
from deploykit import DeployHost, DeployGroup
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue