2023-01-26 19:24:29 +01:00
|
|
|
import { ssh, SSH } from './ssh.js'
|
2023-04-23 10:32:27 +02:00
|
|
|
import { Expression } from './nix.js'
|
2023-01-26 19:24:29 +01:00
|
|
|
|
2023-04-23 12:50:31 +02:00
|
|
|
type Dictionary<T> = Record<string, T>
|
2023-04-23 10:32:27 +02:00
|
|
|
type Command = () => Promise<void>
|
2023-04-23 12:50:31 +02:00
|
|
|
type Setter = (props: Dictionary<string | boolean | number>) => void
|
2023-01-26 19:24:29 +01:00
|
|
|
class Cmd {
|
2023-04-23 12:50:31 +02:00
|
|
|
registry: Dictionary<Command> = {};
|
|
|
|
settings: Dictionary<Setter> = {};
|
2023-04-23 10:32:27 +02:00
|
|
|
register(obj: string, action: string, fn: Command) {
|
2023-01-26 19:24:29 +01:00
|
|
|
this.registry[`${obj}.${action}`] = fn
|
|
|
|
}
|
2023-04-23 12:50:31 +02:00
|
|
|
registerAll(obj: string, intf: Object & { _commands?: string[], set?: Setter }) {
|
2023-03-01 11:40:56 +01:00
|
|
|
if (!intf._commands) return
|
2023-04-23 12:50:31 +02:00
|
|
|
if (intf.set) this.settings[obj] = intf.set.bind(intf)
|
2023-03-01 11:40:56 +01:00
|
|
|
const proto = Object.getPrototypeOf(intf)
|
|
|
|
for (const name of intf._commands) {
|
|
|
|
if (name != "constructor" && typeof proto[name] === "function") {
|
|
|
|
this.register(obj, name, proto[name].bind(intf))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-01-26 19:24:29 +01:00
|
|
|
async run() {
|
|
|
|
const opt = argv._[0]
|
2023-04-23 12:50:31 +02:00
|
|
|
for (const k of Object.keys(argv)) {
|
|
|
|
if (k === "_") continue
|
|
|
|
if (this.settings[k]) {
|
|
|
|
this.settings[k](argv[k])
|
|
|
|
} else {
|
|
|
|
throw new Error("unknown object " + k)
|
|
|
|
}
|
|
|
|
}
|
2023-04-03 15:51:22 +02:00
|
|
|
if (opt == "__autocompletes") {
|
|
|
|
for (const k of Object.keys(this.registry)) {
|
|
|
|
console.log(k)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2023-01-26 19:24:29 +01:00
|
|
|
if (opt && this.registry[opt]) await this.registry[opt]()
|
|
|
|
else {
|
|
|
|
console.log("Possible options: \n")
|
|
|
|
for (const k of Object.keys(this.registry)) {
|
|
|
|
console.log("-", k)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const action = new Cmd()
|
|
|
|
|
|
|
|
async function ping(hostname: string) {
|
|
|
|
return await $`ping -Anqc3 ${hostname}`.exitCode == 0
|
|
|
|
}
|
|
|
|
|
|
|
|
class Machine {
|
|
|
|
name: string;
|
|
|
|
hasHome: boolean = false;
|
|
|
|
hostname?: string;
|
2023-04-23 10:32:27 +02:00
|
|
|
constructor({name = "", hasHome = false, hostname}: {name?: string, hasHome?: boolean, hostname?: string}) {
|
|
|
|
this.name = name
|
|
|
|
this.hasHome = hasHome
|
|
|
|
this.hostname = hostname
|
|
|
|
// name can be set later
|
2023-01-26 19:24:29 +01:00
|
|
|
}
|
|
|
|
isLocal() {
|
|
|
|
return os.hostname() == this.name
|
|
|
|
}
|
|
|
|
get attr(): Expression {
|
|
|
|
return new Expression(`.#nixosConfigurations.${this.name}.toplevel`)
|
|
|
|
}
|
|
|
|
private async sshTarget() {
|
|
|
|
if (this.isLocal()) return "localhost"
|
|
|
|
if (this.hostname && await ping(this.hostname)) {
|
|
|
|
return this.hostname
|
|
|
|
} else {
|
|
|
|
// todo: directify
|
2023-04-23 12:50:31 +02:00
|
|
|
return `${this.name}.home.yori.cc`
|
2023-01-26 19:24:29 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
async findIP(): Promise<typeof Machine.prototype.ssh> {
|
|
|
|
const host = await this.sshTarget()
|
|
|
|
return <R>(user?: string, cb?: () => Promise<R>): Promise<SSH|R> => {
|
|
|
|
const sshTarget = user ? `${user}@${host}` : host
|
2023-03-01 11:40:56 +01:00
|
|
|
if (cb !== undefined) return ssh(sshTarget, cb)
|
|
|
|
return ssh(sshTarget)
|
2023-01-26 19:24:29 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ssh(user?: string): Promise<SSH>
|
|
|
|
ssh<R>(user?: string, cb?: () => Promise<R>): Promise<R>
|
|
|
|
async ssh<R>(user?: string, cb?: () => Promise<R>) {
|
|
|
|
return (await this.findIP())(user, cb)
|
|
|
|
}
|
|
|
|
get targets() {
|
|
|
|
if (this.hasHome) return [this.attr, Home]
|
|
|
|
else return [this.attr]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const machines = {
|
|
|
|
frumar: new Machine({hostname: "frumar.home.yori.cc"}),
|
|
|
|
pennyworth: new Machine({hostname: "pennyworth.yori.cc"}),
|
|
|
|
blackadder: new Machine({hasHome: true}),
|
|
|
|
jarvis: new Machine({hasHome: true}),
|
|
|
|
smithers: new Machine({hasHome: true}),
|
|
|
|
}
|
|
|
|
for (const [name, machine] of Object.entries(machines))
|
|
|
|
machine.name = name
|
|
|
|
|
2023-04-23 10:32:27 +02:00
|
|
|
function cmd(target: { _commands?: string[] }, propertyKey: string, _descriptor: PropertyDescriptor): void {
|
2023-03-01 11:40:56 +01:00
|
|
|
if (target._commands == undefined) target._commands = []
|
|
|
|
target._commands.push(propertyKey)
|
|
|
|
}
|
2023-01-26 19:24:29 +01:00
|
|
|
|
2023-03-01 11:40:56 +01:00
|
|
|
class MachineInterface {
|
|
|
|
_commands?: string[]
|
2023-04-23 09:43:11 +02:00
|
|
|
constructor(public machine: Machine) {
|
2023-04-03 15:51:22 +02:00
|
|
|
// hack:
|
|
|
|
delete this._commands
|
2023-03-01 11:40:56 +01:00
|
|
|
}
|
2023-04-23 12:50:31 +02:00
|
|
|
set(props: Dictionary<string | boolean | number>) {
|
|
|
|
console.log("setting", props)
|
|
|
|
Object.assign(this.machine, props)
|
|
|
|
}
|
2023-03-01 11:40:56 +01:00
|
|
|
@cmd
|
|
|
|
async ssh() {
|
|
|
|
(await this.machine.ssh()).interactive()
|
|
|
|
}
|
|
|
|
@cmd
|
|
|
|
gc() {
|
|
|
|
return this.machine.ssh("root", () => $`nix-collect-garbage -d`)
|
|
|
|
}
|
|
|
|
@cmd
|
|
|
|
eval() {
|
|
|
|
return Promise.all(this.machine.targets.map(x => x.derive()))
|
|
|
|
}
|
|
|
|
@cmd
|
|
|
|
build() {
|
|
|
|
return Promise.all(this.machine.targets.map(x => x.build()))
|
|
|
|
}
|
|
|
|
@cmd
|
|
|
|
status() {
|
|
|
|
return this.machine.ssh(undefined, async () => {
|
|
|
|
await $`systemctl is-system-running`
|
|
|
|
await $`zpool status -x`
|
|
|
|
await $`realpath /run/current-system /nix/var/nix/profiles/system`
|
2023-01-26 19:24:29 +01:00
|
|
|
})
|
2023-03-01 11:40:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@cmd
|
|
|
|
async copy() {
|
|
|
|
const machineSSH = await this.machine.findIP()
|
|
|
|
const outputs = (await Promise.all(this.machine.targets.map(x => x.build().then(Object.values)))).flat()
|
|
|
|
if (this.machine.isLocal()) {
|
2023-01-26 19:24:29 +01:00
|
|
|
console.log("skipping copy, is localhost")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const conn = await machineSSH()
|
2023-03-01 11:40:56 +01:00
|
|
|
await Promise.all(Object.values(outputs).map(x => x.copy(conn)))
|
2023-01-26 19:24:29 +01:00
|
|
|
// machine.toplevel.buildAndCopy(machine.ssh)
|
|
|
|
// Home.buildAndCopy(machine.ssh)
|
2023-03-01 11:40:56 +01:00
|
|
|
}
|
|
|
|
@cmd
|
|
|
|
async "boot-deploy"() {
|
|
|
|
const machineSSH = await this.machine.findIP()
|
|
|
|
const path = (await this.machine.attr.build()).out
|
|
|
|
if (!this.machine.isLocal()) {
|
2023-01-26 19:24:29 +01:00
|
|
|
const conn = await machineSSH()
|
|
|
|
await path.copy(conn)
|
|
|
|
}
|
|
|
|
// machine.toplevel.buildAndCopy(machine.ssh)
|
|
|
|
// machine.ssh.within(machine.toplevel.activate("boot"))
|
|
|
|
await machineSSH("root", async () => {
|
2023-03-01 11:40:56 +01:00
|
|
|
await $`nix-env -p /nix/var/nix/profiles/system --set ${path.path}`
|
2023-04-23 09:42:35 +02:00
|
|
|
await $`${path.path as string}/bin/switch-to-configuration boot`
|
2023-01-26 19:24:29 +01:00
|
|
|
})
|
2023-03-01 11:40:56 +01:00
|
|
|
}
|
|
|
|
@cmd
|
|
|
|
async "switch"() {
|
|
|
|
const machineSSH = await this.machine.findIP()
|
|
|
|
const path = (await this.machine.attr.build()).out
|
|
|
|
if (!this.machine.isLocal()) {
|
2023-01-26 19:24:29 +01:00
|
|
|
const conn = await machineSSH()
|
|
|
|
await path.copy(conn)
|
|
|
|
}
|
|
|
|
const new_kernel = (await $`readlink ${path.path}/kernel`).stdout
|
|
|
|
await machineSSH("root", async () => {
|
|
|
|
const old_kernel = (await $`readlink /run/booted-system/kernel`).stdout
|
|
|
|
if (new_kernel !== old_kernel) {
|
2023-03-01 11:40:56 +01:00
|
|
|
console.error(`[${this.machine.name}] requires reboot because of kernel update`)
|
2023-01-26 19:24:29 +01:00
|
|
|
process.exit(1)
|
|
|
|
}
|
|
|
|
await $`nix-env -p /nix/var/nix/profiles/system --set ${path.path}`
|
2023-03-01 11:40:56 +01:00
|
|
|
await $`${path.path}/bin/switch-to-configuration switch`
|
2023-01-26 19:24:29 +01:00
|
|
|
})
|
|
|
|
}
|
2023-03-01 11:40:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
class MachineInterfaceHome extends MachineInterface {
|
|
|
|
@cmd
|
|
|
|
async "update-home"() {
|
|
|
|
const new_path = (await Home.build()).out
|
|
|
|
if (this.machine.isLocal()) {
|
|
|
|
await $`${new_path.path}/activate`
|
|
|
|
} else {
|
|
|
|
const conn = await this.machine.ssh()
|
|
|
|
await new_path.copy(conn)
|
|
|
|
await conn.within(() => $`${new_path.path}/activate`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const machine of Object.values(machines)) {
|
|
|
|
action.registerAll(machine.name, machine.hasHome ? new MachineInterfaceHome(machine) : new MachineInterface(machine))
|
2023-01-26 19:24:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
action.register("all", "build", async () => {
|
|
|
|
console.log(await (new Expression(".#")).build())
|
|
|
|
})
|
|
|
|
|
|
|
|
const Home = new Expression(".#yorick-home")
|
|
|
|
|
|
|
|
action.register("home", "build", async () => {
|
|
|
|
console.log(await Home.build())
|
|
|
|
})
|
|
|
|
|
|
|
|
action.register("home", "eval", async () => {
|
|
|
|
console.log(await Home.derive())
|
|
|
|
})
|
|
|
|
|
|
|
|
action.register("local", "boot-deploy", async () => {
|
|
|
|
await action.registry[`${os.hostname()}.boot-deploy`]()
|
|
|
|
})
|
|
|
|
|
|
|
|
action.register("local", "status", async () => {
|
|
|
|
await action.registry[`${os.hostname()}.status`]()
|
|
|
|
})
|
|
|
|
|
|
|
|
await action.run()
|