mirror of
https://github.com/kasmtech/kasm-install-wizard.git
synced 2026-07-18 17:04:38 +00:00
Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a3b4f1a3c | ||
|
|
15f1b4aa17 |
4 changed files with 118 additions and 5445 deletions
|
|
@ -36,3 +36,8 @@ As configured files ingested from a current Kasm Workspaces installer are needed
|
||||||
/kasm_release/
|
/kasm_release/
|
||||||
└── Full Kasm workspaces installer
|
└── Full Kasm workspaces installer
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
To report any issues for this repository, please use our central issue tracker: **[Kasm Workspaces Issue Tracker](https://github.com/kasmtech/workspaces-issues/issues)**
|
||||||
|
|
|
||||||
107
index.js
107
index.js
|
|
@ -19,11 +19,12 @@ const docker = new Docker({socketPath: '/var/run/docker.sock'});
|
||||||
const arch = os.arch().replace('x64', 'amd64');
|
const arch = os.arch().replace('x64', 'amd64');
|
||||||
const baseUrl = process.env.SUBFOLDER || '/';
|
const baseUrl = process.env.SUBFOLDER || '/';
|
||||||
const port = process.env.KASM_PORT || '443';
|
const port = process.env.KASM_PORT || '443';
|
||||||
|
const { spawn } = require('node:child_process');
|
||||||
let EULA;
|
let EULA;
|
||||||
let images;
|
let images;
|
||||||
let currentVersion;
|
let currentVersion;
|
||||||
let sourceLocal = false;
|
let sourceLocal = false;
|
||||||
let gpuInfo = {};
|
let gpuInfo;
|
||||||
let installSettings = {};
|
let installSettings = {};
|
||||||
let upgradeSettings = {};
|
let upgradeSettings = {};
|
||||||
|
|
||||||
|
|
@ -47,7 +48,7 @@ function matchVersion(currentVer, compatibilityList) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the image tag for a given compat entry.
|
// Build the image tag for a given compat entry.
|
||||||
// Returns e.g. "1.19.0-rolling-weekly", falling back to "develop".
|
// Returns e.g. "1.18.1-rolling-weekly", falling back to "develop".
|
||||||
function buildImageTag(compat) {
|
function buildImageTag(compat) {
|
||||||
const compatTag = compat.image.split(':')[1] || '';
|
const compatTag = compat.image.split(':')[1] || '';
|
||||||
const versionPrefix = compatTag.replace(/-[^-]+-[^-]+$/, ''); // strip last two dash-segments
|
const versionPrefix = compatTag.replace(/-[^-]+-[^-]+$/, ''); // strip last two dash-segments
|
||||||
|
|
@ -69,7 +70,7 @@ async function fetchListData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the rolling-weekly tag string for the current version by finding
|
// 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").
|
// the first compatible workspace in the registry (e.g. "1.18.1-rolling-weekly").
|
||||||
async function getRollingWeeklyTag(currentVer, archName) {
|
async function getRollingWeeklyTag(currentVer, archName) {
|
||||||
const listData = await fetchListData();
|
const listData = await fetchListData();
|
||||||
const ws = (listData.workspaces || []).find(
|
const ws = (listData.workspaces || []).find(
|
||||||
|
|
@ -108,14 +109,12 @@ async function fetchWorkspaceList(currentVer, archName) {
|
||||||
exec_config: {},
|
exec_config: {},
|
||||||
friendly_name: ws.friendly_name,
|
friendly_name: ws.friendly_name,
|
||||||
gpu_count: 0,
|
gpu_count: 0,
|
||||||
gpu_graphics_acceleration: ['MESA'],
|
|
||||||
gpu_video_encoding: ['SW'],
|
|
||||||
hidden: false,
|
hidden: false,
|
||||||
image_id: '${uuid:image_id:' + imageIdx + '}',
|
image_id: '${uuid:image_id:' + imageIdx + '}',
|
||||||
image_src: 'https://registry.kasmweb.com/1.1/icons/' + ws.image_src,
|
image_src: 'https://registry.kasmweb.com/1.1/icons/' + ws.image_src,
|
||||||
image_type: 'Container',
|
image_type: 'Container',
|
||||||
launch_config: {},
|
launch_config: {},
|
||||||
memory_bytes: 2768000000,
|
memory: 2768000000,
|
||||||
name: imageName,
|
name: imageName,
|
||||||
notes: ws.notes || null,
|
notes: ws.notes || null,
|
||||||
run_config: {},
|
run_config: {},
|
||||||
|
|
@ -143,12 +142,97 @@ async function installerBlobs() {
|
||||||
try {
|
try {
|
||||||
currentVersion = fs.readFileSync('/version.txt', 'utf8').replace(/(\r\n|\n|\r)/gm,'');
|
currentVersion = fs.readFileSync('/version.txt', 'utf8').replace(/(\r\n|\n|\r)/gm,'');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
currentVersion = '1.19.0';
|
currentVersion = '1.18.1';
|
||||||
}
|
}
|
||||||
images = await fetchWorkspaceList(currentVersion, arch);
|
images = await fetchWorkspaceList(currentVersion, arch);
|
||||||
|
gpuInfo = {};
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
let gpuData = [];
|
||||||
|
let gpuCmd = spawn('/gpuinfo.sh');
|
||||||
|
gpuCmd.stdout.on('data', function(data) {
|
||||||
|
gpuData.push(data);
|
||||||
|
});
|
||||||
|
gpuCmd.on('close', function(code) {
|
||||||
|
try {
|
||||||
|
if (code == 0) {
|
||||||
|
gpuInfo = JSON.parse(gpuData.join(''));
|
||||||
|
} else {
|
||||||
|
gpuInfo = {};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Manually backfill GPU info if available
|
||||||
|
gpuInfo = {};
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
let num = i.toString();
|
||||||
|
if (fs.existsSync('/dev/dri/card' + num)) {
|
||||||
|
gpuInfo['/dev/dri/card' + num] = "Unknown GPU";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
installerBlobs();
|
installerBlobs();
|
||||||
|
|
||||||
|
// GPU image yaml merging
|
||||||
|
async function setGpu(imagesI) {
|
||||||
|
if (upgradeSettings['forceGpu'] !== undefined) {
|
||||||
|
installSettings = upgradeSettings;
|
||||||
|
}
|
||||||
|
const forceGpu = installSettings.forceGpu;
|
||||||
|
if (!forceGpu || forceGpu === 'disabled' || !forceGpu.includes('|')) {
|
||||||
|
console.error('setGpu: invalid or missing forceGpu value:', forceGpu);
|
||||||
|
return imagesI;
|
||||||
|
}
|
||||||
|
const [gpu, gpuName] = forceGpu.split('|');
|
||||||
|
if (!gpu || !gpuName) {
|
||||||
|
console.error('setGpu: could not parse GPU path or name from forceGpu:', forceGpu);
|
||||||
|
return imagesI;
|
||||||
|
}
|
||||||
|
const card = gpu.slice(-1);
|
||||||
|
const render = (Number(card) + 128).toString();
|
||||||
|
// Handle NVIDIA Gpus
|
||||||
|
let baseRun;
|
||||||
|
if (gpuName.includes('NVIDIA')) {
|
||||||
|
baseRun = JSON.parse('{"runtime":"nvidia","environment":{"NVIDIA_DRIVER_CAPABILITIES":"all","KASM_EGL_CARD":"/dev/dri/card' + card + '","KASM_RENDERD":"/dev/dri/renderD' + render + '"},"device_requests":[{"driver": "","count": -1,"device_ids": null,"capabilities":[["gpu"]],"options":{}}]}');
|
||||||
|
} else {
|
||||||
|
baseRun = JSON.parse('{"environment":{"DRINODE":"/dev/dri/renderD' + render + '", "HW3D": true},"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 (let i=0; i<imagesI.images.length; i++) {
|
||||||
|
console.log(imagesI.images[i]['run_config']);
|
||||||
|
let finalRun = _.merge(imagesI.images[i]['run_config'], baseRun)
|
||||||
|
let finalExec = _.merge(imagesI.images[i]['exec_config'], baseExec)
|
||||||
|
imagesI.images[i]['run_config'] = finalRun;
|
||||||
|
imagesI.images[i]['exec_config'] = finalExec;
|
||||||
|
}
|
||||||
|
return imagesI;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For rolling-weekly installs, append -rolling to service image tags in docker conf yamls.
|
||||||
|
// Prevents -rolling-rolling by only modifying tags that don't already end with -rolling.
|
||||||
|
async function appendRollingToServiceImages() {
|
||||||
|
const confDir = '/kasm_release/docker';
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = await fsw.readdir(confDir);
|
||||||
|
} catch (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const file of entries.filter(f => f.endsWith('.yaml'))) {
|
||||||
|
const filePath = confDir + '/' + file;
|
||||||
|
const content = await fsw.readFile(filePath, 'utf8');
|
||||||
|
const updated = content.split('\n').map(line => {
|
||||||
|
if (/ image:/.test(line) && /"$/.test(line) && !/-rolling"$/.test(line) && !/develop"$/.test(line)) {
|
||||||
|
return line.replace(/"$/, '-rolling"');
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}).join('\n');
|
||||||
|
await fsw.writeFile(filePath, updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//// Http server ////
|
//// Http server ////
|
||||||
baserouter.use('/public', express.static(__dirname + '/public'));
|
baserouter.use('/public', express.static(__dirname + '/public'));
|
||||||
baserouter.get("/", function (req, res) {
|
baserouter.get("/", function (req, res) {
|
||||||
|
|
@ -173,12 +257,18 @@ io.on('connection', async function (socket) {
|
||||||
installFlags.push('-b');
|
installFlags.push('-b');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GPU yaml merge
|
||||||
|
if (installSettings.forceGpu !== 'disabled' && imagesI && imagesI.images) {
|
||||||
|
imagesI = await setGpu(imagesI);
|
||||||
|
}
|
||||||
|
|
||||||
// Write finalized image data
|
// Write finalized image data
|
||||||
let yamlStr = yaml.dump(imagesI);
|
let yamlStr = yaml.dump(imagesI);
|
||||||
if (yamlStr.startsWith("false")) {
|
if (yamlStr.startsWith("false")) {
|
||||||
installFlags = installFlags.filter(function(e) { return e !== '-W' });
|
installFlags = installFlags.filter(function(e) { return e !== '-W' });
|
||||||
} else {
|
} else {
|
||||||
await fsw.writeFile('/kasm_release/conf/database/seed_data/default_images_' + arch + '.yaml', yamlStr);
|
await fsw.writeFile('/kasm_release/conf/database/seed_data/default_images_' + arch + '.yaml', yamlStr);
|
||||||
|
await appendRollingToServiceImages();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy over version
|
// Copy over version
|
||||||
|
|
@ -200,6 +290,9 @@ io.on('connection', async function (socket) {
|
||||||
async function upgrade(data) {
|
async function upgrade(data) {
|
||||||
let upgradeFlags = ['/kasm_release/upgrade.sh', '-L', port];
|
let upgradeFlags = ['/kasm_release/upgrade.sh', '-L', port];
|
||||||
|
|
||||||
|
// Patch service image tags for rolling builds
|
||||||
|
await appendRollingToServiceImages();
|
||||||
|
|
||||||
// Run upgrade
|
// Run upgrade
|
||||||
let cmd = pty.spawn('/bin/bash', upgradeFlags);
|
let cmd = pty.spawn('/bin/bash', upgradeFlags);
|
||||||
cmd.on('data', function(data) {
|
cmd.on('data', function(data) {
|
||||||
|
|
|
||||||
5437
list.json
5437
list.json
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,7 @@
|
||||||
// Variables
|
// Variables
|
||||||
let EULA;
|
let EULA;
|
||||||
let images;
|
let images;
|
||||||
|
let gpus;
|
||||||
let term;
|
let term;
|
||||||
let installImages = [];
|
let installImages = [];
|
||||||
const installSettings = {};
|
const installSettings = {};
|
||||||
|
|
@ -92,6 +93,7 @@ function renderInstall(data) {
|
||||||
titleChange('EULA');
|
titleChange('EULA');
|
||||||
EULA = data[0];
|
EULA = data[0];
|
||||||
images = data[1];
|
images = data[1];
|
||||||
|
gpus = data[2] || {};
|
||||||
versionChange(data[3], data[4]);
|
versionChange(data[3], data[4]);
|
||||||
let EULADiv = $('<div>', {id: 'EULA'}).text(EULA);
|
let EULADiv = $('<div>', {id: 'EULA'}).text(EULA);
|
||||||
$('#container').append(EULADiv);
|
$('#container').append(EULADiv);
|
||||||
|
|
@ -228,7 +230,14 @@ async function pickSettings() {
|
||||||
$('<label>', {for: 'userPass'}).text('user@kasm.local Password: '),
|
$('<label>', {for: 'userPass'}).text('user@kasm.local Password: '),
|
||||||
$('<input>', {name: 'userPass', id: 'userPass', type: 'password', required: true, placeholder: 'required'})
|
$('<input>', {name: 'userPass', id: 'userPass', type: 'password', required: true, placeholder: 'required'})
|
||||||
]);
|
]);
|
||||||
let gpuNotice = $('<p>').text('GPU acceleration is automatically managed by Kasm Workspaces 1.19.0. Configure GPU settings after installation in the admin panel under Infrastructure > Agents.');
|
let gpuOptions = [$('<option>', {value: 'disabled'}).text('Disabled')];
|
||||||
|
for (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 images: '),
|
||||||
|
$('<select>', {name: 'forceGpu', id: 'forceGpu',}).append(gpuOptions)
|
||||||
|
]);
|
||||||
let submit = $('<div>', {class: 'form-group'}).append([
|
let submit = $('<div>', {class: 'form-group'}).append([
|
||||||
$('<input>', {name: 'submit', type: 'submit', value: 'Next', class: 'btn btn-default btn-ghost'})
|
$('<input>', {name: 'submit', type: 'submit', value: 'Next', class: 'btn btn-default btn-ghost'})
|
||||||
]);
|
]);
|
||||||
|
|
@ -240,7 +249,7 @@ async function pickSettings() {
|
||||||
passHint,
|
passHint,
|
||||||
adminPass,
|
adminPass,
|
||||||
userPass,
|
userPass,
|
||||||
gpuNotice,
|
forceGpu,
|
||||||
submit
|
submit
|
||||||
]);
|
]);
|
||||||
form.append(fieldset);
|
form.append(fieldset);
|
||||||
|
|
@ -258,6 +267,7 @@ async function pickSettings() {
|
||||||
$('#pass-hint').css('color', 'inherit');
|
$('#pass-hint').css('color', 'inherit');
|
||||||
installSettings.adminPass = adminPassVal;
|
installSettings.adminPass = adminPassVal;
|
||||||
installSettings.userPass = userPassVal;
|
installSettings.userPass = userPassVal;
|
||||||
|
installSettings.forceGpu = $('#forceGpu').val();
|
||||||
pickImages(false);
|
pickImages(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue