KASM-2903 add initial upgrade support

This commit is contained in:
ryan.kuba 2022-11-27 20:27:27 -05:00 committed by Chris Hunt
parent f4356863d3
commit cfaa944c84
No known key found for this signature in database
2 changed files with 213 additions and 44 deletions

162
index.js
View file

@ -23,14 +23,63 @@ var port = process.env.KASM_PORT || '443';
const { spawn } = require('node:child_process');
var EULA;
var images;
var currentVersion;
var gpuInfo;
// Grab installer variables
async function installerBlobs() {
EULA = await fsw.readFile('/kasm_release/licenses/LICENSE.txt', 'utf8');
let imagesText = await fsw.readFile('/wizard/default_images_' + arch + '.yaml', 'utf8');
images = yaml.load(imagesText);
currentVersion = fs.readFileSync('/version.txt', 'utf8').replace(/(\r\n|\n|\r)/gm,'');
let gpuData = [];
let gpuCmd = spawn('/gpuinfo.sh');
gpuCmd.stdout.on('data', function(data) {
gpuData.push(data);
});
gpuCmd.on('close', function(code) {
if (code == 0) {
gpuInfo = JSON.parse(gpuData.join(''));
} else {
gpuInfo = {};
}
});
}
installerBlobs();
// GPU image yaml merging
async function setGpu(imagesD) {
if (upgradeSettings['forceGpu'] !== undefined) {
installSettings = upgradeSettings;
}
let gpu = installSettings.forceGpu.split('|')[0];
let gpuName = installSettings.forceGpu.split('|')[1];
let card = gpu.slice(-1);
let render = (Number(card) + 128).toString();
// Handle NVIDIA Gpus
var baseRun;
if (gpuName.indexOf('NVIDIA') !== -1) {
baseRun = JSON.parse('{"environment":{"KASM_EGL_CARD":"/dev/dri/card' + card + '","KASM_RENDERD":"/dev/dri/renderD' + render + '"},"devices":["/dev/dri/card' + card + ':/dev/dri/card' + card + ':rwm","/dev/dri/renderD' + render + ':/dev/dri/renderD' + render + ':rwm"],"device_requests":[{"driver": "","count": -1,"device_ids": null,"capabilities":[["gpu"]],"options":{}}]}');
} else {
baseRun = JSON.parse('{"environment":{"KASM_EGL_CARD":"/dev/dri/card' + card + '","KASM_RENDERD":"/dev/dri/renderD' + render + '"},"devices":["/dev/dri/card' + card + ':/dev/dri/card' + card + ':rwm","/dev/dri/renderD' + render + ':/dev/dri/renderD' + render + ':rwm"]}');
}
let baseExec = JSON.parse('{"first_launch":{"user":"root","cmd": "bash -c \'chown -R kasm-user:kasm-user /dev/dri/*\'"}}');
for await (let image of Object.keys(images.images)) {
if (imagesD.images[image]['run_config']) {
finalRun = _.merge(JSON.parse(imagesD.images[image]['run_config']), baseRun)
} else {
finalRun = baseRun;
}
if (imagesD.images[image]['exec_config']) {
finalExec = _.merge(JSON.parse(imagesD.images[image]['exec_config']), baseExec)
} else {
finalExec = baseExec;
}
imagesD.images[image]['run_config'] = JSON.stringify(finalRun);
imagesD.images[image]['exec_config'] = JSON.stringify(finalExec);
}
return imagesD;
}
//// Http server ////
baserouter.use('/public', express.static(__dirname + '/public'));
baserouter.get("/", function (req, res) {
@ -47,6 +96,7 @@ 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 imagesD = images;
@ -60,6 +110,7 @@ io.on('connection', async function (socket) {
if ((imagesI.hasOwnProperty('images')) && (Object.keys(imagesI.images).length < 10)) {
installFlags.push('-b');
}
// Flag the images properly based on selection
for await (let image of Object.keys(images.images)) {
if ((imagesI.hasOwnProperty('images')) && (imagesI.images.hasOwnProperty(image))) {
@ -70,36 +121,20 @@ io.on('connection', async function (socket) {
imagesD.images[image].hidden = true;
}
}
// GPU yaml merge
if (installSettings.forceGpu !== 'disabled') {
let gpu = installSettings.forceGpu.split('|')[0];
let gpuName = installSettings.forceGpu.split('|')[1];
let card = gpu.slice(-1);
let render = (Number(card) + 128).toString();
// Handle NVIDIA Gpus
var baseRun;
if (gpuName.indexOf('NVIDIA') !== -1) {
baseRun = JSON.parse('{"environment":{"KASM_EGL_CARD":"/dev/dri/card' + card + '","KASM_RENDERD":"/dev/dri/renderD' + render + '"},"devices":["/dev/dri/card' + card + ':/dev/dri/card' + card + ':rwm","/dev/dri/renderD' + render + ':/dev/dri/renderD' + render + ':rwm"],"device_requests":[{"driver": "","count": -1,"device_ids": null,"capabilities":[["gpu"]],"options":{}}]}');
} else {
baseRun = JSON.parse('{"environment":{"KASM_EGL_CARD":"/dev/dri/card' + card + '","KASM_RENDERD":"/dev/dri/renderD' + render + '"},"devices":["/dev/dri/card' + card + ':/dev/dri/card' + card + ':rwm","/dev/dri/renderD' + render + ':/dev/dri/renderD' + render + ':rwm"]}');
}
let baseExec = JSON.parse('{"first_launch":{"user":"root","cmd": "bash -c \'chown -R kasm-user:kasm-user /dev/dri/*\'"}}');
for await (let image of Object.keys(images.images)) {
if (imagesD.images[image]['run_config']) {
finalRun = _.merge(JSON.parse(imagesD.images[image]['run_config']), baseRun)
} else {
finalRun = baseRun;
}
if (imagesD.images[image]['exec_config']) {
finalExec = _.merge(JSON.parse(imagesD.images[image]['exec_config']), baseExec)
} else {
finalExec = baseExec;
}
imagesD.images[image]['run_config'] = JSON.stringify(finalRun);
imagesD.images[image]['exec_config'] = JSON.stringify(finalExec);
}
imagesD = await setGpu(imagesD);
}
// Write finalized image data
let yamlStr = yaml.dump(imagesD);
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);
@ -110,30 +145,78 @@ io.on('connection', async function (socket) {
}
});
}
// Run bash upgrade with our custom flags
async function upgrade(data) {
// Determine upgrade settings
upgradeSettings = data[0];
let imagesI = data[1];
let imagesD = images;
upgradeFlags = ['/kasm_release/upgrade.sh', '-A', '-L', port];
if (upgradeSettings.keepOldImages == true) {
upgradeFlags.push('-K');
} else {
upgradeFlags.push('-U');
}
// Flag the images properly based on selection
for await (let image of Object.keys(images.images)) {
if ((imagesI.hasOwnProperty('images')) && (imagesI.images.hasOwnProperty(image))) {
imagesD.images[image].enabled = true;
imagesD.images[image].hidden = false;
} else {
imagesD.images[image].enabled = false;
imagesD.images[image].hidden = true;
}
}
// GPU yaml merge
if (upgradeSettings.forceGpu !== 'disabled') {
imagesD = await setGpu(imagesD);
}
// Write finalized image data
let yamlStr = yaml.dump(imagesD);
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 upgrade
let cmd = pty.spawn('/bin/bash', upgradeFlags);
cmd.on('data', function(data) {
socket.emit('term', data);
});
cmd.on('exit', function(code, signal) {
if (code == 0) {
socket.emit('done', port);
}
});
}
// 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['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);
socket.emit('renderdash', [dashinfo, images]);
// Render installer
} else {
let gpuData = [];
let gpuCmd = spawn('/gpuinfo.sh');
gpuCmd.stdout.on('data', function(data) {
gpuData.push(data);
});
gpuCmd.on('close', function(code) {
if (code == 0) {
socket.emit('renderinstall', [EULA, images, JSON.parse(gpuData.join(''))]);
} else {
socket.emit('renderinstall', [EULA, images, {}]);
}
});
socket.emit('renderinstall', [EULA, images, gpuInfo]);
}
}
// Disable wizard
@ -145,5 +228,6 @@ io.on('connection', async function (socket) {
//// Incoming requests ////
socket.on('renderlanding', renderLanding);
socket.on('install', install);
socket.on('upgrade', upgrade);
socket.on('nowizard', noWizard);
});

View file

@ -5,6 +5,7 @@ var gpus;
var term;
var installImages = [];
var installSettings = {};
var upgradeSettings = {};
var selected = false;
// Socket.io connection
@ -40,6 +41,25 @@ async function install() {
}
}
// Execute upgrade
async function upgrade() {
showTerminal()
titleChange('Upgrading');
// Create new object based on image selection
let selectedImages = {images: {}};
if (installImages.length == 0) {
socket.emit('upgrade', [upgradeSettings, false]);
} else {
for await (let image of installImages) {
if (images.images[image].hasOwnProperty('enabled')) {
images.images[image].enabled = true;
}
Object.assign(selectedImages.images, {[image]: images.images[image]});
}
socket.emit('upgrade', [upgradeSettings, selectedImages]);
}
}
// Show page container
function showContainer() {
$('#terminal').empty();
@ -82,15 +102,34 @@ function renderInstall(data) {
}
// Render Dashboard
async function renderDash(info) {
async function renderDash(data) {
showContainer();
titleChange('Dashboard');
let info = data[0];
images = data[1];
// Store GPU info
$('body').data('gpuInfo', info.gpuInfo);
// Upgrade button if needed
var upgrade;
if (info.currentVersion !== info.localVersion) {
upgrade = $('<button>', {class: 'btn btn-primary', onclick: 'renderUpgrade()'}).text('Upgrade to ' + info.currentVersion)
} else {
upgrade = info.currentVersion;
}
// Kasm docker containers
containersTable = $('<tbody>');
containersTable.append(
$('<tr>').append([
$('<th>').text('Web URL'),
$('<td>').append($('<a>', {href: 'https://' + host + ':' + info.port, target: '_blank'}).text('https://' + host + ':' + info.port))
]),
$('<tr>').append([
$('<th>').text('Installed Version'),
$('<td>').text(info.localVersion)
]),
$('<tr>').append([
$('<th>').text('Current Version'),
$('<td>').append(upgrade)
])
);
for await (let container of info.containers) {
@ -226,14 +265,60 @@ async function pickSettings() {
installSettings.useRolling = $('#useRolling').is(":checked");
installSettings.noDownload = $('#noDownload').is(":checked");
installSettings.forceGpu = $('#forceGpu').val();
pickImages();
pickImages(false);
});
}
// Render upgrade form
async function renderUpgrade() {
showContainer();
let gpus = $('body').data('gpuInfo');
titleChange('Upgrade Settings');
let form = $('<form>', {id: 'settingsform'});
let fieldset = $('<fieldset>').append($('<legend>').text('Kasm Upgrade Settings'));
let keepOldImages = $('<div>', {class: 'form-group'}).append([
$('<label>', {for: 'keepOldImages'}).text('Do not purge existing images: '),
$('<input>', {name: 'keepOldImages', id: 'keepOldImages', type: 'checkbox'})
]);
let gpuOptions = [$('<option>', {value: 'disabled'}).text('Disabled')];
for await (let card of Object.keys(gpus)) {
gpuOptions.push($('<option>', {value: card + '|' + gpus[card]}).text(card + ' - ' + gpus[card]));
}
let forceGpu = $('<div>', {class: 'form-group'}).append([
$('<label>', {for: 'forceGpu'}).text('Use GPU on all new images: '),
$('<select>', {name: 'forceGpu', id: 'forceGpu',}).append(gpuOptions)
]);
let submit = $('<div>', {class: 'form-group'}).append([
$('<input>', {name: 'submit', type: 'submit', value: 'Next', class: 'btn btn-default btn-ghost'})
]);
fieldset.append([
keepOldImages,
forceGpu,
submit
]);
form.append(fieldset);
$('#container').append(form);
// Grab data and move to image selection
form.on('submit', function (e) {
e.preventDefault();
upgradeSettings.keepOldImages = $('#keepOldImages').is(":checked");
upgradeSettings.forceGpu = $('#forceGpu').val();
pickImages(true);
});
}
// Render image selection
async function pickImages() {
async function pickImages(upgrade) {
var installText;
var installFunction;
if (upgrade) {
installText = 'Upgrade';
installFunction = 'upgrade()';
} else {
installText = 'Install';
installFunction = 'install()';
}
showContainer();
titleChange('Image Selection');
let imagesDiv = $('<div>', {class: 'cardcontainer', id: 'images'});
@ -251,7 +336,7 @@ async function pickImages() {
$('#images').append(imageDiv);
}
let allButton = $('<button>', {class: 'btn btn-default btn-ghost center', onclick: 'selectAll()'}).text('Select All');
let installButton = $('<button>', {class: 'btn btn-default btn-ghost center', onclick: 'install()'}).text('Install');
let installButton = $('<button>', {class: 'btn btn-default btn-ghost center', onclick: installFunction}).text(installText);
$('#container').append([
allButton,
installButton