feat: add xz fun

This commit is contained in:
Toboshii Nakama
2022-07-21 19:28:15 -05:00
parent 7611fef478
commit a20bab661e
5 changed files with 280 additions and 0 deletions

45
scripts/ctl.mjs Executable file
View File

@@ -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`)
}

View File

@@ -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 <app> --namespace <namespace> --kopia-app <kopia-app> --kopia-namespace <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 <app> --namespace <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 }

122
scripts/lib/Talos.class.mjs Normal file
View File

@@ -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 <piKVM user> --password <piKVM password> --nodes <comma-delimited hostnames/ips> --image <url to Talos ISO> --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 }

44
scripts/package-lock.json generated Normal file
View File

@@ -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": {}
}
}
}

17
scripts/package.json Normal file
View File

@@ -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"
}
}