kasm-install-wizard/index.js
Richard Koliser 0774b59deb
KASM-1190 Remove legacy GPU seeding in favour of 1.19.0 agent management
In 1.19.0, the provision agent dynamically assigns GPUs to containers
using capability probing (EGL, DRI3, VAAPI, NVENC, VULKAN) and
load-balancing across devices. Pre-seeding GPU run_config/exec_config
into workspace images at install time conflicts with this system and
uses removed env var names (HW3D/DRINODE replaced by
KVNC_DESKTOP_GPU_HW3D/KVNC_DESKTOP_GPU_DRINODE).

Remove setGpu(), the gpuinfo.sh detection block, the GPU yaml-merge
call in install(), and the "Use GPU on all images" install UI option.
Replace the UI option with a notice directing admins to configure GPU
acceleration in the admin panel post-install.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-17 14:16:12 -04:00

252 lines
8.8 KiB
JavaScript

// Imports
const Docker = require('dockerode');
const socketIO = require('socket.io');
const pty = require("node-pty");
const fsw = require('fs').promises;
const fs = require('fs');
const os = require('os');
const yaml = require('js-yaml');
const _ = require('lodash');
const si = require('systeminformation');
const express = require('express');
const app = require('express')();
const privateKey = fs.readFileSync('/opt/kasm/certs/kasm_wizard.key', 'utf8');
const certificate = fs.readFileSync('/opt/kasm/certs/kasm_wizard.crt', 'utf8');
const credentials = {key: privateKey, cert: certificate};
const https = require('https').Server(credentials, app);
const baserouter = express.Router();
const docker = new Docker({socketPath: '/var/run/docker.sock'});
const arch = os.arch().replace('x64', 'amd64');
const baseUrl = process.env.SUBFOLDER || '/';
const port = process.env.KASM_PORT || '443';
let EULA;
let images;
let currentVersion;
let sourceLocal = false;
let gpuInfo = {};
let installSettings = {};
let upgradeSettings = {};
// Find the best matching compatibility entry for the current version.
// Exact version match takes precedence over wildcard (e.g. "1.18.x").
function matchVersion(currentVer, compatibilityList) {
if (!compatibilityList || compatibilityList.length === 0) return null;
const parts = currentVer.split('.');
const major = parts[0];
const minor = parts[1] !== undefined ? parts[1] : '0';
// Exact match (most precise)
let match = compatibilityList.find(c => c.version === currentVer);
if (match) return match;
// Major.minor wildcard, e.g. "1.18.x"
match = compatibilityList.find(c => c.version === `${major}.${minor}.x`);
if (match) return match;
return null;
}
// Build the image tag for a given compat entry.
// Returns e.g. "1.19.0-rolling-weekly", falling back to "develop".
function buildImageTag(compat) {
const compatTag = compat.image.split(':')[1] || '';
const versionPrefix = compatTag.replace(/-[^-]+-[^-]+$/, ''); // strip last two dash-segments
const candidate = versionPrefix + '-rolling-weekly';
return (compat.available_tags && compat.available_tags.includes(candidate)) ? candidate : 'develop';
}
// Fetch the registry workspace list, with local fallback.
async function fetchListData() {
try {
const response = await fetch('https://registry.kasmweb.com/1.1/list.json');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (err) {
console.error('Failed to fetch workspace list, falling back to local list.json:', err.message);
sourceLocal = true;
return JSON.parse(await fsw.readFile('/wizard/list.json', 'utf8'));
}
}
// Return the rolling-weekly tag string for the current version by finding
// the first compatible workspace in the registry (e.g. "1.19.0-rolling-weekly").
async function getRollingWeeklyTag(currentVer, archName) {
const listData = await fetchListData();
const ws = (listData.workspaces || []).find(
w => w.architecture && w.architecture.includes(archName) && matchVersion(currentVer, w.compatibility || [])
);
if (!ws) return currentVer + '-rolling-weekly';
return buildImageTag(matchVersion(currentVer, ws.compatibility));
}
// Fetch workspace list from registry and convert to the images YAML structure.
// Falls back to local list.json if the remote is unavailable.
async function fetchWorkspaceList(currentVer, archName) {
const listData = await fetchListData();
const filtered = (listData.workspaces || []).filter(
ws => ws.architecture && ws.architecture.includes(archName)
);
let imageIdx = 1;
const imagesList = [];
for (const ws of filtered) {
const compat = matchVersion(currentVer, ws.compatibility || []);
if (!compat) continue;
const imageBase = compat.image.split(':')[0];
const imageTag = buildImageTag(compat);
const imageName = imageBase + ':' + imageTag;
imagesList.push({
categories: ws.categories,
cores: 2.0,
cpu_allocation_method: 'Inherit',
description: ws.description,
docker_registry: ws.docker_registry,
enabled: true,
exec_config: {},
friendly_name: ws.friendly_name,
gpu_count: 0,
hidden: false,
image_id: '${uuid:image_id:' + imageIdx + '}',
image_src: 'https://registry.kasmweb.com/1.1/icons/' + ws.image_src,
image_type: 'Container',
launch_config: {},
memory: 2768000000,
name: imageName,
notes: ws.notes || null,
run_config: {},
uncompressed_size_mb: compat.uncompressed_size_mb,
zone_id: null
});
imageIdx++;
}
let alembicVersion = 'e3900d8a4fee';
try {
const propsText = await fsw.readFile('/kasm_release/conf/database/seed_data/default_properties.yaml', 'utf8');
const props = yaml.load(propsText);
if (props && props.alembic_version) alembicVersion = props.alembic_version;
} catch (err) {
console.error('Could not read default_properties.yaml, using hardcoded alembic_version:', err.message);
}
return { alembic_version: alembicVersion, images: imagesList };
}
// Grab installer variables
async function installerBlobs() {
EULA = await fsw.readFile('/kasm_release/licenses/LICENSE.txt', 'utf8');
try {
currentVersion = fs.readFileSync('/version.txt', 'utf8').replace(/(\r\n|\n|\r)/gm,'');
} catch (err) {
currentVersion = '1.19.0';
}
images = await fetchWorkspaceList(currentVersion, arch);
}
installerBlobs();
//// Http server ////
baserouter.use('/public', express.static(__dirname + '/public'));
baserouter.get("/", function (req, res) {
res.sendFile(__dirname + '/public/index.html');
});
baserouter.get("/favicon.ico", function (req, res) {
res.sendFile(__dirname + '/public/favicon.ico');
});
app.use(baseUrl, baserouter);
https.listen(3000);
//// socketIO comms ////
const io = socketIO(https, {path: baseUrl + 'socket.io'});
io.on('connection', async function (socket) {
// Run bash install with our custom flags
async function install(data) {
// Determine install settings
installSettings = data[0];
let imagesI = data[1];
let installFlags = ['/kasm_release/install.sh', '-W', '-B' ,'-H', '-e', '-L', port, '-P', installSettings.adminPass, '-U', installSettings.userPass];
if (imagesI && typeof imagesI === 'object' && Array.isArray(imagesI.images) && imagesI.images.length < 10) {
installFlags.push('-b');
}
// Write finalized image data
let yamlStr = yaml.dump(imagesI);
if (yamlStr.startsWith("false")) {
installFlags = installFlags.filter(function(e) { return e !== '-W' });
} else {
await fsw.writeFile('/kasm_release/conf/database/seed_data/default_images_' + arch + '.yaml', yamlStr);
}
// Copy over version
await fsw.copyFile('/version.txt', '/opt/version.txt');
// Run install
let cmd = pty.spawn('/bin/bash', installFlags);
cmd.on('data', function(data) {
socket.emit('term', data);
});
cmd.on('exit', function(code, signal) {
if (code == 0) {
socket.emit('done', port);
}
});
}
// Run bash upgrade with our custom flags
async function upgrade(data) {
let upgradeFlags = ['/kasm_release/upgrade.sh', '-L', port];
// Run upgrade
let cmd = pty.spawn('/bin/bash', upgradeFlags);
cmd.on('data', function(data) {
socket.emit('term', data);
});
cmd.on('exit', async function(code, signal) {
if (code == 0) {
await fsw.copyFile('/version.txt', '/opt/version.txt');
const rollingTag = await getRollingWeeklyTag(currentVersion, arch);
socket.emit('done', {port, rollingTag});
}
});
}
// Render landing page depending on installed status
async function renderLanding() {
let containers = await docker.listContainers();
// This is a running system
if (containers.length !== 0) {
let dashinfo = {};
// Get version information
if (fs.existsSync('/opt/version.txt')) {
dashinfo['localVersion'] = fs.readFileSync('/opt/version.txt', 'utf8').replace(/(\r\n|\n|\r)/gm,'');
} else {
dashinfo['localVersion'] = 'Unknown';
}
dashinfo['currentVersion'] = currentVersion;
dashinfo['sourceLocal'] = sourceLocal;
dashinfo['gpuInfo'] = gpuInfo;
dashinfo['containers'] = containers;
dashinfo['cpu'] = await si.cpu();
dashinfo['mem'] = await si.mem();
dashinfo['cpuPercent'] = await si.currentLoad();
dashinfo['port'] = port;
socket.emit('renderdash', [dashinfo, images]);
// Render installer
} else {
socket.emit('renderinstall', [EULA, images, gpuInfo, currentVersion, sourceLocal]);
}
}
// Disable wizard
async function noWizard() {
await fsw.writeFile('/opt/NO_WIZARD', '');
socket.emit('wizardkilled');
let cmd = pty.spawn('/usr/bin/pkill', ['node']);
}
//// Incoming requests ////
socket.on('renderlanding', renderLanding);
socket.on('install', install);
socket.on('upgrade', upgrade);
socket.on('nowizard', noWizard);
});