UNPKG

@underpostnet/underpost

Version:
396 lines (372 loc) 18 kB
import { getNpmRootPath } from '../server/conf.js'; import { getLocalIPv4Address } from '../server/dns.js'; import { pbcopy, shellExec } from '../server/process.js'; import fs from 'fs-extra'; /** * @class UnderpostLxd * @description Provides a set of static methods to interact with LXD, * encapsulating common LXD operations for VM management and network testing. */ class UnderpostLxd { static API = { /** * @method callback * @description Main entry point for LXD operations based on provided options. * @param {object} options - Configuration options for LXD operations. * @param {boolean} [options.init=false] - Initialize LXD. * @param {boolean} [options.reset=false] - Reset LXD installation. * @param {boolean} [options.dev=false] - Run in development mode (adjusts paths). * @param {boolean} [options.install=false] - Install LXD snap. * @param {boolean} [options.createVirtualNetwork=false] - Create default LXD bridge network (lxdbr0). * @param {boolean} [options.createAdminProfile=false] - Create admin-profile for VMs. * @param {boolean} [options.control=false] - Flag for control plane VM initialization. * @param {boolean} [options.worker=false] - Flag for worker node VM initialization. * @param {boolean} [options.k3s=false] - Flag to indicate K3s cluster type for VM initialization. * @param {string} [options.initVm=''] - Initialize a specific VM. * @param {string} [options.createVm=''] - Create a new VM with the given name. * @param {string} [options.infoVm=''] - Display information about a specific VM. * @param {string} [options.rootSize=''] - Root disk size for new VMs (e.g., '32GiB'). * @param {string} [options.joinNode=''] - Join a worker node to a control plane (format: 'workerName,controlName'). * @param {string} [options.expose=''] - Expose ports from a VM to the host (format: 'vmName:port1,port2'). * @param {string} [options.deleteExpose=''] - Delete exposed ports from a VM (format: 'vmName:port1,port2'). * @param {string} [options.test=''] - Test health, status and network connectivity for a VM. * @param {string} [options.autoExposeK8sPorts=''] - Automatically expose common Kubernetes ports for the VM. */ async callback( options = { init: false, reset: false, dev: false, install: false, createVirtualNetwork: false, createAdminProfile: false, control: false, worker: false, k3s: false, initVm: '', createVm: '', infoVm: '', rootSize: '', joinNode: '', expose: '', deleteExpose: '', test: '', autoExposeK8sPorts: '', }, ) { const npmRoot = getNpmRootPath(); const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`; if (options.reset === true) { shellExec(`sudo systemctl stop snap.lxd.daemon || true`); shellExec(`sudo snap remove lxd --purge || true`); } if (options.install === true) shellExec(`sudo snap install lxd`); if (options.init === true) { shellExec(`sudo systemctl start snap.lxd.daemon`); shellExec(`sudo systemctl status snap.lxd.daemon`); const lxdPressedContent = fs .readFileSync(`${underpostRoot}/manifests/lxd/lxd-preseed.yaml`, 'utf8') .replaceAll(`127.0.0.1`, getLocalIPv4Address()); shellExec(`echo "${lxdPressedContent}" | lxd init --preseed`); shellExec(`lxc cluster list`); } if (options.createVirtualNetwork === true) { shellExec(`lxc network create lxdbr0 \ ipv4.address=10.250.250.1/24 \ ipv4.nat=true \ ipv4.dhcp=true \ ipv6.address=none`); } if (options.createAdminProfile === true) { pbcopy(`lxc profile create admin-profile`); shellExec(`cat ${underpostRoot}/manifests/lxd/lxd-admin-profile.yaml | lxc profile edit admin-profile`); shellExec(`lxc profile show admin-profile`); } if (options.createVm && typeof options.createVm === 'string') { pbcopy( `lxc launch images:rockylinux/9 ${ options.createVm } --vm --target lxd-node1 -c limits.cpu=2 -c limits.memory=4GB --profile admin-profile -d root,size=${ options.rootSize && typeof options.rootSize === 'string' ? options.rootSize + 'GiB' : '32GiB' }`, ); } if (options.initVm && typeof options.initVm === 'string') { let flag = ''; if (options.control === true) { if (options.k3s === true) { flag = ' -s -- --k3s'; } else { // Default to kubeadm if not K3s flag = ' -s -- --kubeadm'; } shellExec(`lxc exec ${options.initVm} -- bash -c 'mkdir -p /home/dd/engine'`); shellExec(`lxc file push /home/dd/engine/engine-private ${options.initVm}/home/dd/engine --recursive`); shellExec(`lxc file push /home/dd/engine/manifests ${options.initVm}/home/dd/engine --recursive`); } else if (options.worker == true) { if (options.k3s === true) { flag = ' -s -- --worker --k3s'; } else { // Default to kubeadm worker flag = ' -s -- --worker'; } } console.log(`Executing underpost-setup.sh on VM: ${options.initVm}`); shellExec(`cat ${underpostRoot}/manifests/lxd/underpost-setup.sh | lxc exec ${options.initVm} -- bash${flag}`); console.log(`underpost-setup.sh execution completed on VM: ${options.initVm}`); } // --- Automatic Kubernetes Port Exposure --- if (options.autoExposeK8sPorts && typeof options.autoExposeK8sPorts === 'string') { console.log(`Automatically exposing Kubernetes ports for VM: ${options.autoExposeK8sPorts}`); const vmName = options.autoExposeK8sPorts; const hostIp = getLocalIPv4Address(); let vmIp = ''; let retries = 0; const maxRetries = 10; const delayMs = 5000; // 5 seconds // Wait for VM to get an IP address while (!vmIp && retries < maxRetries) { try { console.log(`Attempting to get IPv4 address for ${vmName} (Attempt ${retries + 1}/${maxRetries})...`); vmIp = shellExec( `lxc list ${vmName} --format json | jq -r '.[0].state.network.enp5s0.addresses[] | select(.family=="inet") | .address'`, { stdout: true }, ).trim(); if (vmIp) { console.log(`IPv4 address found for ${vmName}: ${vmIp}`); } else { console.log(`IPv4 address not yet available for ${vmName}. Retrying in ${delayMs / 1000} seconds...`); await new Promise((resolve) => setTimeout(resolve, delayMs)); } } catch (error) { console.error(`Error getting IPv4 address for exposure: ${error.message}`); console.log(`Retrying in ${delayMs / 1000} seconds...`); await new Promise((resolve) => setTimeout(resolve, delayMs)); } retries++; } if (!vmIp) { console.error(`Failed to get VM IP for ${vmName} after ${maxRetries} attempts. Cannot expose ports.`); return; } let portsToExpose = []; if (options.control === true) { // Kubernetes API Server (Kubeadm and K3s both use 6443 by default) portsToExpose.push('6443'); // Standard HTTP/HTTPS for Ingress if deployed portsToExpose.push('80'); portsToExpose.push('443'); } // Add common NodePorts if needed, or rely on explicit 'expose' portsToExpose.push('30000'); // Example NodePort portsToExpose.push('30001'); // Example NodePort portsToExpose.push('30002'); // Example NodePort const protocols = ['tcp']; // Most K8s services are TCP, UDP for some like DNS for (const port of portsToExpose) { for (const protocol of protocols) { const deviceName = `${vmName}-${protocol}-port-${port}`; try { // Remove existing device first to avoid conflicts if re-running shellExec(`lxc config device remove ${vmName} ${deviceName} || true`); shellExec( `lxc config device add ${vmName} ${deviceName} proxy listen=${protocol}:${hostIp}:${port} connect=${protocol}:${vmIp}:${port} nat=true`, ); console.log(`Exposed ${protocol}:${hostIp}:${port} -> ${vmIp}:${port} for ${vmName}`); } catch (error) { console.error(`Failed to expose port ${port} for ${vmName}: ${error.message}`); } } } } if (options.joinNode && typeof options.joinNode === 'string') { const [workerNode, controlNode] = options.joinNode.split(','); // Determine if it's a Kubeadm or K3s join const isK3sJoin = options.k3s === true; if (isK3sJoin) { console.log(`Attempting to join K3s worker node ${workerNode} to control plane ${controlNode}`); // Get K3s token from control plane const k3sToken = shellExec( `lxc exec ${controlNode} -- bash -c 'sudo cat /var/lib/rancher/k3s/server/node-token'`, { stdout: true }, ).trim(); // Get control plane IP const controlPlaneIp = shellExec( `lxc list ${controlNode} --format json | jq -r '.[0].state.network.enp5s0.addresses[] | select(.family=="inet") | .address'`, { stdout: true }, ).trim(); if (!k3sToken || !controlPlaneIp) { console.error(`Failed to get K3s token or control plane IP. Cannot join worker.`); return; } const k3sJoinCommand = `K3S_URL=https://${controlPlaneIp}:6443 K3S_TOKEN=${k3sToken} curl -sfL https://get.k3s.io | sh -`; shellExec(`lxc exec ${workerNode} -- bash -c '${k3sJoinCommand}'`); console.log(`K3s worker node ${workerNode} join command executed.`); } else { // Kubeadm join console.log(`Attempting to join Kubeadm worker node ${workerNode} to control plane ${controlNode}`); const token = shellExec( `echo "$(lxc exec ${controlNode} -- bash -c 'sudo kubeadm token create --print-join-command')"`, { stdout: true }, ); shellExec(`lxc exec ${workerNode} -- bash -c '${token}'`); console.log(`Kubeadm worker node ${workerNode} join command executed.`); } } if (options.infoVm && typeof options.infoVm === 'string') { shellExec(`lxc config show ${options.infoVm}`); shellExec(`lxc info --show-log ${options.infoVm}`); shellExec(`lxc info ${options.infoVm}`); shellExec(`lxc list ${options.infoVm}`); } if (options.expose && typeof options.expose === 'string') { const [vmName, ports] = options.expose.split(':'); console.log({ vmName, ports }); const protocols = ['tcp']; // udp const hostIp = getLocalIPv4Address(); const vmIp = shellExec( `lxc list ${vmName} --format json | jq -r '.[0].state.network.enp5s0.addresses[] | select(.family=="inet") | .address'`, { stdout: true }, ).trim(); if (!vmIp) { console.error(`Could not get VM IP for ${vmName}. Cannot expose ports.`); return; } for (const port of ports.split(',')) { for (const protocol of protocols) { const deviceName = `${vmName}-${protocol}-port-${port}`; shellExec(`lxc config device remove ${vmName} ${deviceName} || true`); // Use || true to prevent error if device doesn't exist shellExec( `lxc config device add ${vmName} ${deviceName} proxy listen=${protocol}:${hostIp}:${port} connect=${protocol}:${vmIp}:${port} nat=true`, ); console.log(`Manually exposed ${protocol}:${hostIp}:${port} -> ${vmIp}:${port} for ${vmName}`); } } } if (options.deleteExpose && typeof options.deleteExpose === 'string') { const [controlNode, ports] = options.deleteExpose.split(':'); console.log({ controlNode, ports }); const protocols = ['tcp']; // udp for (const port of ports.split(',')) { for (const protocol of protocols) { shellExec(`lxc config device remove ${controlNode} ${controlNode}-${protocol}-port-${port}`); } } } if (options.test && typeof options.test === 'string') { const vmName = options.test; console.log(`Starting comprehensive test for VM: ${vmName}`); // 1. Monitor for IPv4 address let vmIp = ''; let retries = 0; const maxRetries = 10; const delayMs = 5000; // 5 seconds while (!vmIp && retries < maxRetries) { try { console.log(`Attempting to get IPv4 address for ${vmName} (Attempt ${retries + 1}/${maxRetries})...`); vmIp = shellExec( `lxc list ${vmName} --format json | jq -r '.[0].state.network.enp5s0.addresses[] | select(.family=="inet") | .address'`, { stdout: true }, ).trim(); if (vmIp) { console.log(`IPv4 address found for ${vmName}: ${vmIp}`); } else { console.log(`IPv4 address not yet available for ${vmName}. Retrying in ${delayMs / 1000} seconds...`); await new Promise((resolve) => setTimeout(resolve, delayMs)); } } catch (error) { console.error(`Error getting IPv4 address: ${error.message}`); console.log(`Retrying in ${delayMs / 1000} seconds...`); await new Promise((resolve) => setTimeout(resolve, delayMs)); } retries++; } if (!vmIp) { console.error(`Failed to get IPv4 address for ${vmName} after ${maxRetries} attempts. Aborting tests.`); return; } // 2. Iteratively check connection to google.com let connectedToGoogle = false; retries = 0; while (!connectedToGoogle && retries < maxRetries) { try { console.log(`Checking connectivity to google.com from ${vmName} (Attempt ${retries + 1}/${maxRetries})...`); const curlOutput = shellExec( `lxc exec ${vmName} -- bash -c 'curl -s -o /dev/null -w "%{http_code}" http://google.com'`, { stdout: true }, ); if (curlOutput.startsWith('2') || curlOutput.startsWith('3')) { console.log(`Successfully connected to google.com from ${vmName}.`); connectedToGoogle = true; } else { console.log(`Connectivity to google.com not yet verified. Retrying in ${delayMs / 1000} seconds...`); await new Promise((resolve) => setTimeout(resolve, delayMs)); } } catch (error) { console.error(`Error checking connectivity to google.com: ${error.message}`); console.log(`Retrying in ${delayMs / 1000} seconds...`); await new Promise((resolve) => setTimeout(resolve, delayMs)); } retries++; } if (!connectedToGoogle) { console.error( `Failed to connect to google.com from ${vmName} after ${maxRetries} attempts. Aborting further tests.`, ); return; } // 3. Check other connectivity, network, and VM health parameters console.log(`\n--- Comprehensive Health Report for ${vmName} ---`); // VM Status console.log('\n--- VM Status ---'); try { const vmStatus = shellExec(`lxc list ${vmName} --format json`, { stdout: true, silent: true }); console.log(JSON.stringify(JSON.parse(vmStatus), null, 2)); } catch (error) { console.error(`Error getting VM status: ${error.message}`); } // CPU Usage console.log('\n--- CPU Usage ---'); try { const cpuUsage = shellExec(`lxc exec ${vmName} -- bash -c 'top -bn1 | grep "Cpu(s)"'`, { stdout: true }); console.log(cpuUsage.trim()); } catch (error) { console.error(`Error getting CPU usage: ${error.message}`); } // Memory Usage console.log('\n--- Memory Usage ---'); try { const memoryUsage = shellExec(`lxc exec ${vmName} -- bash -c 'free -m'`, { stdout: true }); console.log(memoryUsage.trim()); } catch (error) { console.error(`Error getting memory usage: ${error.message}`); } // Disk Usage console.log('\n--- Disk Usage (Root Partition) ---'); try { const diskUsage = shellExec(`lxc exec ${vmName} -- bash -c 'df -h /'`, { stdout: true }); console.log(diskUsage.trim()); } catch (error) { console.error(`Error getting disk usage: ${error.message}`); } // Network Interface Status console.log('\n--- Network Interface Status (ip a) ---'); try { const ipA = shellExec(`lxc exec ${vmName} -- bash -c 'ip a'`, { stdout: true }); console.log(ipA.trim()); } catch (error) { console.error(`Error getting network interface status: ${error.message}`); } // DNS Resolution (resolv.conf) console.log('\n--- DNS Configuration (/etc/resolv.conf) ---'); try { const resolvConf = shellExec(`lxc exec ${vmName} -- bash -c 'cat /etc/resolv.conf'`, { stdout: true }); console.log(resolvConf.trim()); } catch (error) { console.error(`Error getting DNS configuration: ${error.message}`); } console.log(`\nComprehensive test for VM: ${vmName} completed.`); } }, }; } export default UnderpostLxd;