From a20bab661eea84f912d5fdeef40cd92d8b45816f Mon Sep 17 00:00:00 2001 From: Toboshii Nakama <63410334+toboshii@users.noreply.github.com> Date: Thu, 21 Jul 2022 19:28:15 -0500 Subject: [PATCH] feat: add xz fun --- scripts/ctl.mjs | 45 ++++++++++++ scripts/lib/Snapshot.class.mjs | 52 ++++++++++++++ scripts/lib/Talos.class.mjs | 122 +++++++++++++++++++++++++++++++++ scripts/package-lock.json | 44 ++++++++++++ scripts/package.json | 17 +++++ 5 files changed, 280 insertions(+) create mode 100755 scripts/ctl.mjs create mode 100644 scripts/lib/Snapshot.class.mjs create mode 100644 scripts/lib/Talos.class.mjs create mode 100644 scripts/package-lock.json create mode 100644 scripts/package.json diff --git a/scripts/ctl.mjs b/scripts/ctl.mjs new file mode 100755 index 00000000..364372c4 --- /dev/null +++ b/scripts/ctl.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env zx + +// Usage: +// ctl.mjs snapshot list --app whisparr --namespace default +// ctl.mjs talos prepare --user --pass --nodes k8s-control01 --reset +// ctl.mjs talos install --nodes k8s-control01 --bootstrap-node k8s-control01 +// ctl.mjs talos upgrade --nodes k8s-control01,k8s-control02,k8s-control03 +import { Snapshot } from './lib/Snapshot.class.mjs'; +import { Talos } from './lib/Talos.class.mjs'; + +$.verbose = false + +const COMMAND = argv["_"][0] +const ARG = argv["_"][1] +const DEBUG = argv["debug"] || false +const HELP = argv["help"] || false + +if (DEBUG) { $.verbose = true } +switch(COMMAND) { + case "snapshot": + const snapshot = new Snapshot(DEBUG, HELP) + switch(ARG) { + case "list": + await snapshot.List() + break; + case "create": + await snapshot.Create() + break; + default: + console.log(`404: ${ARG} arg not found`) + } + break; + case "talos": + const talos = new Talos(DEBUG, HELP) + switch(ARG) { + case "prepare": + await talos.Prepare() + break; + default: + console.log(`404: ${ARG} arg not found`) + } + break; + default: + console.log(`404: ${COMMAND} command not found`) +} diff --git a/scripts/lib/Snapshot.class.mjs b/scripts/lib/Snapshot.class.mjs new file mode 100644 index 00000000..92da04ad --- /dev/null +++ b/scripts/lib/Snapshot.class.mjs @@ -0,0 +1,52 @@ +// const PROJECT_ROOT = process.env.PWD; +// const GITHUB_TOKEN = process.env.TOKEN + +// const APP = argv["app"] || argv["a"] || process.env.APP +// const NAMESPACE = argv["namespace"] || argv["n"] || process.env.NAMESPACE + +class Snapshot { + + constructor(debug = false, help = false) { + this.debug = debug + this.help = help + this.app = argv["app"] || argv["a"] || process.env.APP + this.namespace = argv["namespace"] || argv["n"] || process.env.NAMESPACE + this.kopiaApp = argv["kopia-app"] || process.env.KOPIA_APP || "kopia" + this.kopiaNamespace = argv["kopia-namespace"] || process.env.KOPIA_NAMESPACE || "default" + + if (this.debug) { + $.verbose = true + } + } + + async List() { + if (this.help) { + console.log(`Usage: ctl snapshot list --app --namespace --kopia-app --kopia-namespace `) + process.exit(0); + } + if (!this.app) { throw new Error("Argument --app, -a or env APP not set") } + if (!this.namespace) { throw new Error("Argument --namespace, -n or env NAMESPACE not set") } + const snapshots = await $`kubectl -n ${this.kopiaNamespace} exec -it deployment/${this.kopiaApp} -- kopia snapshot list /data/${this.namespace}/${this.app} --json` + let structData = [] + for (const obj of JSON.parse(snapshots.stdout)) { + const latest = obj.retentionReason.includes("latest-1") + structData.push({ "snapshot id": obj.id, "date created": obj.startTime, latest: latest }) + } + console.table(structData); + } + + async Create() { + if (this.help) { + console.log(`Usage: ctl snapshot create --app --namespace `) + process.exit(0); + } + const jobRaw = await $`kubectl -n ${this.namespace} create job --from=cronjob/${this.app}-snapshot ${this.app}-snapshot-${+new Date} --dry-run=client --output json` + const jobJson = JSON.parse(jobRaw.stdout) + delete jobJson.spec.template.spec.initContainers + const jobYaml = new YAML.Document(); + jobYaml.contents = jobJson; + await $`echo ${jobYaml.toString()}`.pipe($`kubectl apply -f -`) + } +} + +export { Snapshot } diff --git a/scripts/lib/Talos.class.mjs b/scripts/lib/Talos.class.mjs new file mode 100644 index 00000000..4328b765 --- /dev/null +++ b/scripts/lib/Talos.class.mjs @@ -0,0 +1,122 @@ +class Talos { + + constructor(debug = false, help = false) { + this.debug = debug + this.help = help + this.kvmHost = argv["host"] || argv["h"] || process.env.KVM_HOST + this.kvmUsername = argv["username"] || argv["u"] || process.env.KVM_USERNAME + this.kvmPassword = argv["password"] || argv["p"] || process.env.KVM_PASSWORD + this.nodes = argv["nodes"] || argv["n"] || process.env.TALOS_NODES + this.reset = argv["reset"] || argv["r"] || process.env.TALOS_RESET + this.image = argv["image"] || argv["i"] || process.env.TALOS_IMAGE + + if (this.debug) { + $.verbose = true + } + } + + async Prepare() { + if (this.help) { + console.log(`Usage: ctl talos prepare --username --password --nodes --image --reset`) + process.exit(0); + } + if (!this.kvmHost) { throw new Error("Argument --host, -h or env KVM_HOST not set") } + if (!this.kvmUsername) { throw new Error("Argument --username, -u or env KVM_USERNAME not set") } + if (!this.kvmPassword) { throw new Error("Argument --password, -p or env KVM_PASSWORD not set") } + if (!this.nodes) { throw new Error("Argument --nodes, -n or env TALOS_NODES not set") } + if (!this.image) { this.image = `https://github.com/siderolabs/talos/releases/latest/download/talos-amd64.iso` } + + const crypto = require('crypto') + const hash = crypto.randomBytes(4).toString('hex') + const imageName = `talos-${hash}.iso` + + let headers = { "X-KVMD-User": this.kvmUsername, "X-KVMD-Passwd": this.kvmPassword } + + console.log(`Attaching ${this.image} to piKVM virtual CD-ROM`) + + // ensure drive is detached to prevent `MsdConnectedError` + await this.attachDrive(headers, false) + + await this.uploadImage(headers, imageName) + await this.selectImage(headers, imageName) + await this.attachDrive(headers, true) + + console.log(`Rebooting machine into Talos installer`) + // @TODO: add support for running `talosctl reset` and ceph wipe if(--reset) + // @TODO: add dry-run support - comment out for now + //await this.sendReboot(headers) + + console.log(`${chalk.green.bold('Success:')} You can now push a machine config to ${this.nodes}`) + + // @TODO: ping host and wait for Talos apid at 50000/tcp + + + console.log(`Disconnecting virtual CD-ROM from piKVM and cleaning up`) + await this.attachDrive(headers, false) + await this.deleteImage(headers, imageName) + } + + // Upload provided or latest image to piKVM + async uploadImage(headers, imageName) { + const response = await fetch(`https://${this.kvmHost}/api/msd/write_remote?url=${this.image}&image=${imageName}&timeout=60`, { method: 'POST', headers }) + if (!response.ok) { + const json = await response.json() + throw new Error(`${json.result.error} - ${json.result.error_msg}`) + } + return await response.text() + } + + // Select active ISO image for piKVM virtual CD-ROM + async selectImage(headers, imageName) { + const response = await fetch(`https://${this.kvmHost}/api/msd/set_params?image=${imageName}&cdrom=1`, { method: 'POST', headers }) + if (!response.ok) { + const json = await response.json() + throw new Error(`${json.result.error} - ${json.result.error_msg}`) + } + return await response.json() + } + + // Delete ISO image from piKVM + async deleteImage(headers, imageName) { + const response = await fetch(`https://${this.kvmHost}/api/msd/remove?image=${imageName}`, { method: 'POST', headers }) + if (!response.ok) { + const json = await response.json() + throw new Error(`${json.result.error} - ${json.result.error_msg}`) + } + return await response.json() + } + + // Attach piKVM virtual CD-ROM to server + async attachDrive(headers, attach) { + const response = await fetch(`https://${this.kvmHost}/api/msd/set_connected?connected=${attach ? 1 : 0}`, { method: 'POST', headers }) + if (!response.ok) { + const json = await response.json() + if (json.result.error == 'MsdDisconnectedError' && attach === false) { + // Ignore errors caused by detaching a detached drive + return json + } + throw new Error(`${json.result.error} - ${json.result.error_msg}`) + } + return await response.json() + } + + // Send CTRL-ALT-DEL to piKVM + async sendReboot(headers) { + await Promise.all([ + fetch(`https://${this.kvmHost}/api/hid/events/send_key?key=ControlLeft&state=true`, { method: 'POST', headers }), + fetch(`https://${this.kvmHost}/api/hid/events/send_key?key=AltLeft&state=true`, { method: 'POST', headers }), + fetch(`https://${this.kvmHost}/api/hid/events/send_key?key=Delete&state=true`, { method: 'POST', headers }), + ]) + + await new Promise(r => setTimeout(r, 500)); + + await Promise.all([ + fetch(`https://${this.kvmHost}/api/hid/events/send_key?key=ControlLeft&state=false`, { method: 'POST', headers }), + fetch(`https://${this.kvmHost}/api/hid/events/send_key?key=AltLeft&state=false`, { method: 'POST', headers }), + fetch(`https://${this.kvmHost}/api/hid/events/send_key?key=Delete&state=false`, { method: 'POST', headers }), + ]) + } + +} + +export { Talos } diff --git a/scripts/package-lock.json b/scripts/package-lock.json new file mode 100644 index 00000000..92aa41a9 --- /dev/null +++ b/scripts/package-lock.json @@ -0,0 +1,44 @@ +{ + "name": "scripts", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "scripts", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/ws": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", + "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + }, + "dependencies": { + "ws": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", + "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", + "requires": {} + } + } +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 00000000..0e470744 --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,17 @@ +{ + "name": "scripts", + "version": "1.0.0", + "description": "", + "main": "ctl.mjs", + "directories": { + "lib": "lib" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "ws": "^8.8.1" + } +}