1543 lines
50 KiB
JavaScript
1543 lines
50 KiB
JavaScript
/*
|
|
* Copyright 2013 Mirantis, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
* not use this file except in compliance with the License. You may obtain
|
|
* a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations
|
|
* under the License.
|
|
**/
|
|
|
|
import $ from 'jquery';
|
|
import _ from 'underscore';
|
|
import i18n from 'i18n';
|
|
import Backbone from 'backbone';
|
|
import Expression from 'expression';
|
|
import {ModelPath} from 'expression/objects';
|
|
import utils from 'utils';
|
|
import customControls from 'views/custom_controls';
|
|
import 'deep-model';
|
|
|
|
var models = {};
|
|
|
|
var superMixin = models.superMixin = {
|
|
_super(method, args) {
|
|
var object = this;
|
|
while (object[method] === this[method]) object = object.constructor.__super__;
|
|
return object[method].apply(this, args || []);
|
|
}
|
|
};
|
|
|
|
// Mixin for adjusting some collection functions to work properly with model.get.
|
|
// Lodash supports some methods with predicate objects, not functions.
|
|
// Underscore has only pure predicate functions.
|
|
// We need to convert predicate objects to functions that use model's
|
|
// get functionality -- otherwise model.property always returns undefined.
|
|
|
|
var collectionMixin = {
|
|
getByIds(ids) {
|
|
return this.filter((model) => _.contains(ids, model.id));
|
|
}
|
|
};
|
|
|
|
var collectionMethods = [
|
|
'dropRightWhile', 'dropWhile', 'takeRightWhile', 'takeWhile',
|
|
'findIndex', 'findLastIndex',
|
|
'findKey', 'findLastKey',
|
|
'find', 'detect', 'findLast',
|
|
'filter', 'select', 'reject',
|
|
'every', 'all', 'some', 'any',
|
|
'partition'
|
|
];
|
|
|
|
_.each(collectionMethods, (method) => {
|
|
collectionMixin[method] = function() {
|
|
var args = _.toArray(arguments);
|
|
var source = args[0];
|
|
|
|
if (_.isPlainObject(source)) {
|
|
args[0] = (model) => _.isMatch(model.attributes, source);
|
|
}
|
|
|
|
args.unshift(this.models);
|
|
|
|
return _[method](...args);
|
|
};
|
|
});
|
|
|
|
var BaseModel = models.BaseModel = Backbone.Model.extend(superMixin);
|
|
var BaseCollection = models.BaseCollection =
|
|
Backbone.Collection.extend(collectionMixin).extend(superMixin);
|
|
|
|
var cacheMixin = {
|
|
fetch(options) {
|
|
if (this.cacheFor && options && options.cache && this.lastSyncTime &&
|
|
(this.cacheFor > (new Date() - this.lastSyncTime))) {
|
|
return $.Deferred().resolve();
|
|
}
|
|
return this._super('fetch', arguments);
|
|
},
|
|
sync() {
|
|
var deferred = this._super('sync', arguments);
|
|
if (this.cacheFor) {
|
|
deferred.done(() => {
|
|
this.lastSyncTime = new Date();
|
|
});
|
|
}
|
|
return deferred;
|
|
},
|
|
cancelThrottling() {
|
|
delete this.lastSyncTime;
|
|
}
|
|
};
|
|
models.cacheMixin = cacheMixin;
|
|
|
|
var restrictionMixin = models.restrictionMixin = {
|
|
checkRestrictions(models, action, setting) {
|
|
var restrictions = _.map(setting ? setting.restrictions : this.get('restrictions'),
|
|
utils.expandRestriction);
|
|
if (action) {
|
|
restrictions = _.where(restrictions, {action: action});
|
|
}
|
|
var satisfiedRestrictions = _.filter(restrictions,
|
|
(restriction) => new Expression(restriction.condition, models, restriction).evaluate()
|
|
);
|
|
return {
|
|
result: !!satisfiedRestrictions.length,
|
|
message: _.compact(_.pluck(satisfiedRestrictions, 'message')).join(' ')
|
|
};
|
|
},
|
|
expandLimits(limits) {
|
|
this.expandedLimits = this.expandedLimits || {};
|
|
this.expandedLimits[this.get('name')] = limits;
|
|
},
|
|
checkLimits(models, nodes, checkLimitIsReached = true, limitTypes = ['min', 'max']) {
|
|
/*
|
|
* Check the 'limits' section of configuration.
|
|
* models -- current models to check the limits
|
|
* nodes -- node collection to check the limits
|
|
* checkLimitIsReached -- boolean (default: true), if true then for min = 1, 1 node is allowed
|
|
* if false, then for min = 1, 1 node is not allowed anymore
|
|
* This is because validation runs in 2 modes: validate current model as is
|
|
* and validate current model checking the possibility of adding/removing node
|
|
* So if max = 1 and we have 1 node then:
|
|
* - the model is valid as is (return true) -- case for checkLimitIsReached = true
|
|
* - there can be no more nodes added (return false) -- case for
|
|
* checkLimitIsReached = false
|
|
* limitType -- array of limit types to check. Possible choices are 'min', 'max', 'recommended'
|
|
**/
|
|
|
|
var evaluateExpressionHelper = (expression, models, options) => {
|
|
var ret;
|
|
|
|
if (_.isUndefined(expression)) {
|
|
return {value: undefined, modelPaths: {}};
|
|
} else if (_.isNumber(expression)) {
|
|
return {value: expression, modelPaths: {}};
|
|
}
|
|
|
|
ret = utils.evaluateExpression(expression, models, options);
|
|
|
|
if (ret.value instanceof ModelPath) {
|
|
ret.value = ret.value.model.get(ret.value.attribute);
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
var checkedLimitTypes = {};
|
|
var name = this.get('name');
|
|
var limits = this.expandedLimits[name] || {};
|
|
var overrides = limits.overrides || [];
|
|
var limitValues = {
|
|
max: evaluateExpressionHelper(limits.max, models).value,
|
|
min: evaluateExpressionHelper(limits.min, models).value,
|
|
recommended: evaluateExpressionHelper(limits.recommended, models).value
|
|
};
|
|
var count = nodes.nodesAfterDeploymentWithRole(name).length;
|
|
var messages;
|
|
var label = this.get('label');
|
|
|
|
var checkOneLimit = (obj, limitType) => {
|
|
var limitValue, comparator;
|
|
|
|
if (_.isUndefined(obj[limitType])) {
|
|
return null;
|
|
}
|
|
switch (limitType) {
|
|
case 'min':
|
|
comparator = checkLimitIsReached ? (a, b) => a < b : (a, b) => a <= b;
|
|
break;
|
|
case 'max':
|
|
comparator = checkLimitIsReached ? (a, b) => a > b : (a, b) => a >= b;
|
|
break;
|
|
default:
|
|
comparator = (a, b) => a < b;
|
|
}
|
|
limitValue = parseInt(evaluateExpressionHelper(obj[limitType], models).value, 10);
|
|
// Update limitValue with overrides, this way at the end we have a flattened
|
|
// limitValues with overrides having priority
|
|
limitValues[limitType] = limitValue;
|
|
checkedLimitTypes[limitType] = true;
|
|
if (comparator(count, limitValue)) {
|
|
return {
|
|
type: limitType,
|
|
value: limitValue,
|
|
message: obj.message || i18n('common.role_limits.' + limitType,
|
|
{limitValue: limitValue, count: count, roleName: label})
|
|
};
|
|
}
|
|
};
|
|
|
|
// Check the overridden limit types
|
|
messages = _.chain(overrides)
|
|
.map((override) => {
|
|
var exp = evaluateExpressionHelper(override.condition, models).value;
|
|
|
|
if (exp) {
|
|
return _.map(limitTypes, _.partial(checkOneLimit, override));
|
|
}
|
|
})
|
|
.flatten()
|
|
.compact()
|
|
.value();
|
|
// Now check the global, not-overridden limit types
|
|
messages = messages.concat(_.chain(limitTypes)
|
|
.map((limitType) => {
|
|
if (checkedLimitTypes[limitType]) {
|
|
return null;
|
|
}
|
|
return checkOneLimit(limitValues, limitType);
|
|
})
|
|
.flatten()
|
|
.compact()
|
|
.value()
|
|
);
|
|
// There can be multiple messages for same limit type
|
|
// (for example, multiple 'min' messages) coming from
|
|
// multiple override methods. We pick a single, worst
|
|
// message, i.e. for 'min' and 'recommended' types we
|
|
// pick one with maximal value, for 'max' type we pick
|
|
// the minimal one.
|
|
messages = _.map(limitTypes, (limitType) => {
|
|
var message = _.chain(messages)
|
|
.filter({type: limitType})
|
|
.sortBy('value')
|
|
.value();
|
|
if (limitType !== 'max') {
|
|
message = message.reverse();
|
|
}
|
|
if (message[0]) {
|
|
return message[0].message;
|
|
}
|
|
});
|
|
messages = _.compact(messages).join(' ');
|
|
|
|
return {
|
|
count: count,
|
|
limits: limitValues,
|
|
message: messages,
|
|
valid: !messages
|
|
};
|
|
}
|
|
};
|
|
|
|
models.Plugin = BaseModel.extend({
|
|
constructorName: 'Plugin',
|
|
urlRoot: '/api/plugins'
|
|
});
|
|
|
|
models.Plugins = BaseCollection.extend({
|
|
constructorName: 'Plugins',
|
|
model: models.Plugin,
|
|
url: '/api/plugins'
|
|
});
|
|
|
|
models.Role = BaseModel.extend(restrictionMixin).extend({
|
|
idAttribute: 'name',
|
|
constructorName: 'Role',
|
|
parse(response) {
|
|
_.extend(response, _.omit(response.meta, 'name'));
|
|
response.label = response.meta.name;
|
|
delete response.meta;
|
|
return response;
|
|
}
|
|
});
|
|
|
|
models.Roles = BaseCollection.extend(restrictionMixin).extend({
|
|
constructorName: 'Roles',
|
|
comparator: 'weight',
|
|
model: models.Role,
|
|
initialize() {
|
|
this.processConflictsAndRestrictions();
|
|
this.on('update', this.processConflictsAndRestrictions, this);
|
|
},
|
|
processConflictsAndRestrictions() {
|
|
this.each((role) => {
|
|
role.expandLimits(role.get('limits'));
|
|
|
|
var roleConflicts = role.get('conflicts');
|
|
var roleName = role.get('name');
|
|
|
|
if (roleConflicts === '*') {
|
|
role.conflicts = _.map(this.reject({name: roleName}), (role) => role.get('name'));
|
|
} else {
|
|
role.conflicts = _.chain(role.conflicts)
|
|
.union(roleConflicts)
|
|
.uniq()
|
|
.compact()
|
|
.value();
|
|
}
|
|
|
|
_.each(role.conflicts, (conflictRoleName) => {
|
|
var conflictingRole = this.findWhere({name: conflictRoleName});
|
|
if (conflictingRole) {
|
|
conflictingRole.conflicts = _.uniq(_.union(conflictingRole.conflicts || [], [roleName]));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
models.Release = BaseModel.extend({
|
|
constructorName: 'Release',
|
|
urlRoot: '/api/releases'
|
|
});
|
|
|
|
models.ReleaseNetworkProperties = BaseModel.extend({
|
|
constructorName: 'ReleaseNetworkProperties'
|
|
});
|
|
|
|
models.Releases = BaseCollection.extend(cacheMixin).extend({
|
|
constructorName: 'Releases',
|
|
cacheFor: 60 * 1000,
|
|
model: models.Release,
|
|
url: '/api/releases'
|
|
});
|
|
|
|
models.Cluster = BaseModel.extend({
|
|
constructorName: 'Cluster',
|
|
urlRoot: '/api/clusters',
|
|
defaults() {
|
|
var defaults = {
|
|
nodes: new models.Nodes(),
|
|
tasks: new models.Tasks()
|
|
};
|
|
defaults.nodes.cluster = defaults.tasks.cluster = this;
|
|
return defaults;
|
|
},
|
|
validate(attrs) {
|
|
var errors = {};
|
|
if (!_.trim(attrs.name) || !_.trim(attrs.name).length) {
|
|
errors.name = 'Environment name cannot be empty';
|
|
}
|
|
if (!attrs.release) {
|
|
errors.release = 'Please choose OpenStack release';
|
|
}
|
|
return _.isEmpty(errors) ? null : errors;
|
|
},
|
|
task(filter1, filter2) {
|
|
var filters = _.isPlainObject(filter1) ? filter1 : {name: filter1, status: filter2};
|
|
return this.get('tasks') && this.get('tasks').findTask(filters);
|
|
},
|
|
tasks(filter1, filter2) {
|
|
var filters = _.isPlainObject(filter1) ? filter1 : {name: filter1, status: filter2};
|
|
return this.get('tasks') && this.get('tasks').filterTasks(filters);
|
|
},
|
|
needsRedeployment() {
|
|
return this.get('nodes').any({pending_addition: false, status: 'error'}) &&
|
|
this.get('status') !== 'update_error';
|
|
},
|
|
fetchRelated(related, options) {
|
|
return this.get(related).fetch(_.extend({data: {cluster_id: this.id}}, options));
|
|
},
|
|
isAvailableForSettingsChanges() {
|
|
return !this.get('is_locked');
|
|
},
|
|
isDeploymentPossible() {
|
|
var nodes = this.get('nodes');
|
|
return this.get('release').get('state') !== 'unavailable' && !!nodes.length &&
|
|
(nodes.hasChanges() || this.needsRedeployment()) &&
|
|
!this.task({group: 'deployment', active: true});
|
|
}
|
|
});
|
|
|
|
models.Clusters = BaseCollection.extend({
|
|
constructorName: 'Clusters',
|
|
model: models.Cluster,
|
|
url: '/api/clusters',
|
|
comparator: 'id'
|
|
});
|
|
|
|
models.Node = BaseModel.extend({
|
|
constructorName: 'Node',
|
|
urlRoot: '/api/nodes',
|
|
statuses: [
|
|
'ready',
|
|
'pending_addition',
|
|
'pending_deletion',
|
|
'provisioned',
|
|
'provisioning',
|
|
'deploying',
|
|
'discover',
|
|
'error',
|
|
'offline',
|
|
'removing'
|
|
],
|
|
resource(resourceName) {
|
|
var resource = 0;
|
|
try {
|
|
if (resourceName === 'cores') {
|
|
resource = this.get('meta').cpu.real;
|
|
} else if (resourceName === 'ht_cores') {
|
|
resource = this.get('meta').cpu.total;
|
|
} else if (resourceName === 'hdd') {
|
|
resource = _.reduce(this.get('meta').disks, (hdd, disk) => {
|
|
return _.isNumber(disk.size) ? hdd + disk.size : hdd;
|
|
}, 0);
|
|
} else if (resourceName === 'ram') {
|
|
resource = this.get('meta').memory.total;
|
|
} else if (resourceName === 'disks') {
|
|
resource = _.pluck(this.get('meta').disks, 'size').sort((a, b) => a - b);
|
|
} else if (resourceName === 'disks_amount') {
|
|
resource = this.get('meta').disks.length;
|
|
} else if (resourceName === 'interfaces') {
|
|
resource = this.get('meta').interfaces.length;
|
|
}
|
|
} catch (ignore) {}
|
|
return _.isNaN(resource) ? 0 : resource;
|
|
},
|
|
sortedRoles(preferredOrder) {
|
|
return _.union(this.get('roles'), this.get('pending_roles')).sort((a, b) => {
|
|
return _.indexOf(preferredOrder, a) - _.indexOf(preferredOrder, b);
|
|
});
|
|
},
|
|
isSelectable() {
|
|
// forbid removing node from adding to environments
|
|
// and useless management of roles, disks, interfaces, etc.
|
|
return this.get('status') !== 'removing';
|
|
},
|
|
hasRole(role, onlyDeployedRoles) {
|
|
var roles = onlyDeployedRoles ? this.get('roles') :
|
|
_.union(this.get('roles'), this.get('pending_roles'));
|
|
return _.contains(roles, role);
|
|
},
|
|
hasChanges() {
|
|
return this.get('pending_addition') ||
|
|
this.get('pending_deletion') ||
|
|
this.get('cluster') && !!this.get('pending_roles').length;
|
|
},
|
|
areDisksConfigurable() {
|
|
var status = this.get('status');
|
|
return status === 'discover' || status === 'error';
|
|
},
|
|
areInterfacesConfigurable() {
|
|
var status = this.get('status');
|
|
return status === 'discover' || status === 'error';
|
|
},
|
|
getRolesSummary(releaseRoles) {
|
|
return _.map(this.sortedRoles(releaseRoles.pluck('name')), (role) => {
|
|
return releaseRoles.findWhere({name: role}).get('label');
|
|
}).join(', ');
|
|
},
|
|
getStatusSummary() {
|
|
// 'offline' status has higher priority
|
|
if (!this.get('online')) return 'offline';
|
|
var status = this.get('status');
|
|
// 'removing' end 'error' statuses have higher priority
|
|
if (_.contains(['removing', 'error'], status)) return status;
|
|
if (this.get('pending_addition')) return 'pending_addition';
|
|
if (this.get('pending_deletion')) return 'pending_deletion';
|
|
return status;
|
|
},
|
|
getLabel(label) {
|
|
var labelValue = this.get('labels')[label];
|
|
return _.isUndefined(labelValue) ? false : labelValue;
|
|
}
|
|
});
|
|
|
|
models.Nodes = BaseCollection.extend({
|
|
constructorName: 'Nodes',
|
|
model: models.Node,
|
|
url: '/api/nodes',
|
|
comparator: 'id',
|
|
sorters: [
|
|
'cluster',
|
|
'roles',
|
|
'status',
|
|
'name',
|
|
'mac',
|
|
'ip',
|
|
'manufacturer',
|
|
'cores',
|
|
'ht_cores',
|
|
'hdd',
|
|
'disks',
|
|
'ram',
|
|
'interfaces',
|
|
'group_id'
|
|
],
|
|
filters: [
|
|
'cluster',
|
|
'roles',
|
|
'status',
|
|
'manufacturer',
|
|
'cores',
|
|
'ht_cores',
|
|
'hdd',
|
|
'disks_amount',
|
|
'ram',
|
|
'interfaces',
|
|
'group_id'
|
|
],
|
|
viewModes: ['standard', 'compact'],
|
|
hasChanges() {
|
|
return _.any(this.invoke('hasChanges'));
|
|
},
|
|
nodesAfterDeployment() {
|
|
return this.filter((node) => !node.get('pending_deletion'));
|
|
},
|
|
nodesAfterDeploymentWithRole(role) {
|
|
return _.filter(this.nodesAfterDeployment(), (node) => node.hasRole(role));
|
|
},
|
|
resources(resourceName) {
|
|
var resources = this.map((node) => node.resource(resourceName));
|
|
return _.reduce(resources, (sum, n) => sum + n, 0);
|
|
},
|
|
getLabelValues(label) {
|
|
return this.invoke('getLabel', label);
|
|
},
|
|
areDisksConfigurable() {
|
|
if (!this.length) return false;
|
|
var roles = _.union(this.at(0).get('roles'), this.at(0).get('pending_roles'));
|
|
var disks = this.at(0).resource('disks');
|
|
return !this.any((node) => {
|
|
var roleConflict = _.difference(roles, _.union(node.get('roles'),
|
|
node.get('pending_roles'))).length;
|
|
return roleConflict || !_.isEqual(disks, node.resource('disks'));
|
|
});
|
|
},
|
|
areInterfacesConfigurable() {
|
|
if (!this.length) return false;
|
|
return _.uniq(this.invoke('resource', 'interfaces')).length === 1;
|
|
}
|
|
});
|
|
|
|
models.NodesStatistics = BaseModel.extend({
|
|
constructorName: 'NodesStatistics',
|
|
urlRoot: '/api/nodes/allocation/stats'
|
|
});
|
|
|
|
models.Task = BaseModel.extend({
|
|
constructorName: 'Task',
|
|
urlRoot: '/api/tasks',
|
|
releaseId() {
|
|
var id;
|
|
try {
|
|
id = this.get('result').release_info.release_id;
|
|
} catch (ignore) {}
|
|
return id;
|
|
},
|
|
groups: {
|
|
network: ['verify_networks', 'check_networks'],
|
|
deployment: ['update', 'stop_deployment', 'deploy', 'reset_environment', 'spawn_vms']
|
|
},
|
|
extendGroups(filters) {
|
|
var names = utils.composeList(filters.name);
|
|
if (_.isEmpty(names)) names = _.flatten(_.values(this.groups));
|
|
var groups = utils.composeList(filters.group);
|
|
if (_.isEmpty(groups)) return names;
|
|
return _.intersection(names, _.flatten(_.values(_.pick(this.groups, groups))));
|
|
},
|
|
extendStatuses(filters) {
|
|
var activeTaskStatuses = ['running', 'pending'];
|
|
var completedTaskStatuses = ['ready', 'error'];
|
|
var statuses = utils.composeList(filters.status);
|
|
if (_.isEmpty(statuses)) {
|
|
statuses = _.union(activeTaskStatuses, completedTaskStatuses);
|
|
}
|
|
if (_.isBoolean(filters.active)) {
|
|
return _.intersection(statuses, filters.active ? activeTaskStatuses : completedTaskStatuses);
|
|
}
|
|
return statuses;
|
|
},
|
|
match(filters) {
|
|
filters = filters || {};
|
|
if (!_.isEmpty(filters)) {
|
|
if ((filters.group || filters.name) &&
|
|
!_.contains(this.extendGroups(filters), this.get('name'))) {
|
|
return false;
|
|
}
|
|
if ((filters.status || _.isBoolean(filters.active)) &&
|
|
!_.contains(this.extendStatuses(filters), this.get('status'))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
isInfinite() {
|
|
return this.match({name: ['stop_deployment', 'reset_environment']});
|
|
},
|
|
isStoppable() {
|
|
return this.match({name: 'deploy', status: 'running'});
|
|
}
|
|
});
|
|
|
|
models.Tasks = BaseCollection.extend({
|
|
constructorName: 'Tasks',
|
|
model: models.Task,
|
|
url: '/api/tasks',
|
|
toJSON() {
|
|
return this.pluck('id');
|
|
},
|
|
comparator: 'id',
|
|
filterTasks(filters) {
|
|
return _.flatten(_.map(this.model.prototype.extendGroups(filters), (name) => {
|
|
return this.filter((task) => {
|
|
return task.match(_.extend(_.omit(filters, 'group'), {name: name}));
|
|
});
|
|
}));
|
|
},
|
|
findTask(filters) {
|
|
return this.filterTasks(filters)[0];
|
|
}
|
|
});
|
|
|
|
models.Notification = BaseModel.extend({
|
|
constructorName: 'Notification',
|
|
urlRoot: '/api/notifications'
|
|
});
|
|
|
|
models.Notifications = BaseCollection.extend({
|
|
constructorName: 'Notifications',
|
|
model: models.Notification,
|
|
url: '/api/notifications',
|
|
comparator(notification) {
|
|
return -notification.id;
|
|
}
|
|
});
|
|
|
|
models.Settings = Backbone.DeepModel
|
|
.extend(superMixin)
|
|
.extend(cacheMixin)
|
|
.extend(restrictionMixin)
|
|
.extend({
|
|
constructorName: 'Settings',
|
|
urlRoot: '/api/clusters/',
|
|
root: 'editable',
|
|
cacheFor: 60 * 1000,
|
|
groupList: ['general', 'security', 'compute', 'network', 'storage',
|
|
'logging', 'openstack_services', 'other'],
|
|
isNew() {
|
|
return false;
|
|
},
|
|
isPlugin(section) {
|
|
return (section.metadata || {}).class === 'plugin';
|
|
},
|
|
parse(response) {
|
|
return response[this.root];
|
|
},
|
|
mergePluginSettings() {
|
|
_.each(this.attributes, (section, sectionName) => {
|
|
if (this.isPlugin(section)) {
|
|
var chosenVersionData = section.metadata.versions.find(
|
|
(version) => version.metadata.plugin_id === section.metadata.chosen_id
|
|
);
|
|
// merge metadata of a chosen plugin version
|
|
_.extend(section.metadata,
|
|
_.omit(chosenVersionData.metadata, 'plugin_id', 'plugin_version'));
|
|
// merge settings of a chosen plugin version
|
|
this.attributes[sectionName] = _.extend(_.pick(section, 'metadata'),
|
|
_.omit(chosenVersionData, 'metadata'));
|
|
}
|
|
}, this);
|
|
},
|
|
toJSON() {
|
|
var settings = this._super('toJSON', arguments);
|
|
if (!this.root) return settings;
|
|
|
|
// update plugin settings
|
|
_.each(settings, (section, sectionName) => {
|
|
if (this.isPlugin(section)) {
|
|
var chosenVersionData = section.metadata.versions.find(
|
|
(version) => version.metadata.plugin_id === section.metadata.chosen_id
|
|
);
|
|
section.metadata = _.omit(section.metadata,
|
|
_.without(_.keys(chosenVersionData.metadata), 'plugin_id', 'plugin_version'));
|
|
_.each(section, (setting, settingName) => {
|
|
if (settingName !== 'metadata') chosenVersionData[settingName].value = setting.value;
|
|
});
|
|
settings[sectionName] = _.pick(section, 'metadata');
|
|
}
|
|
});
|
|
return {[this.root]: settings};
|
|
},
|
|
initialize() {
|
|
this.once('change', this.mergePluginSettings, this);
|
|
},
|
|
validate(attrs, options) {
|
|
var errors = {};
|
|
var models = options ? options.models : {};
|
|
var checkRestrictions = (setting) => this.checkRestrictions(models, null, setting);
|
|
_.each(attrs, (group, groupName) => {
|
|
if ((group.metadata || {}).enabled === false ||
|
|
checkRestrictions(group.metadata).result) return;
|
|
_.each(group, (setting, settingName) => {
|
|
if (checkRestrictions(setting).result) return;
|
|
var path = this.makePath(groupName, settingName);
|
|
// support of custom controls
|
|
var CustomControl = customControls[setting.type];
|
|
if (CustomControl) {
|
|
var error = CustomControl.validate(setting, models);
|
|
if (error) errors[path] = error;
|
|
return;
|
|
}
|
|
|
|
if (!(setting.regex || {}).source) return;
|
|
if (!setting.value.match(new RegExp(setting.regex.source))) {
|
|
errors[path] = setting.regex.error;
|
|
}
|
|
});
|
|
});
|
|
return _.isEmpty(errors) ? null : errors;
|
|
},
|
|
makePath(...args) {
|
|
return args.join('.');
|
|
},
|
|
getValueAttribute(settingName) {
|
|
return settingName === 'metadata' ? 'enabled' : 'value';
|
|
},
|
|
hasChanges(initialAttributes, models) {
|
|
return _.any(this.attributes, (section, sectionName) => {
|
|
var metadata = section.metadata;
|
|
var result = false;
|
|
if (metadata) {
|
|
if (this.checkRestrictions(models, null, metadata).result) return result;
|
|
if (!_.isUndefined(metadata.enabled)) {
|
|
result = metadata.enabled !== initialAttributes[sectionName].metadata.enabled;
|
|
}
|
|
if (!result && this.isPlugin(section)) {
|
|
result = metadata.chosen_id !== initialAttributes[sectionName].metadata.chosen_id;
|
|
}
|
|
}
|
|
return result || (metadata || {}).enabled !== false &&
|
|
_.any(section, (setting, settingName) => {
|
|
if (this.checkRestrictions(models, null, setting).result) return false;
|
|
return !_.isEqual(setting.value,
|
|
(initialAttributes[sectionName][settingName] || {}).value);
|
|
});
|
|
});
|
|
},
|
|
sanitizeGroup(group) {
|
|
return _.contains(this.groupList, group) ? group : 'other';
|
|
},
|
|
getGroupList() {
|
|
var groups = [];
|
|
_.each(this.attributes, (section) => {
|
|
if (section.metadata.group) {
|
|
groups.push(this.sanitizeGroup(section.metadata.group));
|
|
} else {
|
|
_.each(section, (setting, settingName) => {
|
|
if (settingName !== 'metadata') groups.push(this.sanitizeGroup(setting.group));
|
|
});
|
|
}
|
|
});
|
|
return _.intersection(this.groupList, groups);
|
|
}
|
|
});
|
|
|
|
models.FuelSettings = models.Settings.extend({
|
|
constructorName: 'FuelSettings',
|
|
url: '/api/settings',
|
|
root: 'settings',
|
|
parse(response) {
|
|
return _.extend(this._super('parse', arguments), {master_node_uid: response.master_node_uid});
|
|
}
|
|
});
|
|
|
|
models.Disk = BaseModel.extend({
|
|
constructorName: 'Disk',
|
|
urlRoot: '/api/nodes/',
|
|
parse(response) {
|
|
response.volumes = new models.Volumes(response.volumes);
|
|
response.volumes.disk = this;
|
|
return response;
|
|
},
|
|
toJSON(options) {
|
|
return _.extend(this.constructor.__super__.toJSON.call(this, options),
|
|
{volumes: this.get('volumes').toJSON()});
|
|
},
|
|
getUnallocatedSpace(options) {
|
|
options = options || {};
|
|
var volumes = options.volumes || this.get('volumes');
|
|
var allocatedSpace = volumes.reduce((sum, volume) => {
|
|
return volume.get('name') === options.skip ? sum : sum + volume.get('size');
|
|
}, 0);
|
|
return this.get('size') - allocatedSpace;
|
|
},
|
|
validate(attrs) {
|
|
var error;
|
|
var unallocatedSpace = this.getUnallocatedSpace({volumes: attrs.volumes});
|
|
if (unallocatedSpace < 0) {
|
|
error = i18n('cluster_page.nodes_tab.configure_disks.validation_error',
|
|
{size: utils.formatNumber(unallocatedSpace * -1)});
|
|
}
|
|
return error;
|
|
}
|
|
});
|
|
|
|
models.Disks = BaseCollection.extend({
|
|
constructorName: 'Disks',
|
|
model: models.Disk,
|
|
url: '/api/nodes/',
|
|
comparator: 'name'
|
|
});
|
|
|
|
models.Volume = BaseModel.extend({
|
|
constructorName: 'Volume',
|
|
urlRoot: '/api/volumes/',
|
|
getMinimalSize(minimum) {
|
|
var currentDisk = this.collection.disk;
|
|
var groupAllocatedSpace = 0;
|
|
if (currentDisk && currentDisk.collection) {
|
|
groupAllocatedSpace = currentDisk.collection.reduce((sum, disk) => {
|
|
return disk.id === currentDisk.id ? sum : sum +
|
|
disk.get('volumes').findWhere({name: this.get('name')}).get('size');
|
|
}, 0);
|
|
}
|
|
return minimum - groupAllocatedSpace;
|
|
},
|
|
getMaxSize() {
|
|
var volumes = this.collection.disk.get('volumes');
|
|
var diskAllocatedSpace = volumes.reduce((total, volume) => {
|
|
return this.get('name') === volume.get('name') ? total : total + volume.get('size');
|
|
}, 0);
|
|
return this.collection.disk.get('size') - diskAllocatedSpace;
|
|
},
|
|
validate(attrs, options) {
|
|
var min = this.getMinimalSize(options.minimum);
|
|
if (attrs.size < min) {
|
|
return i18n('cluster_page.nodes_tab.configure_disks.volume_error',
|
|
{size: utils.formatNumber(min)});
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
|
|
models.Volumes = BaseCollection.extend({
|
|
constructorName: 'Volumes',
|
|
model: models.Volume,
|
|
url: '/api/volumes/'
|
|
});
|
|
|
|
models.Interface = BaseModel.extend({
|
|
constructorName: 'Interface',
|
|
parse(response) {
|
|
response.assigned_networks = new models.InterfaceNetworks(response.assigned_networks);
|
|
response.assigned_networks.interface = this;
|
|
return response;
|
|
},
|
|
toJSON(options) {
|
|
return _.omit(_.extend(this.constructor.__super__.toJSON.call(this, options), {
|
|
assigned_networks: this.get('assigned_networks').toJSON()
|
|
}), 'checked');
|
|
},
|
|
isBond() {
|
|
return this.get('type') === 'bond';
|
|
},
|
|
getSlaveInterfaces() {
|
|
if (!this.isBond()) return [this];
|
|
var slaveInterfaceNames = _.pluck(this.get('slaves'), 'name');
|
|
return this.collection.filter((slaveInterface) => {
|
|
return _.contains(slaveInterfaceNames, slaveInterface.get('name'));
|
|
});
|
|
},
|
|
validate(attrs) {
|
|
var errors = [];
|
|
var networks = new models.Networks(this.get('assigned_networks')
|
|
.invoke('getFullNetwork', attrs.networks));
|
|
var untaggedNetworks = networks.filter((network) => {
|
|
return _.isNull(network.getVlanRange(attrs.networkingParameters));
|
|
});
|
|
var ns = 'cluster_page.nodes_tab.configure_interfaces.validation.';
|
|
// public and floating networks are allowed to be assigned to the same interface
|
|
var maxUntaggedNetworksCount = networks.any({name: 'public'}) &&
|
|
networks.any({name: 'floating'}) ? 2 : 1;
|
|
if (untaggedNetworks.length > maxUntaggedNetworksCount) {
|
|
errors.push(i18n(ns + 'too_many_untagged_networks'));
|
|
}
|
|
var interfaceProperties = this.get('interface_properties');
|
|
if (interfaceProperties && interfaceProperties.mtu) {
|
|
var mtuValue = interfaceProperties.mtu;
|
|
if (mtuValue && (mtuValue < 42 || mtuValue > 65536)) {
|
|
errors.push(i18n(ns + 'invalid_mtu'));
|
|
}
|
|
}
|
|
|
|
// check interface networks have the same vlan id
|
|
var vlans = _.reject(networks.pluck('vlan_start'), _.isNull);
|
|
if (_.uniq(vlans).length < vlans.length) errors.push(i18n(ns + 'networks_with_the_same_vlan'));
|
|
|
|
// check interface network vlan ids included in Neutron L2 vlan range
|
|
var vlanRanges = _.reject(networks.map(
|
|
(network) => network.getVlanRange(attrs.networkingParameters)
|
|
), _.isNull);
|
|
if (
|
|
_.any(vlanRanges,
|
|
(currentRange) => _.any(vlanRanges,
|
|
(range) => !_.isEqual(currentRange, range) &&
|
|
range[1] >= currentRange[0] && range[0] <= currentRange[1]
|
|
)
|
|
)
|
|
) errors.push(i18n(ns + 'vlan_range_intersection'));
|
|
|
|
return errors;
|
|
}
|
|
});
|
|
|
|
models.Interfaces = BaseCollection.extend({
|
|
constructorName: 'Interfaces',
|
|
model: models.Interface,
|
|
generateBondName(base) {
|
|
var index, proposedName;
|
|
for (index = 0; ; index += 1) {
|
|
proposedName = base + index;
|
|
if (!this.any({name: proposedName})) return proposedName;
|
|
}
|
|
},
|
|
comparator(ifc1, ifc2) {
|
|
return utils.multiSort(ifc1, ifc2, [{attr: 'isBond'}, {attr: 'name'}]);
|
|
}
|
|
});
|
|
|
|
var networkPreferredOrder = ['public', 'floating', 'storage', 'management',
|
|
'private', 'fixed', 'baremetal'];
|
|
|
|
models.InterfaceNetwork = BaseModel.extend({
|
|
constructorName: 'InterfaceNetwork',
|
|
getFullNetwork(networks) {
|
|
return networks.findWhere({name: this.get('name')});
|
|
}
|
|
});
|
|
|
|
models.InterfaceNetworks = BaseCollection.extend({
|
|
constructorName: 'InterfaceNetworks',
|
|
model: models.InterfaceNetwork,
|
|
comparator(network) {
|
|
return _.indexOf(networkPreferredOrder, network.get('name'));
|
|
}
|
|
});
|
|
|
|
models.Network = BaseModel.extend({
|
|
constructorName: 'Network',
|
|
getVlanRange(networkingParameters) {
|
|
if (!this.get('meta').neutron_vlan_range) {
|
|
var externalNetworkData = this.get('meta').ext_net_data;
|
|
var vlanStart = externalNetworkData ?
|
|
networkingParameters.get(externalNetworkData[0]) : this.get('vlan_start');
|
|
return _.isNull(vlanStart) ? vlanStart :
|
|
[vlanStart, externalNetworkData ?
|
|
vlanStart + networkingParameters.get(externalNetworkData[1]) - 1 : vlanStart];
|
|
}
|
|
return networkingParameters.get('vlan_range');
|
|
}
|
|
});
|
|
|
|
models.Networks = BaseCollection.extend({
|
|
constructorName: 'Networks',
|
|
model: models.Network,
|
|
comparator(network) {
|
|
return _.indexOf(networkPreferredOrder, network.get('name'));
|
|
}
|
|
});
|
|
|
|
models.NetworkingParameters = BaseModel.extend({
|
|
constructorName: 'NetworkingParameters'
|
|
});
|
|
|
|
models.NetworkConfiguration = BaseModel.extend(cacheMixin).extend({
|
|
constructorName: 'NetworkConfiguration',
|
|
cacheFor: 60 * 1000,
|
|
parse(response) {
|
|
response.networks = new models.Networks(response.networks);
|
|
response.networking_parameters = new models.NetworkingParameters(
|
|
response.networking_parameters
|
|
);
|
|
return response;
|
|
},
|
|
toJSON() {
|
|
return {
|
|
networks: this.get('networks').toJSON(),
|
|
networking_parameters: this.get('networking_parameters').toJSON()
|
|
};
|
|
},
|
|
isNew() {
|
|
return false;
|
|
},
|
|
validateNetworkIpRanges(network, cidr) {
|
|
if (network.get('meta').notation === 'ip_ranges') {
|
|
var errors = utils.validateIPRanges(network.get('ip_ranges'), cidr);
|
|
return errors.length ? {ip_ranges: errors} : null;
|
|
}
|
|
return null;
|
|
},
|
|
validateNetworkGateway(network, cidr) {
|
|
if (network.get('meta').use_gateway) {
|
|
if (!utils.validateIP(network.get('gateway'))) {
|
|
return {gateway: i18n('cluster_page.network_tab.validation.invalid_gateway')};
|
|
}
|
|
if (cidr && !utils.validateIpCorrespondsToCIDR(cidr, network.get('gateway'))) {
|
|
return {gateway: i18n('cluster_page.network_tab.validation.gateway_does_not_match_cidr')};
|
|
}
|
|
return null;
|
|
}
|
|
return null;
|
|
},
|
|
validate(attrs) {
|
|
var errors = {};
|
|
var networkingParametersErrors = {};
|
|
var ns = 'cluster_page.network_tab.validation.';
|
|
var networks = attrs.networks;
|
|
var networkParameters = attrs.networking_parameters;
|
|
var nodeNetworkGroupsErrors = {};
|
|
var nodeNetworkGroups = app.nodeNetworkGroups;
|
|
var novaNetManager = networkParameters.get('net_manager');
|
|
var floatingRangesErrors;
|
|
|
|
nodeNetworkGroups.map((nodeNetworkGroup) => {
|
|
var networksToCheck = new models.Networks(networks.filter((network) => {
|
|
return network.get('group_id') === nodeNetworkGroup.id && network.get('meta').configurable;
|
|
}));
|
|
var nodeNetworkGroupErrors = {};
|
|
networksToCheck.each((network) => {
|
|
var networkErrors = {};
|
|
|
|
var cidr = network.get('cidr');
|
|
_.extend(networkErrors, utils.validateCidr(cidr));
|
|
var cidrError = _.has(networkErrors, 'cidr');
|
|
_.extend(networkErrors, this.validateNetworkIpRanges(network, cidrError ? null : cidr));
|
|
_.extend(networkErrors, this.validateNetworkGateway(network, cidrError ? null : cidr));
|
|
//FIXME (morale): same VLAN IDs are not permitted for nova-network for now
|
|
var forbiddenVlans = novaNetManager ? networksToCheck.map((net) => {
|
|
return net.id !== network.id ? net.get('vlan_start') : null;
|
|
}) : [];
|
|
_.extend(networkErrors,
|
|
utils.validateVlan(network.get('vlan_start'), forbiddenVlans, 'vlan_start'));
|
|
|
|
if (!_.isEmpty(networkErrors)) {
|
|
nodeNetworkGroupErrors[network.id] = networkErrors;
|
|
}
|
|
|
|
if (network.get('name') === 'baremetal') {
|
|
var baremetalGateway = networkParameters.get('baremetal_gateway');
|
|
if (!utils.validateIP(baremetalGateway)) {
|
|
networkingParametersErrors.baremetal_gateway = i18n(ns + 'invalid_gateway');
|
|
} else if (!cidrError && !utils.validateIpCorrespondsToCIDR(cidr, baremetalGateway)) {
|
|
networkingParametersErrors.baremetal_gateway = i18n(ns + 'gateway_does_not_match_cidr');
|
|
}
|
|
var baremetalRangeErrors =
|
|
utils.validateIPRanges([networkParameters.get('baremetal_range')], cidrError ?
|
|
null : cidr);
|
|
if (baremetalRangeErrors.length) {
|
|
var [{start, end}] = baremetalRangeErrors;
|
|
networkingParametersErrors.baremetal_range = [start, end];
|
|
}
|
|
}
|
|
}, this);
|
|
|
|
if (!_.isEmpty(nodeNetworkGroupErrors)) {
|
|
nodeNetworkGroupsErrors[nodeNetworkGroup.id] = nodeNetworkGroupErrors;
|
|
}
|
|
});
|
|
|
|
if (!_.isEmpty(nodeNetworkGroupsErrors)) {
|
|
errors.networks = nodeNetworkGroupsErrors;
|
|
}
|
|
|
|
// validate networking parameters
|
|
if (novaNetManager) {
|
|
networkingParametersErrors = _.extend(networkingParametersErrors,
|
|
utils.validateCidr(networkParameters.get('fixed_networks_cidr'), 'fixed_networks_cidr'));
|
|
var fixedAmount = networkParameters.get('fixed_networks_amount');
|
|
var fixedVlan = networkParameters.get('fixed_networks_vlan_start');
|
|
if (!utils.isNaturalNumber(parseInt(fixedAmount, 10))) {
|
|
networkingParametersErrors.fixed_networks_amount = i18n(ns + 'invalid_amount');
|
|
}
|
|
var vlanErrors = utils.validateVlan(fixedVlan, networks.pluck('vlan_start'),
|
|
'fixed_networks_vlan_start', novaNetManager === 'VlanManager');
|
|
_.extend(networkingParametersErrors, vlanErrors);
|
|
if (_.isEmpty(vlanErrors)) {
|
|
if (!networkingParametersErrors.fixed_networks_amount && fixedAmount > 4095 - fixedVlan) {
|
|
networkingParametersErrors.fixed_networks_amount = i18n(ns + 'need_more_vlan');
|
|
}
|
|
var vlanIntersection = false;
|
|
_.each(_.compact(networks.pluck('vlan_start')), (vlan) => {
|
|
if (utils.validateVlanRange(fixedVlan, fixedVlan + fixedAmount - 1, vlan)) {
|
|
vlanIntersection = true;
|
|
}
|
|
});
|
|
if (vlanIntersection) {
|
|
networkingParametersErrors.fixed_networks_vlan_start = i18n(ns + 'vlan_intersection');
|
|
}
|
|
}
|
|
floatingRangesErrors = utils.validateIPRanges(networkParameters.get('floating_ranges'), null);
|
|
if (floatingRangesErrors.length) {
|
|
networkingParametersErrors.floating_ranges = floatingRangesErrors;
|
|
}
|
|
} else {
|
|
var idRangeErrors = ['', ''];
|
|
var segmentation = networkParameters.get('segmentation_type');
|
|
var idRangeAttr = segmentation === 'vlan' ? 'vlan_range' : 'gre_id_range';
|
|
var maxId = segmentation === 'vlan' ? 4094 : 65535;
|
|
var idRange = networkParameters.get(idRangeAttr);
|
|
var idStart = Number(idRange[0]);
|
|
var idEnd = Number(idRange[1]);
|
|
if (!utils.isNaturalNumber(idStart) || idStart < 2 || idStart > maxId) {
|
|
idRangeErrors[0] = i18n(ns + 'invalid_id_start');
|
|
} else if (!utils.isNaturalNumber(idEnd) || idEnd < 2 || idEnd > maxId) {
|
|
idRangeErrors[1] = i18n(ns + 'invalid_id_end');
|
|
} else if (idStart > idEnd) {
|
|
idRangeErrors[0] = idRangeErrors[1] = i18n(ns + 'invalid_id_range');
|
|
} else if (idStart === idEnd) {
|
|
idRangeErrors[0] = idRangeErrors[1] = i18n(ns + 'not_enough_id');
|
|
} else if (segmentation === 'vlan') {
|
|
_.each(_.compact(networks.pluck('vlan_start')), (vlan) => {
|
|
if (utils.validateVlanRange(idStart, idEnd, vlan)) {
|
|
idRangeErrors[0] = i18n(ns + 'vlan_intersection');
|
|
}
|
|
return idRangeErrors[0];
|
|
});
|
|
}
|
|
if (_.compact(idRangeErrors).length) {
|
|
networkingParametersErrors[idRangeAttr] = idRangeErrors;
|
|
}
|
|
if (!networkParameters.get('base_mac').match(utils.regexes.mac)) {
|
|
networkingParametersErrors.base_mac = i18n(ns + 'invalid_mac');
|
|
}
|
|
var cidr = networkParameters.get('internal_cidr');
|
|
networkingParametersErrors = _.extend(networkingParametersErrors,
|
|
utils.validateCidr(cidr, 'internal_cidr'));
|
|
var gateway = networkParameters.get('internal_gateway');
|
|
if (!utils.validateIP(gateway)) {
|
|
networkingParametersErrors.internal_gateway = i18n(ns + 'invalid_gateway');
|
|
} else if (!utils.validateIpCorrespondsToCIDR(cidr, gateway)) {
|
|
networkingParametersErrors.internal_gateway = i18n(ns + 'gateway_does_not_match_cidr');
|
|
}
|
|
var networkNamesRegExp = /^[a-z][\w\-]*$/i;
|
|
_.each(['internal_name', 'floating_name'], (paramName) => {
|
|
if (!networkParameters.get(paramName).match(networkNamesRegExp)) {
|
|
networkingParametersErrors[paramName] = i18n(ns + 'invalid_name');
|
|
}
|
|
});
|
|
|
|
var floatingRanges = networkParameters.get('floating_ranges');
|
|
var networkToCheckFloatingRange = networks.find((network) => {
|
|
if (!network.get('meta').floating_range_var) return false;
|
|
var cidrError = false;
|
|
try {
|
|
cidrError = !!errors.networks[network.get('group_id')][network.id].cidr;
|
|
} catch (error) {}
|
|
if (cidrError) return false;
|
|
return utils.validateIpCorrespondsToCIDR(network.get('cidr'), floatingRanges[0][0]) &&
|
|
utils.validateIpCorrespondsToCIDR(network.get('cidr'), floatingRanges[0][1]);
|
|
});
|
|
|
|
var networkToCheckFloatingRangeData = networkToCheckFloatingRange ? {
|
|
cidr: networkToCheckFloatingRange.get('cidr'),
|
|
network: _.capitalize(networkToCheckFloatingRange.get('name')),
|
|
nodeNetworkGroup: nodeNetworkGroups
|
|
.get(networkToCheckFloatingRange.get('group_id'))
|
|
.get('name')
|
|
} : {};
|
|
var networkToCheckFloatingRangeIPRanges = networkToCheckFloatingRange ?
|
|
_.filter(networkToCheckFloatingRange.get('ip_ranges'), (range, index) => {
|
|
var ipRangeError = false;
|
|
try {
|
|
ipRangeError = !_.all(range) ||
|
|
!!_.find(errors
|
|
.networks[networkToCheckFloatingRange
|
|
.get('group_id')][networkToCheckFloatingRange.id].ip_ranges, {index: index});
|
|
} catch (error) {}
|
|
return !ipRangeError;
|
|
}) : [];
|
|
|
|
floatingRangesErrors = utils.validateIPRanges(
|
|
floatingRanges,
|
|
networkToCheckFloatingRangeData.cidr,
|
|
networkToCheckFloatingRangeIPRanges,
|
|
{
|
|
IP_RANGES_INTERSECTION: i18n(ns + 'floating_and_public_ip_ranges_intersection',
|
|
networkToCheckFloatingRangeData),
|
|
IP_RANGE_IS_NOT_IN_PUBLIC_CIDR: i18n(ns + 'floating_range_is_not_in_public_cidr')
|
|
}
|
|
);
|
|
|
|
if (floatingRangesErrors.length) {
|
|
networkingParametersErrors.floating_ranges = floatingRangesErrors;
|
|
}
|
|
}
|
|
var nameserverErrors = [];
|
|
_.each(networkParameters.get('dns_nameservers'), (nameserver) => {
|
|
nameserverErrors.push(!utils.validateIP(nameserver) ? i18n(ns + 'invalid_nameserver') : null);
|
|
});
|
|
if (_.compact(nameserverErrors).length) {
|
|
networkingParametersErrors.dns_nameservers = nameserverErrors;
|
|
}
|
|
|
|
if (!_.isEmpty(networkingParametersErrors)) {
|
|
errors.networking_parameters = networkingParametersErrors;
|
|
}
|
|
|
|
return _.isEmpty(errors) ? null : errors;
|
|
}
|
|
});
|
|
|
|
models.LogSource = BaseModel.extend({
|
|
constructorName: 'LogSource',
|
|
urlRoot: '/api/logs/sources'
|
|
});
|
|
|
|
models.LogSources = BaseCollection.extend({
|
|
constructorName: 'LogSources',
|
|
model: models.LogSource,
|
|
url: '/api/logs/sources'
|
|
});
|
|
|
|
models.TestSet = BaseModel.extend({
|
|
constructorName: 'TestSet',
|
|
urlRoot: '/ostf/testsets'
|
|
});
|
|
|
|
models.TestSets = BaseCollection.extend({
|
|
constructorName: 'TestSets',
|
|
model: models.TestSet,
|
|
url: '/ostf/testsets'
|
|
});
|
|
|
|
models.Test = BaseModel.extend({
|
|
constructorName: 'Test',
|
|
urlRoot: '/ostf/tests'
|
|
});
|
|
|
|
models.Tests = BaseCollection.extend({
|
|
constructorName: 'Tests',
|
|
model: models.Test,
|
|
url: '/ostf/tests'
|
|
});
|
|
|
|
models.TestRun = BaseModel.extend({
|
|
constructorName: 'TestRun',
|
|
urlRoot: '/ostf/testruns'
|
|
});
|
|
|
|
models.TestRuns = BaseCollection.extend({
|
|
constructorName: 'TestRuns',
|
|
model: models.TestRun,
|
|
url: '/ostf/testruns'
|
|
});
|
|
|
|
models.OSTFClusterMetadata = BaseModel.extend({
|
|
constructorName: 'OSTFClusterMetadata',
|
|
urlRoot: '/api/ostf'
|
|
});
|
|
|
|
models.FuelVersion = BaseModel.extend({
|
|
constructorName: 'FuelVersion',
|
|
urlRoot: '/api/version',
|
|
authExempt: true
|
|
});
|
|
|
|
models.User = BaseModel.extend({
|
|
constructorName: 'User',
|
|
locallyStoredAttributes: ['username', 'token'],
|
|
initialize() {
|
|
_.each(this.locallyStoredAttributes, (attribute) => {
|
|
var locallyStoredValue = localStorage.getItem(attribute);
|
|
if (locallyStoredValue) {
|
|
this.set(attribute, locallyStoredValue);
|
|
}
|
|
this.on('change:' + attribute, (model, value) => {
|
|
if (_.isUndefined(value)) {
|
|
localStorage.removeItem(attribute);
|
|
} else {
|
|
localStorage.setItem(attribute, value);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
models.LogsPackage = BaseModel.extend({
|
|
constructorName: 'LogsPackage',
|
|
urlRoot: '/api/logs/package'
|
|
});
|
|
|
|
models.CapacityLog = BaseModel.extend({
|
|
constructorName: 'CapacityLog',
|
|
urlRoot: '/api/capacity'
|
|
});
|
|
|
|
models.WizardModel = Backbone.DeepModel.extend({
|
|
constructorName: 'WizardModel',
|
|
parseConfig(config) {
|
|
var result = {};
|
|
_.each(config, (paneConfig, paneName) => {
|
|
result[paneName] = {};
|
|
_.each(paneConfig, (attributeConfig, attribute) => {
|
|
var attributeConfigValue = attributeConfig.value;
|
|
if (_.isUndefined(attributeConfigValue)) {
|
|
switch (attributeConfig.type) {
|
|
case 'checkbox':
|
|
attributeConfigValue = false;
|
|
break;
|
|
case 'radio':
|
|
attributeConfigValue = _.first(attributeConfig.values).data;
|
|
break;
|
|
case 'password':
|
|
case 'text':
|
|
attributeConfigValue = '';
|
|
break;
|
|
}
|
|
}
|
|
result[paneName][attribute] = attributeConfigValue;
|
|
});
|
|
});
|
|
return result;
|
|
},
|
|
processConfig(config) {
|
|
this.set(this.parseConfig(config));
|
|
},
|
|
restoreDefaultValues(panesToRestore) {
|
|
var result = {};
|
|
_.each(this.defaults, (paneConfig, paneName) => {
|
|
if (_.contains(panesToRestore, paneName)) {
|
|
result[paneName] = this.defaults[paneName];
|
|
}
|
|
});
|
|
this.set(result);
|
|
},
|
|
validate(attrs, options) {
|
|
var errors = [];
|
|
_.each(options.config, (attributeConfig, attribute) => {
|
|
if (!(attributeConfig.regex && attributeConfig.regex.source)) return;
|
|
var hasNoSatisfiedRestrictions = _.every(_.reject(attributeConfig.restrictions,
|
|
{action: 'none'}), (restriction) => {
|
|
// this probably will be changed when other controls need validation
|
|
return !utils.evaluateExpression(restriction.condition, {default: this}).value;
|
|
});
|
|
if (hasNoSatisfiedRestrictions) {
|
|
var regExp = new RegExp(attributeConfig.regex.source);
|
|
if (!this.get(options.paneName + '.' + attribute).match(regExp)) {
|
|
errors.push({
|
|
field: attribute,
|
|
message: i18n(attributeConfig.regex.error)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
return errors.length ? errors : null;
|
|
},
|
|
initialize(config) {
|
|
this.defaults = this.parseConfig(config);
|
|
}
|
|
});
|
|
|
|
models.MirantisCredentials = Backbone.DeepModel.extend(superMixin).extend({
|
|
constructorName: 'MirantisCredentials',
|
|
baseUrl: 'https://software.mirantis.com/wp-content/themes/' +
|
|
'mirantis_responsive_v_1_0/scripts/fuel_forms_api/',
|
|
validate(attrs) {
|
|
var errors = {};
|
|
_.each(attrs, (group, groupName) => {
|
|
_.each(group, (setting, settingName) => {
|
|
var path = this.makePath(groupName, settingName);
|
|
if (!setting.regex || !setting.regex.source) return;
|
|
if (!setting.value.match(new RegExp(setting.regex.source))) {
|
|
errors[path] = setting.regex.error;
|
|
}
|
|
});
|
|
});
|
|
return _.isEmpty(errors) ? null : errors;
|
|
},
|
|
makePath(...args) {
|
|
return args.join('.');
|
|
}
|
|
});
|
|
|
|
models.MirantisLoginForm = models.MirantisCredentials.extend({
|
|
constructorName: 'MirantisLoginForm',
|
|
url() {
|
|
return this.baseUrl + 'login';
|
|
},
|
|
nailgunUrl: 'api/tracking/login'
|
|
});
|
|
|
|
models.MirantisRegistrationForm = models.MirantisCredentials.extend({
|
|
constructorName: 'MirantisRegistrationForm',
|
|
url() {
|
|
return this.baseUrl + 'registration';
|
|
},
|
|
nailgunUrl: 'api/tracking/registration'
|
|
});
|
|
|
|
models.MirantisRetrievePasswordForm = models.MirantisCredentials.extend({
|
|
constructorName: 'MirantisRetrievePasswordForm',
|
|
url() {
|
|
return this.baseUrl + 'restore_password';
|
|
},
|
|
nailgunUrl: 'api/tracking/restore_password'
|
|
});
|
|
|
|
models.NodeNetworkGroup = BaseModel.extend({
|
|
constructorName: 'NodeNetworkGroup',
|
|
urlRoot: '/api/nodegroups',
|
|
validate(options = {}) {
|
|
var newName = _.trim(options.name) || '';
|
|
if (!newName) {
|
|
return i18n('cluster_page.network_tab.node_network_group_empty_name');
|
|
}
|
|
if ((this.collection || options.nodeNetworkGroups).any({name: newName})) {
|
|
return i18n('cluster_page.network_tab.node_network_group_duplicate_error');
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
|
|
models.NodeNetworkGroups = BaseCollection.extend(cacheMixin).extend({
|
|
constructorName: 'NodeNetworkGroups',
|
|
cacheFor: 60 * 1000,
|
|
model: models.NodeNetworkGroup,
|
|
url: '/api/nodegroups',
|
|
comparator: (nodeNetworkGroup) => -nodeNetworkGroup.get('is_default')
|
|
});
|
|
|
|
models.PluginLink = BaseModel.extend({
|
|
constructorName: 'PluginLink'
|
|
});
|
|
|
|
models.PluginLinks = BaseCollection.extend(cacheMixin).extend({
|
|
constructorName: 'PluginLinks',
|
|
cacheFor: 60 * 1000,
|
|
model: models.PluginLink,
|
|
comparator: 'id'
|
|
});
|
|
|
|
class ComponentPattern {
|
|
constructor(pattern) {
|
|
this.pattern = pattern;
|
|
this.parts = pattern.split(':');
|
|
this.hasWildcard = _.contains(this.parts, '*');
|
|
}
|
|
match(componentName) {
|
|
if (!this.hasWildcard) {
|
|
return this.pattern === componentName;
|
|
}
|
|
|
|
var componentParts = componentName.split(':');
|
|
if (componentParts.length < this.parts.length) {
|
|
return false;
|
|
}
|
|
var matched = true;
|
|
_.each(this.parts, (part, index) => {
|
|
if (part !== '*') {
|
|
if (part !== componentParts[index]) {
|
|
matched = false;
|
|
return matched;
|
|
}
|
|
}
|
|
});
|
|
return matched;
|
|
}
|
|
}
|
|
|
|
models.ComponentModel = BaseModel.extend({
|
|
initialize(component) {
|
|
var parts = component.name.split(':');
|
|
this.set({
|
|
id: component.name,
|
|
enabled: component.enabled,
|
|
type: parts[0],
|
|
subtype: parts[1],
|
|
name: component.name,
|
|
label: i18n(component.label),
|
|
description: component.description && i18n(component.description),
|
|
compatible: component.compatible,
|
|
incompatible: component.incompatible,
|
|
weight: component.weight || 100
|
|
});
|
|
},
|
|
expandWildcards(components) {
|
|
var expandProperty = (propertyName, components) => {
|
|
var expandedComponents = [];
|
|
_.each(this.get(propertyName), (patternDescription) => {
|
|
var patternName = _.isString(patternDescription) ? patternDescription :
|
|
patternDescription.name;
|
|
var pattern = new ComponentPattern(patternName);
|
|
components.each((component) => {
|
|
if (pattern.match(component.id)) {
|
|
expandedComponents.push({
|
|
component: component,
|
|
message: i18n(patternDescription.message || '')
|
|
});
|
|
}
|
|
});
|
|
});
|
|
return expandedComponents;
|
|
};
|
|
|
|
this.set({
|
|
compatible: expandProperty('compatible', components),
|
|
incompatible: expandProperty('incompatible', components),
|
|
requires: expandProperty('requires', components)
|
|
});
|
|
},
|
|
restoreDefaultValue() {
|
|
this.set({enabled: this.get('default')});
|
|
},
|
|
toJSON() {
|
|
return this.get('enabled') ? this.id : null;
|
|
},
|
|
isML2Driver() {
|
|
return /:ml2:\w+$/.test(this.id);
|
|
}
|
|
});
|
|
|
|
models.ComponentsCollection = BaseCollection.extend({
|
|
model: models.ComponentModel,
|
|
allTypes: ['hypervisor', 'network', 'storage', 'additional_service'],
|
|
initialize(models, options) {
|
|
this.releaseId = options.releaseId;
|
|
},
|
|
url() {
|
|
return '/api/v1/releases/' + this.releaseId + '/components';
|
|
},
|
|
parse(response) {
|
|
return _.isArray(response) ? response : [];
|
|
},
|
|
getComponentsByType(type, options = {sorted: true}) {
|
|
var components = this.where({type: type});
|
|
if (options.sorted) {
|
|
components.sort((component1, component2) => {
|
|
return component1.get('weight') - component2.get('weight');
|
|
});
|
|
}
|
|
return components;
|
|
},
|
|
restoreDefaultValues(types) {
|
|
types = types || this.allTypes;
|
|
var components = _.filter(this.models, (model) => _.contains(types, model.get('type')));
|
|
_.invoke(components, 'restoreDefaultValue');
|
|
},
|
|
toJSON() {
|
|
return _.compact(_.map(this.models, (model) => model.toJSON()));
|
|
}
|
|
});
|
|
|
|
export default models;
|