From c8a3aab2361eead195b23d7efbcf2d497122b4be Mon Sep 17 00:00:00 2001 From: jfwang Date: Wed, 17 Jul 2024 15:30:42 +0100 Subject: [PATCH] feat(ae): proxy for jupyter --- packages/itmat-commons/src/utils/poller.ts | 3 +- .../create_lxd_image_jupyter_container.sh | 84 ++++++ .../config/create_lxd_image_matlab_desktop.sh | 174 ++++++++++++ packages/itmat-interface/config/system.key | 54 ---- .../itmat-interface/config/system.key.pub | 14 - .../itmat-interface/src/core/instanceCore.ts | 2 +- packages/itmat-interface/src/lxd/index.ts | 162 ++++++++++- .../itmat-interface/src/lxd/lxdManager.ts | 6 +- packages/itmat-interface/src/server/router.ts | 6 +- .../src/jobHandlers/lxdJobHandler.ts | 6 +- packages/itmat-ui-react/proxy.conf.js | 6 + .../src/components/instance/instance.tsx | 46 ++-- .../components/lxd/lxd.instance.console.tsx | 28 +- .../src/components/lxd/lxd.instance.list.tsx | 10 +- .../lxd/lxd.instance.text.console.tsx | 251 +++++++++++------- .../src/components/lxd/lxd.module.css | 23 +- 16 files changed, 652 insertions(+), 223 deletions(-) create mode 100644 packages/itmat-interface/config/create_lxd_image_jupyter_container.sh create mode 100644 packages/itmat-interface/config/create_lxd_image_matlab_desktop.sh delete mode 100644 packages/itmat-interface/config/system.key delete mode 100644 packages/itmat-interface/config/system.key.pub diff --git a/packages/itmat-commons/src/utils/poller.ts b/packages/itmat-commons/src/utils/poller.ts index 7903dc97a..b5990ad14 100644 --- a/packages/itmat-commons/src/utils/poller.ts +++ b/packages/itmat-commons/src/utils/poller.ts @@ -52,7 +52,7 @@ export class JobPoller { try { const result = await this.action(job); - Logger.log(`[JOB] Job Execution finished: ${new Date((Date.now())).toISOString()}, ${JSON.stringify(result?.error)}, ${JSON.stringify(result)}`); + // Logger.log(`[JOB] Job Execution finished: ${new Date((Date.now())).toISOString()}, ${JSON.stringify(result?.error)}, ${JSON.stringify(result)}`); if (job.period) { setObj.status = enumJobStatus.PENDING; @@ -97,7 +97,6 @@ export class JobPoller { }); } } catch (error) { - console.error('[JOB poller]Job execution Error', new Date((Date.now())).toISOString(), error); const currentHistory = job.history || []; setObj.history = [...currentHistory, { time: Date.now(), diff --git a/packages/itmat-interface/config/create_lxd_image_jupyter_container.sh b/packages/itmat-interface/config/create_lxd_image_jupyter_container.sh new file mode 100644 index 000000000..d256355c9 --- /dev/null +++ b/packages/itmat-interface/config/create_lxd_image_jupyter_container.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# Set variables +BASE_IMAGE="ubuntu:20.04" +NEW_IMAGE_ALIAS="ubuntu-jupyter-container-image" +CONTAINER_NAME="temp-container" +TIMESTAMP=$(date +%Y%m%d%H%M%S) + +# Check if an image with the same alias exists and rename it +if lxc image list | grep -q "$NEW_IMAGE_ALIAS"; then + lxc image alias rename $NEW_IMAGE_ALIAS "${NEW_IMAGE_ALIAS}-${TIMESTAMP}" + echo "Existing image with alias '$NEW_IMAGE_ALIAS' renamed." +fi + +# Launch a new container from the base image +lxc launch $BASE_IMAGE $CONTAINER_NAME + +# Wait for the container to start +sleep 10 + +# Preconfigure debconf for non-interactive davfs2 installation +lxc exec $CONTAINER_NAME -- bash -c "echo 'davfs2 davfs2/suid_file boolean true' | debconf-set-selections" + +# Install necessary packages in the container +lxc exec $CONTAINER_NAME -- bash -c "DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y python3 python3-pip net-tools davfs2" + +# Upgrade zipp to the required version +lxc exec $CONTAINER_NAME -- bash -c "pip3 install --upgrade zipp" + +# Install Jupyter using pip +lxc exec $CONTAINER_NAME -- bash -c "pip3 install notebook" + +# Create Jupyter configuration file +lxc exec $CONTAINER_NAME -- bash -c "sudo mkdir -p /root/.jupyter" +lxc exec $CONTAINER_NAME -- bash -c "echo ' +c.NotebookApp.ip = \"0.0.0.0\" +c.NotebookApp.port = 8888 +c.NotebookApp.open_browser = False +c.NotebookApp.token = \"\" +c.NotebookApp.password = \"\" +c.NotebookApp.allow_root = True +' > /root/.jupyter/jupyter_notebook_config.py" + +# Create Jupyter systemd service file +lxc exec $CONTAINER_NAME -- bash -c "echo ' +[Unit] +Description=Jupyter Notebook + +[Service] +Type=simple +PIDFile=/run/jupyter.pid +ExecStart=/usr/local/bin/jupyter notebook --config=/root/.jupyter/jupyter_notebook_config.py --allow-root +User=root +Group=root +WorkingDirectory=/home/ubuntu +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +' > /etc/systemd/system/jupyter.service" + +# Enable and start the Jupyter service +lxc exec $CONTAINER_NAME -- systemctl enable jupyter.service +lxc exec $CONTAINER_NAME -- systemctl start jupyter.service + +# Wait for the Jupyter service to start +sleep 10 + +# Stop the container +lxc stop $CONTAINER_NAME + +# Wait for the container to stop +while lxc info $CONTAINER_NAME | grep -q 'Status: Running'; do + sleep 1 +done + +# Publish the container as a new image with properties +lxc publish $CONTAINER_NAME --alias $NEW_IMAGE_ALIAS + +# Clean up +lxc delete $CONTAINER_NAME + +echo "New image '$NEW_IMAGE_ALIAS' created successfully." \ No newline at end of file diff --git a/packages/itmat-interface/config/create_lxd_image_matlab_desktop.sh b/packages/itmat-interface/config/create_lxd_image_matlab_desktop.sh new file mode 100644 index 000000000..bc22cd9de --- /dev/null +++ b/packages/itmat-interface/config/create_lxd_image_matlab_desktop.sh @@ -0,0 +1,174 @@ +#!/bin/bash + +# Set variables +BASE_IMAGE="images:ubuntu/focal/desktop" +NEW_IMAGE_ALIAS="ubuntu-matlab-image" +CONTAINER_NAME="temp-vm-v1" +wesTIMESTAMP=$(date +%Y%m%d%H%M%S) + +# Check if an image with the same alias exists and rename it +if lxc image list | grep -q "$NEW_IMAGE_ALIAS"; then + lxc image alias rename $NEW_IMAGE_ALIAS "${NEW_IMAGE_ALIAS}-${TIMESTAMP}" + echo "Existing image with alias '$NEW_IMAGE_ALIAS' renamed." +fi + +# Launch a new VM from the base image +lxc launch $BASE_IMAGE $CONTAINER_NAME --vm + +# Wait for the VM to start +sleep 30 + +# Update and install necessary packages +# lxc exec $CONTAINER_NAME -- bash -c "DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y python3 python3-pip davfs2 cloud-init" +lxc exec $CONTAINER_NAME -- bash -c "export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' python3 python3-pip davfs2 cloud-init" + + +# Enable and start cloud-init +lxc exec $CONTAINER_NAME -- systemctl enable cloud-init +lxc exec $CONTAINER_NAME -- systemctl start cloud-init +lxc exec $CONTAINER_NAME -- systemctl start lxd-agent.service + +# Install Jupyter using pip +lxc exec $CONTAINER_NAME -- bash -c "pip3 install notebook" + +# Create Jupyter configuration file +lxc exec $CONTAINER_NAME -- bash -c "sudo mkdir -p /root/.jupyter" +lxc exec $CONTAINER_NAME -- bash -c "echo ' +c.NotebookApp.ip = \"0.0.0.0\" +c.NotebookApp.port = 8888 +c.NotebookApp.open_browser = False +c.NotebookApp.token = \"\" +c.NotebookApp.password = \"\" +c.NotebookApp.allow_root = True +' > /root/.jupyter/jupyter_notebook_config.py" + +# Create Jupyter systemd service file +lxc exec $CONTAINER_NAME -- bash -c "echo ' +[Unit] +Description=Jupyter Notebook + +[Service] +Type=simple +PIDFile=/run/jupyter.pid +ExecStart=/usr/local/bin/jupyter notebook --config=/root/.jupyter/jupyter_notebook_config.py --allow-root +User=root +Group=root +WorkingDirectory=/home/ubuntu +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +' > /etc/systemd/system/jupyter.service" + +# Enable and start the Jupyter service +lxc exec $CONTAINER_NAME -- systemctl enable jupyter.service +lxc exec $CONTAINER_NAME -- systemctl start jupyter.service + + +MATLAB_VERSION="R2022b" +MATLAB_INSTALLER_URL="https://ssd.mathworks.com/supportfiles/downloads/mpm/2024.1.1/glnxa64/mpm" +MATLAB_INSTALL_DIR="/usr/local/MATLAB/${MATLAB_VERSION}" +MATLAB_PRODUCTS="MATLAB Simulink" # Add all required MATLAB products here + +# Install MATLAB (refer to the official MathWorks instructions for unattended installation) + +lxc exec $CONTAINER_NAME -- bash -c "wget ${MATLAB_INSTALLER_URL} -O /var/tmp/mpm && chmod +x /var/tmp/mpm" + +lxc exec $CONTAINER_NAME -- bash -c "/var/tmp/mpm install --release ${MATLAB_VERSION} --destination ${MATLAB_INSTALL_DIR} --products ${MATLAB_PRODUCTS}" + +sleep 10 + + +# Create MATLAB desktop shortcut +lxc exec $CONTAINER_NAME -- sudo -i bash -c " +echo '[Desktop Entry] +Version=1.0 +Type=Application +Terminal=false +MimeType=text/x-matlab +Name=MATLAB R2022b +Exec=env MATLAB_USE_USERWORK=1 /usr/local/MATLAB/R2022b/bin/matlab -desktop +Icon=/usr/local/MATLAB/R2022b/bin/glnxa64/cef_resources/matlab_icon.png +Categories=Development;Math;Science +Comment=Scientific computing environment +StartupNotify=true' > /home/ubuntu/.local/share/applications/matlab.desktop + +# Change ownership and permissions +chown ubuntu:ubuntu /home/ubuntu/.local/share/applications/matlab.desktop +chmod +x /home/ubuntu/.local/share/applications/matlab.desktop + +# Create a symbolic link on the desktop +ln -sf /home/ubuntu/.local/share/applications/matlab.desktop /home/ubuntu/Desktop/matlab.desktop +chown ubuntu:ubuntu /home/ubuntu/Desktop/matlab.desktop + +# Force update the desktop menu +xdg-desktop-menu install --novendor /home/ubuntu/.local/share/applications/matlab.desktop +xdg-desktop-menu forceupdate +" + +# Validate the .desktop file creation +# lxc exec $CONTAINER_NAME -- bash -c "ls -l /home/ubuntu/.local/share/applications/matlab.desktop" +# lxc exec $CONTAINER_NAME -- bash -c "ls -l /home/ubuntu/Desktop/matlab.desktop" + +# Add MATLAB to the favorites +lxc exec $CONTAINER_NAME -- bash -c "su - ubuntu -c 'gsettings set org.gnome.shell favorite-apps \"['\''firefox.desktop'\'', '\''org.gnome.Terminal.desktop'\'', '\''matlab.desktop'\'']\"'" + +# Restart GNOME shell +# lxc exec $CONTAINER_NAME -- bash -c "su - ubuntu -c 'DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus gnome-shell --replace &'" +# Set MATLAB to start automatically +lxc exec $CONTAINER_NAME -- bash -c "mkdir -p /home/ubuntu/.config/autostart" +lxc exec $CONTAINER_NAME -- bash -c "cp /home/ubuntu/.local/share/applications/matlab.desktop /home/ubuntu/.config/autostart/" +lxc exec $CONTAINER_NAME -- bash -c "chmod 755 /home/ubuntu/.config/autostart/matlab.desktop" +lxc exec $CONTAINER_NAME -- bash -c "sudo chown ubuntu:ubuntu /home/ubuntu/.config/autostart/matlab.desktop" +lxc exec $CONTAINER_NAME -- bash -c "ls -l /home/ubuntu/.config/autostart/matlab.desktop" + + + +lxc exec $CONTAINER_NAME -- bash -c 'cat < /home/ubuntu/setup_nopasswd.sh +DEFAULT_USER="\${USERNAME:-ubuntu}" +if ! getent group nopasswdlogin > /dev/null; then + sudo addgroup nopasswdlogin +fi +if ! id -u \${DEFAULT_USER} > /dev/null 2>&1; then + sudo adduser \${DEFAULT_USER} nopasswdlogin || true +else + sudo usermod -aG nopasswdlogin \${DEFAULT_USER} +fi +sudo passwd -d \${DEFAULT_USER} || true + +echo "@reboot \${DEFAULT_USER} DISPLAY=:0 /home/\${DEFAULT_USER}/disable_autolock.sh" | sudo crontab -u \${DEFAULT_USER} - + +cat << 'EOD' > "/home/\${DEFAULT_USER}/disable_autolock.sh" +#!/bin/bash +gsettings set org.gnome.desktop.screensaver lock-enabled false +gsettings set org.gnome.desktop.session idle-delay 0 +EOD + +sudo chmod +x /home/\${DEFAULT_USER}/disable_autolock.sh +EOF' + +lxc exec $CONTAINER_NAME -- bash -c "chmod +x /home/ubuntu/setup_nopasswd.sh" +lxc exec $CONTAINER_NAME -- bash -c "sudo /home/ubuntu/setup_nopasswd.sh" + +# Wait for the services to start +sleep 10 + +# Stop the VM +lxc stop $CONTAINER_NAME + +# Wait for the VM to stop +while lxc info $CONTAINER_NAME | grep -q 'Status: Running'; do + sleep 1 +done + +# Publish the VM as a new image with properties +lxc publish $CONTAINER_NAME --alias $NEW_IMAGE_ALIAS + +# Set image not to expire +lxc image set-property $NEW_IMAGE_ALIAS remote_cache_expiry 0 + +# Clean up +lxc delete $CONTAINER_NAME + +echo "New image '$NEW_IMAGE_ALIAS' created successfully." \ No newline at end of file diff --git a/packages/itmat-interface/config/system.key b/packages/itmat-interface/config/system.key deleted file mode 100644 index 2916e5fb7..000000000 --- a/packages/itmat-interface/config/system.key +++ /dev/null @@ -1,54 +0,0 @@ ------BEGIN ENCRYPTED PRIVATE KEY----- -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 ------END ENCRYPTED PRIVATE KEY----- diff --git a/packages/itmat-interface/config/system.key.pub b/packages/itmat-interface/config/system.key.pub deleted file mode 100644 index 2fd4bf255..000000000 --- a/packages/itmat-interface/config/system.key.pub +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnKXlNUDuKSAAClMe2cXf -7maUnwcS2qsPYuG/gjZn8/Y+ICguPia5c+xlcQgHFYP7bBLeohsPbnp5bG0YTgeg -GlLPoy+z/rZKBdKTXnv3fAy6ko9SnjpxfC/+kIkNfMcQe7YiwyGsvhVRYUErn5zZ -Qdxjot+DHXrrOsX00fu/E7mU0VqeISouRKkeYSzx4kJVYxGE89hrya8RyOmnyAay -P5rhPicde4Oiag4J4K+t6eyNS3cel4JiKct8p+fPs+ryw/yzhYOffpOhkAJZvVcR -/FRB1dUio2pn6BrqYZGWEJllZbOEbhKep8VKCQgqhI74Ab9Bl/hZU6n6+FFJbJfe -FUaFqdkIMrEy5lKA7K50prhvkjokvf3XeDl+2WnfndIGVjcDMag1KXoQqdqdEScq -T9DCQuib+JIdq4P81krtqxMOKg4c5cPhzTydACluAr6FLNjStCIMcI0uDHlmfIt/ -fd3/VdYoCxl0paZlA6Ku406HvpBk9oGz7i7eLQ2G6Wby3OP2EPA3JNW6zqe+AnfJ -szLc39MrBeR4BMmj4++9QK5EsF+hg9KVVLwbaVs0HxlRqvnCN99M0EVPZL1DhnmQ -QCn5O5vVAvmeDnTb+keI79qA6JIABdoEZSjFB2pWrwC/EQQj2nzL56EPT1oCNvLF -Bp5cqMAVb4vGCe2LAZ8MNxECAwEAAQ== ------END PUBLIC KEY----- diff --git a/packages/itmat-interface/src/core/instanceCore.ts b/packages/itmat-interface/src/core/instanceCore.ts index d5a666abb..8f39e4fa8 100644 --- a/packages/itmat-interface/src/core/instanceCore.ts +++ b/packages/itmat-interface/src/core/instanceCore.ts @@ -85,7 +85,7 @@ export class InstanceCore { const webdavServer = `${config.webdavServer}:${config.webdavPort}`; const webdavMountPath = config.webdavMountPath; - const instanceProfile = type==='virtual-machine'? 'matlab-profile' : 'jupyter-profile'; // Assuming 'default' profile, adjust as necessary + const instanceProfile = type==='virtual-machine'? 'matlab-profile' : 'jupyter-profile'; // Prepare user-data for cloud-init to initialize the instance const cloudInitUserDataContainer = ` diff --git a/packages/itmat-interface/src/lxd/index.ts b/packages/itmat-interface/src/lxd/index.ts index fe93b328f..b680ece09 100644 --- a/packages/itmat-interface/src/lxd/index.ts +++ b/packages/itmat-interface/src/lxd/index.ts @@ -1,11 +1,16 @@ import { WebSocketServer, WebSocket } from 'ws'; +import { Express, NextFunction, Request, Response} from 'express'; +import http from 'node:http'; import qs from 'qs'; +import { Socket } from 'net'; import lxdManager from './lxdManager'; import { Logger } from '@itmat-broker/itmat-commons'; +import { LXDInstanceState } from '@itmat-broker/itmat-types'; +import { createProxyMiddleware } from 'http-proxy-middleware'; -// define text decoder -const textDecoder = new TextDecoder(); + +const textDecoder = new TextDecoder('utf-8'); export const registerContainSocketServer = (server: WebSocketServer) => { @@ -40,9 +45,7 @@ export const registerContainSocketServer = (server: WebSocketServer) => { }; const flushContainerMessageBuffers = () => { - // console.log(' clientSocket.readyState 11',clientSocket?.readyState); if (clientSocket.readyState === WebSocket.OPEN) { - // console.log(' clientSocket.readyState 12',clientSocket.readyState); const curr = containerMessageBuffers[0]; if (curr) { clientSocket.send(curr[0], { binary: curr[1] }, (err) => { @@ -57,6 +60,11 @@ export const registerContainSocketServer = (server: WebSocketServer) => { } } }; + // send test message to client + clientSocket.on('open', () => { + console.log('client socket open'); + clientSocket.send('test message'); + }); clientSocket.on('message', (message, isBinary) => { // console.log(`Message from client: ${message}`); @@ -104,7 +112,7 @@ export const registerContainSocketServer = (server: WebSocketServer) => { // } else { // // It's already an ArrayBuffer // arrayBuffer = message; - // } + // }ßßß // const messageContent = `Binary message: ${textDecoder.decode(arrayBuffer)}`; // console.log(`Message from container: ${messageContent}`); @@ -119,7 +127,7 @@ export const registerContainSocketServer = (server: WebSocketServer) => { }); containerSocket.on('close', (code, reason) => { - console.log('container websocket close:',code, reason.toString()); + // console.log('container websocket close:',code, reason.toString()); flushContainerMessageBuffers(); if (clientSocket?.readyState === WebSocket.OPEN) clientSocket?.close(4010, `The container socket was closed with code${code}: ${reason.toString()}`); @@ -139,6 +147,146 @@ export const registerContainSocketServer = (server: WebSocketServer) => { } }); server.on('error', (err) => { - console.error('LXD socket broker errored', err); + Logger.error(`LXD socket broker errored: ${JSON.stringify(err)}`); + }); +}; + + +// Middleware to fetch the container IP +async function getContainerIP(containerName: string): Promise { + const response = await lxdManager.getInstanceState(containerName); + if (response.error || !response.data) { + Logger.error(`Unable to retrieve instance state: ${containerName}`); + throw new Error('Unable to retrieve instance state.'); + } + + const instanceState = response.data as LXDInstanceState; + if (!instanceState.network || !instanceState.network.eth0) { + Logger.error(`Unable to retrieve network details for instance: ${containerName}`); + throw new Error('Unable to retrieve network details for instance.'); + } + + const ipv4Address = instanceState.network.eth0.addresses + .filter((addr: any) => addr.family === 'inet') + .map((addr: any) => addr.address)[0]; + + if (!ipv4Address) { + Logger.error(`No IPv4 address found for instance: ${containerName}`); + throw new Error('No IPv4 address found for instance.'); + } + + return ipv4Address; +} + + +// Middleware to handle proxying Jupyter requests +export const jupyterProxyMiddleware = async (req: Request, res: Response, next: NextFunction) => { + const containerName = req.params.containerName; + console.log(`Incoming request: ${req.method} ${req.url}`); + console.log(`jupyterProxyMiddleware ${containerName}`); + try { + // const containerIP = getContainerIP(containerName); + const containerIP = 'localhost'; + const jupyterPort = 8889; + + const proxy = createProxyMiddleware({ + target: `http://${containerIP}:${jupyterPort}`, + changeOrigin: true, + ws: true, + autoRewrite: true, + followRedirects: true, + selfHandleResponse: true, // Enable custom response handling + protocolRewrite: 'http', + pathRewrite: (path, req) => { + return path.replace(`/jupyter/${containerName}`, ''); + }, + onProxyRes: (proxyRes, req, res) => { + const contentType = proxyRes.headers['content-type']; + // Handling redirects + if (proxyRes.statusCode && [307, 308].indexOf(proxyRes.statusCode) > -1 && proxyRes.headers.location) { + let redirect = proxyRes.headers.location; + Logger.warn('Received code ' + proxyRes.statusCode + ' from Jupyter Server for URL - ' + redirect); + redirect = redirect.replace('http://localhost:8889', `/jupyter/${containerName}`); + Logger.warn('Manipulating header location and redirecting to - ' + redirect); + proxyRes.headers.location = redirect; + } + + if (contentType && contentType.includes('text/html')) { + let body = ''; + + proxyRes.on('data', (chunk) => { + body += chunk; + }); + + proxyRes.on('end', () => { + + body = body.replace(/(href|src|data-main)="\/(tree|notebooks|lab|api|files|static|custom|nbconvert|kernelspecs|services|terminals|hub|user)\//g, `$1="/jupyter/${containerName}/$2/`); + + res.setHeader('Content-Length', Buffer.byteLength(body)); + res.setHeader('Content-Type', contentType); + res.end(body); + }); + } else { + proxyRes.pipe(res); + } + }, + onProxyReq: (proxyReq, req, res) => { + + // Handle body parsing if necessary + if (req.body && Object.keys(req.body).length) { + const contentType = proxyReq.getHeader('Content-Type'); + proxyReq.setHeader('origin', `http://${containerIP}:${jupyterPort}`); + const writeBody = (bodyData: string) => { + proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData)); + proxyReq.write(bodyData); + proxyReq.end(); + }; + + if (contentType === 'application/json') { + writeBody(JSON.stringify(req.body)); + } + + if (contentType === 'application/x-www-form-urlencoded') { + writeBody(qs.stringify(req.body)); + } + } + }, + onError: (err, req, res) => { + Logger.error(`Error proxying to Jupyter: ${err.message}`); + if (res instanceof http.ServerResponse) { + res.writeHead(500, { + 'Content-Type': 'text/plain' + }); + res.end('Error connecting to Jupyter server'); + } + } + }); + + proxy(req, res, next); + } catch (error) { + Logger.error(`Error in Jupyter proxy middleware: ${error}`); + res.status(500).send('Error connecting to Jupyter server'); + } +}; + + +// Apply the proxy middleware to the defined paths +export const applyProxyMiddleware = (app: Express) => { + const proxyRouters = [ + '/jupyter/:containerName', + '/jupyter/:containerName/*', + '/static/*', + '/custom/*', + '/tree/*', + '/api/*', + '/files/*', + '/lab/*', + '/nbconvert/*', + '/notebooks/*' + ]; + + proxyRouters.forEach(router => { + app.use(router, jupyterProxyMiddleware); }); + }; \ No newline at end of file diff --git a/packages/itmat-interface/src/lxd/lxdManager.ts b/packages/itmat-interface/src/lxd/lxdManager.ts index e2b2dda52..1821bfa07 100644 --- a/packages/itmat-interface/src/lxd/lxdManager.ts +++ b/packages/itmat-interface/src/lxd/lxdManager.ts @@ -195,15 +195,15 @@ export default { Logger.error(`Failed to fetch Logger log data. ${response.data}`); return { error: true, - data: `Failed to fetch Logger log data. ${response.data}` + data: `Failed to fetch Logger log data. ${JSON.stringify(response.data)}` }; } } catch (error: any) { if (error.response && error.response.status === 404) { - Logger.error(`Logger log file not found.${JSON.stringify(error.response)}`); + Logger.error(`Logger log file not found.${error.response}`); return { error: true, - data: `Logger log file not found.${JSON.stringify(error.response)}` + data: `Logger log file not found.${error.response}` }; } else { Logger.error(`Error fetching instance Logger log from LXD:${error}`); diff --git a/packages/itmat-interface/src/server/router.ts b/packages/itmat-interface/src/server/router.ts index 2413d8982..26ddfdcea 100644 --- a/packages/itmat-interface/src/server/router.ts +++ b/packages/itmat-interface/src/server/router.ts @@ -35,14 +35,12 @@ import { inferAsyncReturnType, initTRPC } from '@trpc/server'; import * as trpcExpress from '@trpc/server/adapters/express'; import multer from 'multer'; import { MerkleTreeLog } from '../log/merkleTree'; -import { registerContainSocketServer } from '../lxd'; +import { registerContainSocketServer, applyProxyMiddleware} from '../lxd'; // local test import cors from 'cors'; -import { Logger } from '@itmat-broker/itmat-commons'; // created for each request - export const createContext = async ({ req, res @@ -99,6 +97,7 @@ interface ApolloServerContext { token?: string; } + export class Router { private readonly app: Express; private readonly server: http.Server; @@ -417,6 +416,7 @@ export class Router { }) ); + applyProxyMiddleware(this.app); // this.app.listen(4200); } diff --git a/packages/itmat-job-executor/src/jobHandlers/lxdJobHandler.ts b/packages/itmat-job-executor/src/jobHandlers/lxdJobHandler.ts index 7b2d8be2c..b85e0f592 100644 --- a/packages/itmat-job-executor/src/jobHandlers/lxdJobHandler.ts +++ b/packages/itmat-job-executor/src/jobHandlers/lxdJobHandler.ts @@ -354,7 +354,6 @@ export class LXDMonitorHandler extends JobHandler { // update the instance status private async updateInstanceState(userId: string): Promise { - Logger.log(`Updating instance state for user: ${userId}`); // Retrieve all instances belonging to the user const instances = await this.instanceCollection.find({ userId }).toArray(); @@ -382,7 +381,7 @@ export class LXDMonitorHandler extends JobHandler { Logger.error(`Failed to retrieve state for instance: ${instance.name}`); } } catch (error) { - Logger.error(`Error updating state for instance ${instance.name}: ${error}`); + // Logger.error(`Error updating state for instance ${instance.name}: ${error}`); } } } @@ -396,7 +395,8 @@ export class LXDMonitorHandler extends JobHandler { } else if (state.status === 'Error') { return enumInstanceStatus.FAILED; } else { - return enumInstanceStatus.DELETED; + return enumInstanceStatus.FAILED; + Logger.error(`Unknown instance state: ${state.status}`); } } } \ No newline at end of file diff --git a/packages/itmat-ui-react/proxy.conf.js b/packages/itmat-ui-react/proxy.conf.js index 4de62f8fb..025b869a1 100644 --- a/packages/itmat-ui-react/proxy.conf.js +++ b/packages/itmat-ui-react/proxy.conf.js @@ -67,5 +67,11 @@ module.exports = { changeOrigin: true, autoRewrite: true, ws: true + }, + '/jupyter': { + target: API_SERVER, + changeOrigin: true, + autoRewrite: true, + ws: true } }; \ No newline at end of file diff --git a/packages/itmat-ui-react/src/components/instance/instance.tsx b/packages/itmat-ui-react/src/components/instance/instance.tsx index 74b6144b1..0e00d0bd1 100644 --- a/packages/itmat-ui-react/src/components/instance/instance.tsx +++ b/packages/itmat-ui-react/src/components/instance/instance.tsx @@ -3,7 +3,7 @@ import { Button, message, Modal, Form, Select, Card, Tag, Progress, Space, Row import { trpc } from '../../utils/trpc'; import css from './instance.module.css'; import { enumAppType,enumInstanceType, enumInstanceStatus, IInstance} from '@itmat-broker/itmat-types'; -import { LXDConsole } from '../lxd/lxd.instance.console'; +import { LXDConsole , LXDConsoleRef} from '../lxd/lxd.instance.console'; import LXDTextConsole from '../lxd/lxd.instance.text.console'; @@ -38,7 +38,7 @@ export const InstanceSection: FunctionComponent = () => { const [selectedInstanceTypeDetails, setSelectedInstanceTypeDetails] = useState(''); const [isConnectingToJupyter, setIsConnectingToJupyter] = useState(false); - const handleFullScreenRef = useRef(()=> { console.log('1111 handleFullScreenRef not set yet!'); }); + const handleFullScreenRef = useRef(null); const handleConsoleConnect = (instance) => { setSelectedInstance(instance); @@ -84,7 +84,6 @@ export const InstanceSection: FunctionComponent = () => { message.error(`Failed to restart instance: ${error.message}`); } }); - const instanceJupyter = trpc.lxd.getInstanceJupyterUrl.useMutation(); const [isModalOpen, setIsModalOpen] = useState(false); @@ -135,16 +134,17 @@ export const InstanceSection: FunctionComponent = () => { setIsConnectingToJupyter(true); // Indicate that connection attempt is in progress try { - // get the jupyter url by call tRPC procedure - const data = await instanceJupyter.mutateAsync({ instanceName }); - - if (data && data?.jupyterUrl) { - - window.open(data.jupyterUrl, '_blank'); - - } else { - message.error('Jupyter URL not found.'); - } + // Construct the Jupyter proxy URL directly + const baseUrl = new URL(window.location.href); + console.log('baseUrl', baseUrl.href); + // const jupyterProxyUrl = `${baseUrl.origin}/jupyter/${instanceName}`; + baseUrl.pathname = '/jupyter'; + const jupyterProxyUrl = `http://localhost:3333/jupyter/${instanceName}`; + // const jupyterProxyUrl = `${baseUrl.href}/${instanceName}`; + console.log(jupyterProxyUrl); + + // Open the Jupyter service in a new tab + window.open(jupyterProxyUrl, '_blank'); } catch (error: any) { message.error(error?.message || 'Failed to connect to Jupyter. Please try again.'); } finally { @@ -189,9 +189,10 @@ export const InstanceSection: FunctionComponent = () => { return 'default'; } }; - - const onChildMount = (childHandleFullScreen) => { - handleFullScreenRef.current = childHandleFullScreen; + const enterFullScreen = () => { + if (handleFullScreenRef.current) { + handleFullScreenRef.current.handleFullScreen(); + } }; // Reset the connect signal when the modal is closed @@ -276,7 +277,9 @@ export const InstanceSection: FunctionComponent = () => { )} {instance.status === enumInstanceStatus.RUNNING && ( - + // change the danger to warning color + + // )} {/* Only show Delete button for STOPPED status */} {(instance.status === enumInstanceStatus.STOPPED || instance.status === enumInstanceStatus.FAILED) && ( @@ -287,7 +290,7 @@ export const InstanceSection: FunctionComponent = () => { )} {/** console connection button, only show for RUNNING status */} - {instance.status === enumInstanceStatus.RUNNING && ( + {instance.appType !== enumAppType.JUPYTER && instance.status === enumInstanceStatus.RUNNING && ( // set the button color to green )} @@ -350,7 +353,7 @@ export const InstanceSection: FunctionComponent = () => { , - ]} @@ -358,15 +361,14 @@ export const InstanceSection: FunctionComponent = () => { {selectedInstance && selectedInstance.name && (selectedInstance.type === 'container' ? ( ) : ( ))} - + › ); diff --git a/packages/itmat-ui-react/src/components/lxd/lxd.instance.console.tsx b/packages/itmat-ui-react/src/components/lxd/lxd.instance.console.tsx index 33a7f9d35..e3c58ead6 100644 --- a/packages/itmat-ui-react/src/components/lxd/lxd.instance.console.tsx +++ b/packages/itmat-ui-react/src/components/lxd/lxd.instance.console.tsx @@ -1,6 +1,5 @@ -import React, { useState, useEffect, useRef} from 'react'; +import React, { useState, useEffect, useRef, useImperativeHandle, forwardRef} from 'react'; import { SpiceMainConn, handle_resize } from './spice/src/main'; -import 'xterm/css/xterm.css'; import {message} from 'antd'; import css from './lxd.module.css'; @@ -14,9 +13,12 @@ declare global { interface LXDConsoleProps { instanceName: string; - onMount: (handler: () => void) => void; } +export interface LXDConsoleRef { + handleFullScreen: () => void +} + const updateVgaConsoleSize = () => { const spiceScreen = document.getElementById('spice-screen'); if (spiceScreen) { @@ -35,9 +37,7 @@ const updateVgaConsoleSize = () => { }; -export const LXDConsole: React.FC = ({ instanceName, onMount}) => { - - console.log('Rendering LXDConsole'); +export const LXDConsole = forwardRef(({ instanceName }, ref) => { const spiceRef = useRef(null); const [isVgaLoading, setIsVgaLoading] = useState(false); @@ -60,6 +60,7 @@ export const LXDConsole: React.FC = ({ instanceName, onMount}) const height = spiceScreen ? spiceScreen.clientHeight : 768; // use trpc to get the console console + const res: any = await getInstanceConsole.mutateAsync({ container: instanceName, options: { @@ -108,7 +109,6 @@ export const LXDConsole: React.FC = ({ instanceName, onMount}) }; const handleFullScreen = () => { - console.log('2222 Entering full-screen mode'); const container = spiceRef.current; if (!container) { return; @@ -120,8 +120,13 @@ export const LXDConsole: React.FC = ({ instanceName, onMount}) message.error(`Failed to enter full-screen mode: ${JSON.stringify(e)}`); }); handleResize(); + + }; + useImperativeHandle(ref, () => ({ + handleFullScreen + })); useEffect(() => { @@ -131,7 +136,6 @@ export const LXDConsole: React.FC = ({ instanceName, onMount}) controlWebsocketPromise.then((controlWebsocket) => { if (controlWebsocket && controlWebsocket.readyState === WebSocket.OPEN) { controlWebsocket.close(); - console.log('WebSocket connection closed.'); } }).catch(e => { message.error(`Error closing WebSocket: ${JSON.stringify(e)}`); @@ -158,12 +162,6 @@ export const LXDConsole: React.FC = ({ instanceName, onMount}) }; }, [instanceName]); - useEffect(() => { - console.log('222222 attached the full-screen function to the onMount event.'); - onMount(handleFullScreen); - }, [instanceName, onMount]); - - return isVgaLoading ? (
Loading VGA console...
) : ( @@ -171,4 +169,4 @@ export const LXDConsole: React.FC = ({ instanceName, onMount})
); -}; \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/itmat-ui-react/src/components/lxd/lxd.instance.list.tsx b/packages/itmat-ui-react/src/components/lxd/lxd.instance.list.tsx index e07ecf470..d431b4316 100644 --- a/packages/itmat-ui-react/src/components/lxd/lxd.instance.list.tsx +++ b/packages/itmat-ui-react/src/components/lxd/lxd.instance.list.tsx @@ -277,10 +277,8 @@ const LXDInstanceList = () => { onOk={handleCloseModal} onCancel={handleCloseModal} style={{ - width: 'auto !important', - height: 'auto !important' + width: 'auto !important' }} - bodyStyle={{ height: 'calc(100vh - 110px)', overflowY: 'auto' }} className={css.modalOverrides} footer={null} closable={true} @@ -290,16 +288,14 @@ const LXDInstanceList = () => { > {selectedInstance && selectedTab === 'console' && (
- {selectedInstance.type === 'container' ? + {/* {selectedInstance.type === 'container' ? : // Render LXDTextConsole for containers // Render LXDConsole for virtual machines - } + } */}
)} diff --git a/packages/itmat-ui-react/src/components/lxd/lxd.instance.text.console.tsx b/packages/itmat-ui-react/src/components/lxd/lxd.instance.text.console.tsx index 8f0af046a..4f4ff5587 100644 --- a/packages/itmat-ui-react/src/components/lxd/lxd.instance.text.console.tsx +++ b/packages/itmat-ui-react/src/components/lxd/lxd.instance.text.console.tsx @@ -1,7 +1,9 @@ import React, { useState, useEffect, useRef, useLayoutEffect} from 'react'; -import { XTerm } from 'xterm-for-react'; -import { FitAddon } from 'xterm-addon-fit'; -import 'xterm/css/xterm.css'; +import { Terminal } from '@xterm/xterm'; +import '@xterm/xterm/css/xterm.css'; +// https://www.npmjs.com/package/@xterm/xterm +import { FitAddon } from '@xterm/addon-fit'; +// https://www.npmjs.com/package/@xterm/addon-fit import css from './lxd.module.css'; // import { useWebSocket } from 'react-use-websocket'; // Import the hook import { trpc } from '../../utils/trpc'; @@ -13,14 +15,16 @@ interface LXDTextConsoleProps { } const LXDTextConsole: React.FC = ({ instanceName}) => { - console.log('Rendering LXDTextConsole'); - const xtermRef = useRef(null); + const terminalRef = useRef(null); const textEncoder = new TextEncoder(); const [isLoading, setLoading] = useState(false); const [consoleBuffer, setConsoleBuffer] = useState(''); const [dataWs, setDataWs] = useState(null); - const [fitAddon] = useState(new FitAddon()); + // const [terminalInstanceRef.current] = useState(new Terminal()); + const terminalInstanceRef = useRef(null); + const fitAddonRef = useRef(null); + const isRendered = useRef(false); const getInstanceConsole = trpc.lxd.getInstanceConsole.useMutation(); const getInstanceConsolelog = trpc.lxd.getInstanceConsoleLog.useMutation(); @@ -34,79 +38,143 @@ const LXDTextConsole: React.FC = ({ instanceName}) => { container: instanceName }); if (result && result.data) { + console.log('Console buffer:', result.data); setConsoleBuffer(result.data); } } catch (error) { - message.error(`Failed to load console buffer: ${error}`); + console.log('Failed to load console buffer:', JSON.stringify(error)); + // message.error(`Failed to load console buffer: ${JSON.stringify(error)}`); } }; - const initiateConnection = async (width = 100, height = 100) => { + const initiateConnection = async ( width = 100, height = 100) => { setLoading(true); - fetchConsoleLogBuffer(); - // use trpc to get the console log buffer - const result: any = await getInstanceConsole.mutateAsync({ - container: instanceName, - options: { height, width, type: 'console' } // Ensure these options match your backend's expected input - }).catch((error) => { setLoading(false); throw new Error(`Failed to initiate console session: ${error}`); }); + try { + await fetchConsoleLogBuffer(); + // use trpc to get the console log buffer + const result: any = await getInstanceConsole.mutateAsync({ + container: instanceName, + options: { height, width, type: 'console' } // Ensure these options match your backend's expected input + }); - if (result.error) { - setLoading(false); - throw new Error(`Failed to initiate console session: ${result.error}`); - } + if (result.error) { + throw new Error(`Failed to initiate console session: ${result.error}`); + } - const baseUrl = new URL(window.location.href); //TODO import from the common module - baseUrl.protocol = baseUrl.protocol === 'https:' ? 'wss:' : 'ws:'; - baseUrl.pathname = '/rtc'; - const dataUrl = `${baseUrl.href}?t=d&o=${result.operationId}&s=${result.operationSecrets['0']}`; - const controlUrl = `${baseUrl.href}?t=c&o=${result.operationId}&s=${result.operationSecrets.control}`; + const baseUrl = new URL(window.location.href); //TODO import from the common module + baseUrl.protocol = baseUrl.protocol === 'https:' ? 'wss:' : 'ws:'; + baseUrl.pathname = '/rtc'; + const dataUrl = `${baseUrl.href}?t=d&o=${result.operationId}&s=${result.operationSecrets['0']}`; + const controlUrl = `${baseUrl.href}?t=c&o=${result.operationId}&s=${result.operationSecrets.control}`; + + const data_ws = new WebSocket(dataUrl); + const control_ws = new WebSocket(controlUrl); + + control_ws.onopen = () => { + console.log('Control websocket opened'); + setLoading(false); + }; + control_ws.onclose = () => { + console.log('Control websocket closed'); + }; + control_ws.onerror = (error) => { + console.error('Control WebSocket error:', error); + }; + control_ws.onmessage = (message) => { + console.log('Control message:', message); + }; + + + data_ws.onopen = () => { + console.log('Data websocket opened'); + setDataWs(data_ws); + initTerminal(data_ws, new Uint8Array(textEncoder.encode('test\r'))); + }; + data_ws.onerror = (error) => { + // show the message notification no more than 5 seconds + console.log('Data WebSocket error:', error); + message.error('Console connection error', 2); + }; + + data_ws.binaryType = 'arraybuffer'; + data_ws.onmessage = (message: MessageEvent) => { + console.log('Data message:', message); + if (terminalInstanceRef.current) { + terminalInstanceRef.current.write(new Uint8Array(message.data)); + } else { + console.error('Terminal instance is null'); + initTerminal(data_ws,new Uint8Array(message.data)); + } - const data_ws = new WebSocket(dataUrl); - const control_ws = new WebSocket(controlUrl); + }; + data_ws.onclose = () => { + setDataWs(null); + }; - control_ws.onopen = () => { + return [data_ws, control_ws]; + } catch (error) { + // console.error('Error initiating connection:', error); + message.error(`Error initiating connection: ${JSON.stringify(error)}`); setLoading(false); - }; - control_ws.onclose = () => { - console.log('Control websocket closed'); - }; - control_ws.onerror = (error) => { - console.error('Control WebSocket error:', error); - }; - control_ws.onmessage = (message) => { - console.log('Control message:', message); - }; - + return []; + } + }; - data_ws.onopen = () => { - setDataWs(data_ws); - }; - data_ws.onerror = (error) => { - // show the message notification no more than 5 seconds - message.error(`Console connection error: ${JSON.stringify(error)}`, 5); - }; + const initTerminal = (dataWs: WebSocket, initialMessage?: Uint8Array) => { + if (terminalRef.current && !terminalInstanceRef.current) { + console.log('Creating terminal instance'); + const terminal = new Terminal({ + convertEol: true, + cursorStyle: 'block', + cursorBlink: true, + fontFamily: 'monospace', + fontSize: 14, + fontWeight: '600', + theme: { + background: '#2b2b2b', + foreground: '#FFFFFF', + cursor: '#00FF00' + } + }); + const fitAddon = new FitAddon(); + terminal.loadAddon(fitAddon); + terminal.open(terminalRef.current); + terminal.onData((data) => { + console.log('Sending data:', data); + dataWs.send(textEncoder.encode(data)); + }); + // write stream to terminal with utf-8 encoding + terminal.writeln('Connecting to console...'); + if (initialMessage) { + terminal.write(initialMessage); + } - data_ws.binaryType = 'arraybuffer'; - data_ws.onmessage = (message: MessageEvent) => { - xtermRef.current?.terminal.writeUtf8(new Uint8Array(message.data)); - }; - data_ws.onclose = () => { - setDataWs(null); - }; + terminal.write(consoleBuffer); + setConsoleBuffer(''); - return [data_ws, control_ws]; + terminalInstanceRef.current = terminal; + fitAddonRef.current = fitAddon; + fitAddon.fit(); + terminal.focus(); + } }; useEffect(() => { - xtermRef.current?.terminal.focus(); - }, [xtermRef.current, instanceName]); + if (consoleBuffer && terminalInstanceRef.current && !isLoading) { + terminalInstanceRef.current.write(consoleBuffer); + setConsoleBuffer(''); + } + }, [consoleBuffer, isLoading]); useEffect(() => { - if (dataWs) return; - let isActive = true; - const container = document.querySelector('.console-modal .ant-modal-body'); + console.log('Rendering terminal'); + if (dataWs || isRendered.current ||!terminalRef.current) return; + console.log('Initiating connection', terminalRef.current); + isRendered.current = true; + + const container = terminalRef.current; const { clientWidth, clientHeight } = container || document.documentElement; const width = Math.floor(clientWidth / 9); const height = Math.floor(clientHeight / 17); @@ -114,37 +182,32 @@ const LXDTextConsole: React.FC = ({ instanceName}) => { const websocketPromise = initiateConnection(width, height); return () => { - isActive = false; - void websocketPromise.then((websockets) => { - if(!isActive) websockets?.map((ws) => ws.close()); + websocketPromise.then((websockets) => { + websockets?.forEach((ws) => ws.close()); }); + if (terminalInstanceRef.current) { + terminalInstanceRef.current.dispose(); + terminalInstanceRef.current = null; + } + if (fitAddonRef.current) { + fitAddonRef.current.dispose(); + fitAddonRef.current = null; + } }; + }, [instanceName]); - }, [xtermRef, fitAddon, instanceName]); - - useEffect(() => { - if (!consoleBuffer || !xtermRef.current || isLoading) { - return; + const handleResize = () => { + updateMaxHeight('p_terminal', undefined, 20); + try { + fitAddonRef.current?.fit(); + // terminalInstanceRef.current?.focus(); + } catch (error) { + console.error('Error fitting terminal:', error); } - xtermRef.current.terminal.write(consoleBuffer); - setConsoleBuffer(''); - } , [consoleBuffer, xtermRef, isLoading]); - - // Make sure to apply the fit only when the terminal is rendered and visible - useLayoutEffect(() => { - const handleResize = () => { - - if (xtermRef.current) { + }; - updateMaxHeight('p_terminal', undefined, 20); - try { - fitAddon.fit(); - } catch (error) { - console.error('Error fitting terminal:', error); - } - } - }; + useLayoutEffect(() => { // Listen to resize event window.addEventListener('resize', handleResize); @@ -158,20 +221,26 @@ const LXDTextConsole: React.FC = ({ instanceName}) => { }, []); // Empty dependency array ensures it runs once on mount and cleanup on unmount - return ( + const handleFullScreen = () => { + const container = terminalRef.current; + if (!container) { + return; + } + container + .requestFullscreen() + .then(handleResize) + .catch((e) => { + message.error(`Failed to enter full-screen mode: ${JSON.stringify(e)}`); + }); + handleResize(); + }; + return ( isLoading ? (

Loading text console...

) : (
- { - dataWs?.send(textEncoder.encode(data)); - }} - /> +
) ); diff --git a/packages/itmat-ui-react/src/components/lxd/lxd.module.css b/packages/itmat-ui-react/src/components/lxd/lxd.module.css index 488c51103..1f47f0787 100644 --- a/packages/itmat-ui-react/src/components/lxd/lxd.module.css +++ b/packages/itmat-ui-react/src/components/lxd/lxd.module.css @@ -104,14 +104,35 @@ max-height: 100%; } -.consoleContainer { +/* .consoleContainer { width: 100%; height: 100%; padding: 16px; background-color: #1e1e1e; color: white; box-sizing: border-box; + } */ + + html, body, #root { + height: 100%; + margin: 0; + padding: 0; + } + + .consoleContainer { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background-color: #000; + } + + .p_terminal { + flex-grow: 1; + width: 100%; + height: 100%; } + .pTerminalFullscreen {