diff --git a/less/__vms-list.less b/less/__vms-list.less
index 16e6bbcb..956b4df4 100644
--- a/less/__vms-list.less
+++ b/less/__vms-list.less
@@ -42,7 +42,8 @@
}
.__vms-list {
- .vms-list-header {
+ .vms-list-header,
+ .list-header {
.make-row(0);
background: @state-info-bg;
.select {
@@ -139,7 +140,7 @@
text-transform: uppercase;
&.stopped { background-color: @gray-light; color: #fff; }
&.failed { background-color: @brand-danger; color: #fff; }
- &.running { background-color: @brand-success; color: #fff; }
- &.destroyed { background-color: @gray-dark; color: #fff; }
+ &.running, &.ready { background-color: @brand-success; color: #fff; }
+ &.destroyed, &.unknown { background-color: @gray-dark; color: #fff; }
}
}
diff --git a/less/app.less b/less/app.less
index 117bbe94..03395b88 100644
--- a/less/app.less
+++ b/less/app.less
@@ -39,6 +39,7 @@
@import "vm.less";
@import "vms.less";
+@import "volumes.less";
@import "image.less";
@import "settings.less";
diff --git a/less/vm.less b/less/vm.less
index 66a12fb8..fe86f879 100644
--- a/less/vm.less
+++ b/less/vm.less
@@ -49,7 +49,7 @@ section.fwrules {
.dropdown-menu .delete { color: red; }
.resource-status {
- .running { background: @brand-success; }
+ .running, .ready { background: @brand-success; }
.failed { background: @brand-danger; }
.stopped { background: @gray-light; }
}
@@ -123,7 +123,8 @@ section.fwrules {
.server-uuid,
.owner-uuid,
.billing-id,
- .image-uuid {
+ .image-uuid,
+ .volume-uuid {
.uuid;
.pull-right;
}
diff --git a/less/volumes.less b/less/volumes.less
new file mode 100644
index 00000000..eb5598de
--- /dev/null
+++ b/less/volumes.less
@@ -0,0 +1,27 @@
+
+@import "vm";
+@import "networks";
+@import "__vms-list";
+
+.volumes-list {
+ .__vms-list;
+ td.status {width: 90px;}
+ a.delete {color: @state-danger-text; text-decoration: none; font-weight: bold; visibility: hidden; float: right;}
+ tr:hover .actions a, .actions:hover a {visibility: visible;}
+}
+
+#page-volume {
+ #page-vm;
+}
+.networks-list {
+ #page-networks > .networks-list;
+}
+
+#page-volume-form {
+ .help-block {
+ margin-bottom: 0;
+ }
+ .control-label.required {
+ font-weight: 600;
+ }
+}
diff --git a/lib/adminui.js b/lib/adminui.js
index 1492d673..6bfaf560 100644
--- a/lib/adminui.js
+++ b/lib/adminui.js
@@ -38,6 +38,13 @@ function resume(req, res, next) {
next();
}
+function checkVolApiConfig(req, res, next) {
+ if (req.sdc[req.dc].hasOwnProperty('volapi')) {
+ return next();
+ }
+ next(new restify.ServiceUnavailableError('Cannot connect to VOLAPI service. Please verify configuration.'));
+}
+
function setUfds(req, res, next) {
var trySetUfds = function () {
if (req.ufdsMaster.connected) {
@@ -176,8 +183,6 @@ ADMINUI.prototype.createServer = function () {
});
});
-
-
server.use(sdcClients.handler);
server.use((function attachOtherClients(req, res, next) {
@@ -283,6 +288,32 @@ ADMINUI.prototype.createServer = function () {
auth.requireRole('operators'),
vms.del);
+ var volumes = require('./volumes');
+ server.get('/api/volumes',
+ auth.requireAuth,
+ auth.requireRole('operators'),
+ checkVolApiConfig,
+ volumes.list);
+
+ server.get('/api/volumes/:uuid',
+ auth.requireAuth,
+ auth.requireRole('operators'),
+ checkVolApiConfig,
+ volumes.get);
+
+ server.post('/api/volumes',
+ auth.requireAuth,
+ auth.requireRole('operators'),
+ checkVolApiConfig,
+ resume,
+ bodyParser,
+ volumes.create);
+
+ server.del('/api/volumes/:uuid',
+ auth.requireAuth,
+ auth.requireRole('operators'),
+ checkVolApiConfig,
+ volumes.del);
var jobs = require('./jobs');
server.get('/api/jobs', auth.requireAuth, jobs.listJobs);
@@ -323,8 +354,6 @@ ADMINUI.prototype.createServer = function () {
images.aclAction);
-
-
var packages = require('./packages');
server.get('/api/packages', auth.requireAuth, packages.list);
server.get('/api/packages/:uuid', auth.requireAuth, packages.get);
@@ -909,7 +938,7 @@ ADMINUI.prototype.createServer = function () {
}
var method = req.method;
var body =
- !(method === 'POST' && route.name === 'authenticate') &&
+ !(method === 'POST' && route && route.name === 'authenticate') &&
!(method === 'GET' && Math.floor(res.statusCode/100) === 2);
auditLogger({
log: req.log.child({ component: 'audit', route: route && route.name }, true),
diff --git a/lib/auth.js b/lib/auth.js
index 762e1cd2..804abba3 100644
--- a/lib/auth.js
+++ b/lib/auth.js
@@ -18,7 +18,8 @@ var LOCKED_TIME = 4 * 60 * 60;
var userLockTimes = {};
var userLoginAttempts = {};
-var MANTA_ENALBED = require('../etc/config.json')['manta'] || false;
+var config = require('../etc/config.json');
+var MANTA_ENABLED = config.manta || false;
var Auth = module.exports = {};
Auth.optionalAuth = function optionalAuth(req, res, next) {
@@ -189,10 +190,12 @@ Auth.authenticate = function authenticate(req, res, next) {
return next(new restify.ConflictError('User does not have permission to use Operations Portal.'));
}
+ var isVolapiEnabled = !!(config.datacenters[req.dc] && config.datacenters[req.dc].volapi);
req.sessions.create({
roles: user.groups(),
user: user.uuid,
- manta: MANTA_ENALBED
+ manta: MANTA_ENABLED,
+ volapiEnabled: isVolapiEnabled
}, function (err2, token) {
if (err2) {
req.log.info(err2, 'error creating session');
@@ -203,7 +206,8 @@ Auth.authenticate = function authenticate(req, res, next) {
user: user,
token: token,
adminUuid: req.adminUuid,
- manta: MANTA_ENALBED,
+ manta: MANTA_ENABLED,
+ volapiEnabled: isVolapiEnabled,
dc: req.dc,
roles: user.groups()
});
diff --git a/lib/sdc-clients.js b/lib/sdc-clients.js
index a0a893e7..377de3ba 100644
--- a/lib/sdc-clients.js
+++ b/lib/sdc-clients.js
@@ -76,6 +76,10 @@ module.exports.createClients = function (config) {
clients[dc].imgapi = new IMGAPI(cfg.imgapi);
clients[dc].papi = new SDC.PAPI(cfg.papi);
+ if (cfg.volapi) {
+ clients[dc].volapi = restify.createJsonClient(cfg.volapi);
+ }
+
cfg.moray.no_count = true;
clients[dc].moray = createMorayClient({
diff --git a/lib/volumes.js b/lib/volumes.js
new file mode 100644
index 00000000..39b82f05
--- /dev/null
+++ b/lib/volumes.js
@@ -0,0 +1,98 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/*
+ * Copyright (c) 2016, Joyent, Inc.
+ */
+
+var sprintf = require('util').format;
+
+var getVolumeParams = function (params) {
+ var query = params.query || [];
+
+ if (params.type && params.type.length) {
+ query.push(sprintf('(type=*%s*)', params.type));
+ delete params.type;
+ }
+
+ if (params.state && params.state.length) {
+ query.push(sprintf('(state=%s)', params.state));
+ delete params.state;
+ }
+
+ if (params.tag && params.tag.length) {
+ query.push(sprintf('(tag=%s)', params.tag));
+ delete params.tag;
+ }
+
+ if (query.length) {
+ params.filter = '(&' + query.join('') + ')';
+ }
+
+ return params;
+};
+
+function list(req, res, next) {
+ var params = getVolumeParams(req.params);
+ var opts = {
+ path: '/volumes',
+ query: params
+ };
+ req.sdc[req.dc].volapi.get(opts, function (err, _req, _res, volumes) {
+ if (err) {
+ req.log.error(err, 'Error retrieving volumes');
+ return next(err);
+ }
+ if (params.uuid && params.uuid.length === 36) {
+ volumes = volumes.filter(function (volume) {
+ return volume.uuid === params.uuid;
+ });
+ }
+ res.send(volumes);
+ return next();
+ });
+}
+
+function get(req, res, next) {
+ req.sdc[req.dc].volapi.get('/volumes/' + req.params.uuid, function (err, _req, _res, volume) {
+ if (err) {
+ req.log.error(err, 'Error retrieving volume');
+ return next(err);
+ }
+ res.send(volume);
+ return next();
+ });
+}
+
+function create(req, res, next) {
+ req.log.info(req.body, 'VOLAPI Create Request');
+ req.sdc[req.dc].volapi.post('/volumes', req.body, function (err, _req, _res, volume) {
+ if (err) {
+ req.log.error(err, 'Error creating volume');
+ return next(err);
+ }
+ res.send(volume);
+ return next();
+ });
+}
+
+function del(req, res, next) {
+ req.sdc[req.dc].volapi.del('/volumes/' + req.params.uuid, function (err, _req, _res, volume) {
+ if (err) {
+ req.log.error(err, 'Error deleting volume');
+ return next(err);
+ }
+ res.send(volume);
+ return next();
+ });
+}
+
+module.exports = {
+ list: list,
+ get: get,
+ create: create,
+ del: del
+};
diff --git a/www/js/components/error-alert.jsx b/www/js/components/error-alert.jsx
index 6cc37976..f3a8aa55 100644
--- a/www/js/components/error-alert.jsx
+++ b/www/js/components/error-alert.jsx
@@ -5,30 +5,32 @@
*/
/*
- * Copyright (c) 2014, Joyent, Inc.
+ * Copyright (c) 2016, Joyent, Inc.
*/
-"use strict";
+'use strict';
var React = require('react');
var ErrorAlert = React.createClass({
- propTypes: {
- error: React.PropTypes.object
- },
- render: function() {
+ render: function () {
var error = this.props.error;
-
- if (!error || !error.code) {
- return
;
+ if (!error) {
+ return ;
}
+ var errorMessage = error.message || (error.errors ? null : error);
+
return (
{
- error.message ?
{error.message}
: ''
+ errorMessage ?
{errorMessage}
: ''
}
{
- error.errors && error.errors.length ?
{error.errors.map(function(err) {
- return - {err.field} - {err.message}
;
- }) }
: ''
+ error.errors && error.errors.length ?
+ {
+ error.errors.map(function (err) {
+ return - {err.field} - {err.message}
;
+ })
+ }
+
: ''
}
);
}
diff --git a/www/js/components/filter-form.jsx b/www/js/components/filter-form.jsx
index a5f4ce28..671a6dca 100644
--- a/www/js/components/filter-form.jsx
+++ b/www/js/components/filter-form.jsx
@@ -8,13 +8,27 @@ var utils = require('../lib/utils');
var NicTags = require('../models/nictags');
var FabricVlans = require('../models/fabrics-vlans');
var INPUT_TYPES = {};
+var DEFAULT_WIDTH = {
+ uuid: 4,
+ owner_uuid: 4,
+ alias: 3,
+ name: 3,
+ package_name: 3,
+ state: 3,
+ tag: 3,
+ type: 3,
+ volume_state: 3,
+ ip: 2,
+ nic_tag: 2,
+ vlan_id: 2
+};
INPUT_TYPES.uuid = React.createClass({
onChange: function (e) {
this.props.onChange('uuid', e.target.value);
},
render: function () {
- return (
+ return (
);
@@ -26,7 +40,7 @@ INPUT_TYPES.alias = React.createClass({
this.props.onChange('alias', e.target.value);
},
render: function () {
- return (
+ return (
);
@@ -42,7 +56,7 @@ INPUT_TYPES.package_name = React.createClass({
this.typeahead.remove();
},
render: function () {
- return (
+ return (
);
@@ -54,20 +68,43 @@ INPUT_TYPES.name = React.createClass({
this.props.onChange('name', e.target.value);
},
render: function () {
- return (
+ return (
);
}
});
+INPUT_TYPES.tag = React.createClass({
+ onChange: function (e) {
+ this.props.onChange('tag', e.target.value);
+ },
+ render: function () {
+ return (
+
+
+
);
+ }
+});
+
+INPUT_TYPES.type = React.createClass({
+ onChange: function (e) {
+ this.props.onChange('type', e.target.value);
+ },
+ render: function () {
+ return (
+
+
+
);
+ }
+});
INPUT_TYPES.ip = React.createClass({
onChange: function (e) {
this.props.onChange('ip', e.target.value);
},
render: function () {
- return (
+ return (
);
@@ -106,7 +143,7 @@ INPUT_TYPES.state = React.createClass({
this.props.onChange('state', e.target.value);
},
render: function () {
- return (
+ return (