summaryrefslogtreecommitdiffstats
path: root/app/server-control
diff options
context:
space:
mode:
authorGunnar Mills <gmills@us.ibm.com>2019-09-19 14:12:16 -0500
committerGunnar Mills <gmills@us.ibm.com>2019-09-24 14:16:14 +0000
commit418db63c77aad03fe3401c7acd9f9792fab96a68 (patch)
tree79f995939cf688fe3d45c9b9b48931d7ca0afea8 /app/server-control
parent5ccf9e36c1da9b6396288c3cfe8d32dddfbb1a10 (diff)
downloadphosphor-webui-418db63c77aad03fe3401c7acd9f9792fab96a68.tar.gz
phosphor-webui-418db63c77aad03fe3401c7acd9f9792fab96a68.zip
Move Virtual Media files and route
"Virtual media" was already part of the "Server control" submenu but the files were at app/configuration and the route was /configuration. Marta Mazur did a similar file and route move here as part of supporting multiple Virtual Media endpoints: https://gerrit.openbmc-project.xyz/c/openbmc/phosphor-webui/+/25218 Tested: Built and loaded on a Witherspoon. The VM route is now: #/server-control/virtual-media. Was able to use the VM panel. Change-Id: Iec46a8ece7ddfbc356bc1e71875def70c4418027 Signed-off-by: Gunnar Mills <gmills@us.ibm.com>
Diffstat (limited to 'app/server-control')
-rw-r--r--app/server-control/controllers/virtual-media-controller.html38
-rw-r--r--app/server-control/controllers/virtual-media-controller.js391
-rw-r--r--app/server-control/index.js6
-rw-r--r--app/server-control/styles/index.scss1
-rw-r--r--app/server-control/styles/virtual-media.scss82
5 files changed, 518 insertions, 0 deletions
diff --git a/app/server-control/controllers/virtual-media-controller.html b/app/server-control/controllers/virtual-media-controller.html
new file mode 100644
index 0000000..e756986
--- /dev/null
+++ b/app/server-control/controllers/virtual-media-controller.html
@@ -0,0 +1,38 @@
+<primary>
+ <h1>Virtual media</h1>
+ <p class="vm__page-description" ng-if="devices.length >= 1">Specify image file location to start session.</p>
+ <p ng-if="devices.length < 1">
+ There are no Virtual Media devices available.
+ </primary>
+ <div ng-repeat="device in devices track by $index" class="vm__upload">
+ <h2 class="h3">{{device.deviceName}}</h2>
+ <div class="vm__upload-chooser">
+ <!-- name and error message -->
+ <div class="vm__upload-content">
+ <div class="vm__upload-controls">
+ <!-- Button -->
+ <label class="vm__upload-choose-label">
+ <input id="file-upload" type="file" file="device.file" class="hide" ng-disabled="device.isActive"/>
+ <span class="vm__upload-choose-button btn btn-secondary" ng-class="{disabled:device.isActive}">Choose file</span>
+ </label>
+ <div class="vm__upload-name">
+ <span ng-if="!device.file">No file selected</span>
+ <span ng-if="device.file.name !== undefined">{{device.file.name}}</span>
+ <span class="icon__exit" ng-if="device.file && !device.isActive" ng-click="resetFile($index);">
+ <icon file="icon-close.svg"></icon>
+ </span>
+ </div>
+ </div>
+ <div class="vm__active-text vm__active-border" ng-if="device.isActive">
+ <span>Active Session</span>
+ </div>
+ <div class="vm__active-text vm__error-border" ng-if="device.hasError">
+ <span>Error in connecting to the selected file</span>
+ </div>
+ </div>
+ <div class="vm__upload-start">
+ <input type="button" ng-value="device.isActive ? 'Stop' : 'Start'" ng-class="{disabled:!device.file}" class="btn btn-primary" ng-click="device.isActive? stopVM($index) : startVM($index)"/>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/app/server-control/controllers/virtual-media-controller.js b/app/server-control/controllers/virtual-media-controller.js
new file mode 100644
index 0000000..19c7e73
--- /dev/null
+++ b/app/server-control/controllers/virtual-media-controller.js
@@ -0,0 +1,391 @@
+/**
+ * Controller for virtual-media
+ *
+ * @module app/serverControl
+ * @exports virtualMediaController
+ * @name virtualMediaController
+ */
+
+window.angular && (function(angular) {
+ 'use strict';
+
+ angular.module('app.serverControl').controller('virtualMediaController', [
+ '$scope', 'APIUtils', 'toastService', 'dataService', 'nbdServerService',
+ function($scope, APIUtils, toastService, dataService, nbdServerService) {
+ $scope.devices = [];
+
+ // Only one Virtual Media WebSocket device is currently available.
+ // Path is /vm/0/0.
+ // TODO: Support more than 1 VM device, when backend support is added.
+ var vmDevice = {};
+ // Hardcode to 0 since /vm/0/0. Last 0 is the device ID.
+ // To support more than 1 device ID, replace with a call to get the
+ // device IDs and names.
+ vmDevice.id = 0;
+ vmDevice.deviceName = 'Virtual media device';
+ findExistingConnection(vmDevice);
+ $scope.devices.push(vmDevice);
+
+ $scope.startVM = function(index) {
+ $scope.devices[index].isActive = true;
+ var file = $scope.devices[index].file;
+ var id = $scope.devices[index].id;
+ var host = dataService.getHost().replace('https://', '');
+ var server = new NBDServer('wss://' + host + '/vm/0/' + id, file, id);
+ $scope.devices[index].nbdServer = server;
+ nbdServerService.addConnection(id, server, file);
+ server.start();
+ };
+ $scope.stopVM = function(index) {
+ $scope.devices[index].isActive = false;
+ var server = $scope.devices[index].nbdServer;
+ server.stop();
+ };
+
+ $scope.resetFile = function(index) {
+ document.getElementById('file-upload').value = '';
+ $scope.devices[index].file = '';
+ };
+
+ function findExistingConnection(vmDevice) {
+ // Checks with existing connections kept in nbdServerService for an open
+ // Websocket connection.
+ var existingConnectionsMap = nbdServerService.getExistingConnections();
+ if (existingConnectionsMap.hasOwnProperty(vmDevice.id)) {
+ // Open ws will have a ready state of 1
+ if (existingConnectionsMap[vmDevice.id].server.ws.readyState === 1) {
+ vmDevice.isActive = true;
+ vmDevice.file = existingConnectionsMap[vmDevice.id].file;
+ vmDevice.nbdServer = existingConnectionsMap[vmDevice.id].server;
+ }
+ }
+ return vmDevice;
+ }
+ }
+ ]);
+})(angular);
+
+/* handshake flags */
+const NBD_FLAG_FIXED_NEWSTYLE = 0x1;
+const NBD_FLAG_NO_ZEROES = 0x2;
+
+/* transmission flags */
+const NBD_FLAG_HAS_FLAGS = 0x1;
+const NBD_FLAG_READ_ONLY = 0x2;
+
+/* option negotiation */
+const NBD_OPT_EXPORT_NAME = 0x1;
+const NBD_REP_FLAG_ERROR = 0x1 << 31;
+const NBD_REP_ERR_UNSUP = NBD_REP_FLAG_ERROR | 1;
+
+/* command definitions */
+const NBD_CMD_READ = 0;
+const NBD_CMD_WRITE = 1;
+const NBD_CMD_DISC = 2;
+const NBD_CMD_TRIM = 4;
+
+/* errno */
+const EPERM = 1;
+const EIO = 5;
+const EINVAL = 22;
+const ENOSPC = 28;
+
+/* internal object state */
+const NBD_STATE_UNKNOWN = 1;
+const NBD_STATE_OPEN = 2;
+const NBD_STATE_WAIT_CFLAGS = 3;
+const NBD_STATE_WAIT_OPTION = 4;
+const NBD_STATE_TRANSMISSION = 5;
+
+function NBDServer(endpoint, file, id) {
+ this.file = file;
+ this.id = id;
+ this.endpoint = endpoint;
+ this.ws = null;
+ this.state = NBD_STATE_UNKNOWN;
+ this.msgbuf = null;
+
+ this.start = function() {
+ this.ws = new WebSocket(this.endpoint);
+ this.state = NBD_STATE_OPEN;
+ this.ws.binaryType = 'arraybuffer';
+ this.ws.onmessage = this._on_ws_message.bind(this);
+ this.ws.onopen = this._on_ws_open.bind(this);
+ this.ws.onclose = this._on_ws_close.bind(this);
+ this.ws.onerror = this._on_ws_error.bind(this);
+ };
+
+ this.stop = function() {
+ this.ws.close();
+ this.state = NBD_STATE_UNKNOWN;
+ };
+
+ this._on_ws_error = function(ev) {
+ console.log('vm/0/' + id + 'error: ' + ev);
+ };
+
+ this._on_ws_close = function(ev) {
+ console.log(
+ 'vm/0/' + id + ' closed with code: ' + ev.code +
+ ' reason: ' + ev.reason);
+ };
+
+ /* websocket event handlers */
+ this._on_ws_open = function(ev) {
+ console.log('vm/0/' + id + ' opened');
+ this.client = {
+ flags: 0,
+ };
+ this._negotiate();
+ };
+
+ this._on_ws_message = function(ev) {
+ var data = ev.data;
+
+ if (this.msgbuf == null) {
+ this.msgbuf = data;
+ } else {
+ var tmp = new Uint8Array(this.msgbuf.byteLength + data.byteLength);
+ tmp.set(new Uint8Array(this.msgbuf), 0);
+ tmp.set(new Uint8Array(data), this.msgbuf.byteLength);
+ this.msgbuf = tmp.buffer;
+ }
+
+ for (;;) {
+ var handler = this.recv_handlers[this.state];
+ if (!handler) {
+ console.log('no handler for state ' + this.state);
+ this.stop();
+ break;
+ }
+
+ var consumed = handler(this.msgbuf);
+ if (consumed < 0) {
+ console.log(
+ 'handler[state=' + this.state + '] returned error ' + consumed);
+ this.stop();
+ break;
+ }
+
+ if (consumed == 0) {
+ break;
+ }
+
+ if (consumed > 0) {
+ if (consumed == this.msgbuf.byteLength) {
+ this.msgbuf = null;
+ break;
+ }
+ this.msgbuf = this.msgbuf.slice(consumed);
+ }
+ }
+ };
+
+ this._negotiate = function() {
+ var buf = new ArrayBuffer(18);
+ var data = new DataView(buf, 0, 18);
+
+ /* NBD magic: NBDMAGIC */
+ data.setUint32(0, 0x4e42444d);
+ data.setUint32(4, 0x41474943);
+
+ /* newstyle negotiation: IHAVEOPT */
+ data.setUint32(8, 0x49484156);
+ data.setUint32(12, 0x454F5054);
+
+ /* flags: fixed newstyle negotiation, no padding */
+ data.setUint16(16, NBD_FLAG_FIXED_NEWSTYLE | NBD_FLAG_NO_ZEROES);
+
+ this.state = NBD_STATE_WAIT_CFLAGS;
+ this.ws.send(buf);
+ };
+
+ /* handlers */
+ this._handle_cflags = function(buf) {
+ if (buf.byteLength < 4) {
+ return 0;
+ }
+
+ var data = new DataView(buf, 0, 4);
+ this.client.flags = data.getUint32(0);
+
+ this.state = NBD_STATE_WAIT_OPTION;
+ return 4;
+ };
+
+ this._handle_option = function(buf) {
+ if (buf.byteLength < 16) return 0;
+
+ var data = new DataView(buf, 0, 16);
+ if (data.getUint32(0) != 0x49484156 || data.getUint32(4) != 0x454F5054) {
+ console.log('invalid option magic');
+ return -1;
+ }
+
+ var opt = data.getUint32(8);
+ var len = data.getUint32(12);
+
+
+ if (buf.byteLength < 16 + len) {
+ return 0;
+ }
+
+ switch (opt) {
+ case NBD_OPT_EXPORT_NAME:
+ var n = 10;
+ if (!(this.client.flags & NBD_FLAG_NO_ZEROES)) n += 124;
+ var resp = new ArrayBuffer(n);
+ var view = new DataView(resp, 0, 10);
+ /* export size. */
+ var size = this.file.size;
+ view.setUint32(0, Math.floor(size / (2 ** 32)));
+ view.setUint32(4, size & 0xffffffff);
+ /* transmission flags: read-only */
+ view.setUint16(8, NBD_FLAG_HAS_FLAGS | NBD_FLAG_READ_ONLY);
+ this.ws.send(resp);
+
+ this.state = NBD_STATE_TRANSMISSION;
+ break;
+
+ default:
+ console.log('handle_option: Unsupported option: ' + opt);
+ /* reject other options */
+ var resp = new ArrayBuffer(20);
+ var view = new DataView(resp, 0, 20);
+ view.setUint32(0, 0x0003e889);
+ view.setUint32(4, 0x045565a9);
+ view.setUint32(8, opt);
+ view.setUint32(12, NBD_REP_ERR_UNSUP);
+ view.setUint32(16, 0);
+ this.ws.send(resp);
+ }
+
+ return 16 + len;
+ };
+
+ this._create_cmd_response = function(req, rc, data = null) {
+ var len = 16;
+ if (data) len += data.byteLength;
+ var resp = new ArrayBuffer(len);
+ var view = new DataView(resp, 0, 16);
+ view.setUint32(0, 0x67446698);
+ view.setUint32(4, rc);
+ view.setUint32(8, req.handle_msB);
+ view.setUint32(12, req.handle_lsB);
+ if (data) new Uint8Array(resp, 16).set(new Uint8Array(data));
+ return resp;
+ };
+
+ this._handle_cmd = function(buf) {
+ if (buf.byteLength < 28) {
+ return 0;
+ }
+
+ var view = new DataView(buf, 0, 28);
+
+ if (view.getUint32(0) != 0x25609513) {
+ console.log('invalid request magic');
+ return -1;
+ }
+
+ var req = {
+ flags: view.getUint16(4),
+ type: view.getUint16(6),
+ handle_msB: view.getUint32(8),
+ handle_lsB: view.getUint32(12),
+ offset_msB: view.getUint32(16),
+ offset_lsB: view.getUint32(20),
+ length: view.getUint32(24),
+ };
+
+ /* we don't support writes, so nothing needs the data at present */
+ /* req.data = buf.slice(28); */
+
+ var err = 0;
+ var consumed = 28;
+
+ /* the command handlers return 0 on success, and send their
+ * own response. Otherwise, a non-zero error code will be
+ * used as a simple error response
+ */
+ switch (req.type) {
+ case NBD_CMD_READ:
+ err = this._handle_cmd_read(req);
+ break;
+
+ case NBD_CMD_DISC:
+ err = this._handle_cmd_disconnect(req);
+ break;
+
+ case NBD_CMD_WRITE:
+ /* we also need length bytes of data to consume a write
+ * request */
+ if (buf.byteLength < 28 + req.length) {
+ return 0;
+ }
+ consumed += req.length;
+ err = EPERM;
+ break;
+
+ case NBD_CMD_TRIM:
+ err = EPERM;
+ break;
+
+ default:
+ console.log('invalid command 0x' + req.type.toString(16));
+ err = EINVAL;
+ }
+
+ if (err) {
+ console.log('error handle_cmd: ' + err);
+ var resp = this._create_cmd_response(req, err);
+ this.ws.send(resp);
+ }
+
+ return consumed;
+ };
+
+ this._handle_cmd_read = function(req) {
+ var offset;
+
+ offset = (req.offset_msB * 2 ** 32) + req.offset_lsB;
+
+ if (offset > Number.MAX_SAFE_INTEGER) return ENOSPC;
+
+ if (offset + req.length > Number.MAX_SAFE_INTEGER) return ENOSPC;
+
+ if (offset + req.length > file.size) return ENOSPC;
+
+ var blob = this.file.slice(offset, offset + req.length);
+ var reader = new FileReader();
+
+ reader.onload = (function(ev) {
+ var reader = ev.target;
+ if (reader.readyState != FileReader.DONE) return;
+ var resp =
+ this._create_cmd_response(req, 0, reader.result);
+ this.ws.send(resp);
+ }).bind(this);
+
+ reader.onerror = (function(ev) {
+ var reader = ev.target;
+ console.log('error reading file: ' + reader.error);
+ var resp = this._create_cmd_response(req, EIO);
+ this.ws.send(resp);
+ }).bind(this);
+ reader.readAsArrayBuffer(blob);
+
+ return 0;
+ };
+
+ this._handle_cmd_disconnect = function(req) {
+ this.stop();
+ return 0;
+ };
+
+ this.recv_handlers = Object.freeze({
+ [NBD_STATE_WAIT_CFLAGS]: this._handle_cflags.bind(this),
+ [NBD_STATE_WAIT_OPTION]: this._handle_option.bind(this),
+ [NBD_STATE_TRANSMISSION]: this._handle_cmd.bind(this),
+ });
+}
diff --git a/app/server-control/index.js b/app/server-control/index.js
index 1b8aad5..8bf03b4 100644
--- a/app/server-control/index.js
+++ b/app/server-control/index.js
@@ -53,6 +53,12 @@ window.angular && (function(angular) {
'controller': 'kvmController',
authenticated: true
})
+ .when('/server-control/virtual-media', {
+ 'template':
+ require('./controllers/virtual-media-controller.html'),
+ 'controller': 'virtualMediaController',
+ authenticated: true
+ })
.when('/server-control', {
'template':
require('./controllers/power-operations-controller.html'),
diff --git a/app/server-control/styles/index.scss b/app/server-control/styles/index.scss
index 7171a7e..cd91860 100644
--- a/app/server-control/styles/index.scss
+++ b/app/server-control/styles/index.scss
@@ -3,3 +3,4 @@
@import "./remote-console.scss";
@import "./server-led.scss";
@import "./kvm.scss";
+@import "./virtual-media.scss";
diff --git a/app/server-control/styles/virtual-media.scss b/app/server-control/styles/virtual-media.scss
new file mode 100644
index 0000000..4070e34
--- /dev/null
+++ b/app/server-control/styles/virtual-media.scss
@@ -0,0 +1,82 @@
+.vm__page-description {
+ margin-top: 1em;
+ margin-bottom: 3em;
+}
+
+.vm__upload {
+ align-items: center;
+ margin-bottom: 2em;
+}
+
+.vm__upload-chooser {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ margin: 0.7em 0.7em 0.7em 0;
+}
+.vm__upload-choose-label {
+ flex-grow: 0;
+ flex-shrink: 0;
+ flex-basis: auto;
+ margin-right: 4px;
+}
+
+.vm__upload-choose-button {
+ padding: 0.5em 0.75em;
+ font-size: 1rem;
+ background-color: $primary-light;
+ min-width: 3em;
+}
+
+.vm__upload-content {
+ flex: 1 0 220px;
+ max-width: 640px;
+
+ .icon__exit {
+ float: right;
+ cursor: pointer;
+ width: 0.75em;
+ height: 0.75em;
+ margin: 0 1em 0.3em 0.3em;
+ }
+}
+
+.vm__upload-controls {
+ display: flex;
+ align-items: center;
+}
+
+.vm__upload-name {
+ flex: 1 0 220px;
+ background-color: $background-03;
+ padding: 0.5em;
+}
+
+.vm__error-border,
+.vm__active-border {
+ border-top: 2px solid $status-error;
+ margin-top: 4px;
+ padding-top: 4px;
+}
+.vm__active-border {
+ border-color: $status-ok;
+}
+
+.vm__active-text {
+ color: $status-ok;
+ font-size: 0.8rem;
+ margin-top: 4px;
+}
+
+.vm__upload-start {
+ flex-grow: 0;
+ flex-shrink: 0;
+ flex-basis: 100%;
+ margin-top: 1em;
+
+ @media screen and (min-width: 760px) {
+ flex-basis: auto;
+ margin-top: 0;
+ margin-left: 0.75em;
+ }
+}
OpenPOWER on IntegriCloud