mirror of
https://github.com/outbackdingo/home-ops.git
synced 2026-01-27 10:19:11 +00:00
feat: add xz fun
This commit is contained in:
45
scripts/ctl.mjs
Executable file
45
scripts/ctl.mjs
Executable 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`)
|
||||
}
|
||||
52
scripts/lib/Snapshot.class.mjs
Normal file
52
scripts/lib/Snapshot.class.mjs
Normal 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
122
scripts/lib/Talos.class.mjs
Normal 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
44
scripts/package-lock.json
generated
Normal 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
17
scripts/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user