fuel-ui/static/models.js

1898 lines
62 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 'underscore';
import i18n from 'i18n';
import Backbone from 'backbone';
import Cookies from 'js-cookie';
import Expression from 'expression';
import {ModelPath} from 'expression/objects';
import utils from 'utils';
import customControls from 'views/custom_controls';
import {Input} from 'views/controls';
import deepModelMixin from 'deep_model_mixin';
import {DEPLOYMENT_GRAPH_LEVELS} from 'consts';
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) => _.includes(ids, model.id));
}
};
var collectionMethods = [
'dropRightWhile', 'dropWhile', 'takeRightWhile', 'takeWhile',
'findIndex', 'findLastIndex',
'findKey', 'findLastKey',
'find', 'findLast',
'filter', 'reject',
'every', 'some', 'invokeMap',
'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 fetchOptionsMixin = {
initialize({fetchOptions} = {fetchOptions: {}}) {
this._super('initialize', arguments);
this.updateFetchOptions(fetchOptions);
},
updateFetchOptions(fetchOptions) {
this.fetchOptions = fetchOptions;
},
fetch(options) {
var fetchOptions = _.result(this, 'fetchOptions', {});
return Backbone.Collection.prototype.fetch.call(
this, !_.isEmpty(fetchOptions) ? _.extend({data: fetchOptions}, options) : options
);
}
};
var BaseModel = models.BaseModel = Backbone.Model.extend(superMixin);
var BaseCollection = models.BaseCollection =
Backbone.Collection.extend(collectionMixin).extend(superMixin).extend(fetchOptionsMixin);
var cacheMixin = {
fetch(options) {
if (this.cacheFor && options && options.cache && this.lastSyncTime &&
(this.cacheFor > (new Date() - this.lastSyncTime))) {
return Promise.resolve();
}
if (options) delete options.cache;
return this._super('fetch', arguments);
},
sync() {
var promise = this._super('sync', arguments);
if (this.cacheFor) {
promise.then(() => {
this.lastSyncTime = new Date();
});
}
return promise;
},
cancelThrottling() {
delete this.lastSyncTime;
}
};
models.cacheMixin = cacheMixin;
var restrictionMixin = models.restrictionMixin = {
checkRestrictions(models, actions, setting) {
var restrictions = _.map(
setting ? setting.restrictions : this.get('restrictions'),
utils.expandRestriction
);
if (!actions) {
actions = [];
} else if (_.isString(actions)) {
actions = [actions];
}
if (actions.length) {
restrictions = _.filter(restrictions,
(restriction) => _.includes(actions, restriction.action)
);
}
var satisfiedRestrictions = _.filter(restrictions,
(restriction) => new Expression(restriction.condition, models, restriction).evaluate()
);
return {
result: !!satisfiedRestrictions.length,
message: _.compact(_.map(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) => {
if (_.isUndefined(expression) || _.isNumber(expression)) return expression;
var result = (new Expression(expression, models)).evaluate();
if (result instanceof ModelPath) result = result.model.get(result.attribute);
return result;
};
var checkedLimitTypes = {};
var name = this.get('name');
var limits = this.expandedLimits[name] || {};
var overrides = limits.overrides || [];
var limitValues = {
max: evaluateExpressionHelper(limits.max, models),
min: evaluateExpressionHelper(limits.min, models),
recommended: evaluateExpressionHelper(limits.recommended, models)
};
var count = nodes.nodesAfterDeploymentWithRole(name).length;
var messages;
var label = this.get('label');
var checkOneLimit = (obj, limitType) => {
if (_.isUndefined(obj[limitType])) return null;
var limitValue, comparator;
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), 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;
return comparator(count, limitValue) && {
type: limitType,
value: limitValue,
message: obj.message ||
i18n('common.role_limits.' + limitType, {limitValue, count, roleName: label})
};
};
// Check the overridden limit types
messages = _.chain(overrides)
.map((override) => {
var exp = evaluateExpressionHelper(override.condition, models);
return exp && _.map(limitTypes, _.partial(checkOneLimit, override));
})
.flatten()
.compact()
.value();
// Now check the global, not-overridden limit types
messages = messages.concat(_.chain(limitTypes)
.map((limitType) => !checkedLimitTypes[limitType] && 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();
return (message[0] || {}).message;
});
messages = _.compact(messages).join(' ');
return {
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,
groups: ['base', 'compute', 'storage', 'other'],
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.find({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(),
nodeNetworkGroups: new models.NodeNetworkGroups(),
transactions: new models.Transactions(),
deploymentGraphs: new models.DeploymentGraphs()
};
_.each(defaults, (collection, key) => {
collection.cluster = this;
collection.updateFetchOptions(() => {
if (key === 'deploymentGraphs') return {clusters_ids: this.id, fetch_related: 1};
var fetchOptions = {cluster_id: this.id};
if (key === 'transactions') {
fetchOptions.transaction_types = ['deployment', 'dry_run_deployment'].join(',');
}
return fetchOptions;
});
});
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').some({pending_addition: false, status: 'error'}) &&
this.get('status') !== 'update_error';
},
isAvailableForSettingsChanges() {
return !this.get('is_locked');
},
isDeploymentPossible({configModels}) {
return this.get('release').get('state') !== 'unavailable' &&
!this.task({group: 'deployment', active: true}) && (
this.get('status') !== 'operational' ||
this.hasChanges({configModels}) ||
this.needsRedeployment()
);
},
isConfigurationChanged({configModels}) {
var settings = this.get('settings');
var deployedSettings = this.get('deployedSettings');
var networkConfiguration = this.get('networkConfiguration');
var deployedNetworkConfiguration = this.get('deployedNetworkConfiguration');
if (
this.get('status') === 'new' ||
_.isEmpty(deployedSettings.attributes) ||
_.isEmpty(deployedNetworkConfiguration.attributes)
) return false;
if (settings.hasChanges(deployedSettings.attributes, configModels)) return true;
if (
!_.isEqual(
networkConfiguration.get('networking_parameters').toJSON(),
deployedNetworkConfiguration.get('networking_parameters').toJSON()
)
) return true;
return networkConfiguration.get('networks').some((network) => {
var deployedNetwork = deployedNetworkConfiguration.get('networks').get(network.id);
if (!deployedNetwork || !network.get('meta').configurable) return false;
return _.some(network.getEditableAttributes(), (attribute) => {
if (_.isArray(attribute)) {
// check network metadata changes
var networkMetadata = network.get('meta');
var deployedNetworkMetadata = deployedNetwork.get('meta');
return _.some(attribute,
(metadata) => !_.isEqual(networkMetadata[metadata], deployedNetworkMetadata[metadata])
);
}
return !_.isEqual(network.get(attribute), deployedNetwork.get(attribute));
});
});
},
hasChanges({configModels}) {
return this.get('nodes').hasChanges() || this.isConfigurationChanged({configModels});
},
getAllocatedRoles() {
return _.chain(this.get('nodes').filter({pending_deletion: false}))
.map((node) => node.get('roles').concat(node.get('pending_roles')))
.flatten()
.uniq()
.value();
},
isHealthCheckAvailable() {
return _.includes(['operational', 'error'], this.get('status'));
}
});
models.Clusters = BaseCollection.extend({
constructorName: 'Clusters',
model: models.Cluster,
url: '/api/clusters',
comparator: 'id'
});
models.Node = BaseModel.extend({
constructorName: 'Node',
urlRoot: '/api/nodes',
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 = _.map(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(roles = [], onlyDeployedRoles = false) {
if (_.isString(roles)) roles = [roles];
var nodeRoles = this.get('roles');
if (!onlyDeployedRoles) nodeRoles = nodeRoles.concat(this.get('pending_roles'));
return !!_.intersection(nodeRoles, roles).length;
},
isProvisioningPossible() {
var status = this.get('status');
return (
status === 'discover' ||
status === 'error' && this.get('error_type') === 'provision'
) &&
// virt nodes should be provisioned with spawn_vms task
!this.hasRole('virt');
},
isDeploymentPossible() {
var status = this.get('status');
return status === 'provisioned' ||
status === 'stopped' ||
status === 'error' && this.get('error_type') === 'deploy' ||
status === 'ready' && !!this.get('pending_roles').length;
},
hasChanges() {
return this.get('pending_addition') ||
this.get('pending_deletion') ||
this.get('status') === 'stopped' ||
!!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');
var error = this.get('error_type');
// FIXME(jaranovich): #1592355 - interfaces of 'ready' node should be also configurable
return status === 'discover' || status === 'stopped' ||
status === 'error' && (error === 'discover' || error === 'deploy');
},
getRolesSummary(releaseRoles) {
return _.map(this.sortedRoles(releaseRoles.map('name')),
(role) => releaseRoles.find({name: role}).get('label')
).join(', ');
},
getStatus() {
if (!this.get('online') && this.get('status') !== 'removing') return 'offline';
return this.get('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',
hasChanges() {
return _.some(this.invokeMap('hasChanges'));
},
nodesAfterDeployment() {
return this.filter((node) => !node.get('pending_deletion'));
},
nodesAfterDeploymentWithRole(role) {
return _.filter(this.nodesAfterDeployment(), (node) => node.hasRole(role));
},
resources(resourceName) {
return _.reduce(this.invokeMap('resource', resourceName), (sum, n) => sum + n, 0);
},
getLabelValues(label) {
return this.invokeMap('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.some((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.invokeMap('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: [
'stop_deployment',
'deploy',
'provision',
'deployment',
'dry_run_deployment',
'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) &&
!_.includes(this.extendGroups(filters), this.get('name'))) {
return false;
}
if ((filters.status || _.isBoolean(filters.active)) &&
!_.includes(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', 'provision', 'deployment']});
}
});
models.Tasks = BaseCollection.extend({
constructorName: 'Tasks',
model: models.Task,
url: '/api/tasks',
toJSON() {
return this.map('id');
},
comparator: 'id',
filterTasks(filters) {
return _.chain(this.model.prototype.extendGroups(filters))
.map((name) => {
return this.filter((task) => task.match(_.extend(_.omit(filters, 'group'), {name: name})));
})
.flatten()
.compact()
.value();
},
findTask(filters) {
return this.filterTasks(filters)[0];
}
});
models.Transaction = models.Task.extend({
constructorName: 'Transaction'
});
models.Transactions = models.Tasks.extend({
constructorName: 'Transactions',
model: models.Transaction,
url: '/api/transactions'
});
models.DeploymentTask = BaseModel.extend({
constructorName: 'DeploymentTask'
});
models.DeploymentTasks = BaseCollection.extend({
constructorName: 'DeploymentTasks',
model: models.DeploymentTask,
parse(response) {
// no need to show tasks of Virtual Sync Node (node_id is Null)
// also no need to show tasks that were not executed on any node (node_id is '-')
return _.filter(response, (task) => !_.isNull(task.node_id) && task.node_id !== '-');
},
fetch(options) {
options = _.extend({}, options, {beforeSend: (xhr) => {
xhr.then(() => {
this.lastFetchDate = xhr.getResponseHeader('Date');
});
}});
return this._super('fetch', [options]);
}
});
models.DeploymentGraph = BaseModel.extend({
constructorName: 'DeploymentGraph',
urlRoot: 'api/graphs/',
toJSON() {
var attributes = this._super('toJSON', arguments);
return _.omit(attributes, 'type');
},
getType() {
// relations attribute has one element only for now
return this.get('relations')[0].type;
},
getLevel() {
return this.get('relations')[0].model;
},
validate(attrs, {usedTypes = []}) {
var errors = {};
if (!attrs.type || !attrs.type.match(/^[\w-]+$/)) {
errors.type = i18n('dialog.upload_graph.invalid_type');
} else if (_.includes(usedTypes, attrs.type)) {
errors.type = i18n('dialog.upload_graph.existing_type');
}
return _.isEmpty(errors) ? null : errors;
}
});
models.DeploymentGraphs = BaseCollection
.extend(cacheMixin)
.extend({
url: '/api/graphs',
constructorName: 'DeploymentGraphs',
cacheFor: 60 * 1000,
model: models.DeploymentGraph,
comparator(graph1, graph2) {
// sort graphs by type then by level
var type1 = graph1.getType();
var type2 = graph2.getType();
if (type1 === type2) {
var level1 = graph1.getLevel();
var level2 = graph2.getLevel();
if (level1 === level2) return graph1.id - graph2.id;
return _.indexOf(DEPLOYMENT_GRAPH_LEVELS, level1) -
_.indexOf(DEPLOYMENT_GRAPH_LEVELS, level2);
}
// graphs with type 'default' should go first
if (type1 === 'default') return -1;
if (type2 === 'default') return 1;
return utils.natsort(type1, type2);
}
});
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 = BaseModel
.extend(deepModelMixin)
.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 this.root ? response[this.root] : response;
},
mergePluginSettings(pluginNames) {
if (!pluginNames) {
pluginNames = _.compact(_.map(this.attributes,
(section, sectionName) =>
this.isPlugin(section) && section.metadata.versions && sectionName
));
} else if (_.isString(pluginNames)) {
pluginNames = [pluginNames];
}
var mergeSettings = (pluginName) => {
var plugin = this.get(pluginName);
var chosenVersionData = plugin.metadata.versions.find(
(version) => version.metadata.plugin_id === plugin.metadata.chosen_id
);
// merge metadata of a chosen plugin version
_.extend(plugin.metadata,
_.omit(chosenVersionData.metadata, 'plugin_id', 'plugin_version'));
// merge settings of a chosen plugin version
this.attributes[pluginName] = _.extend(_.pick(plugin, 'metadata'),
_.omit(chosenVersionData, 'metadata'));
};
_.each(pluginNames, mergeSettings);
},
toJSON() {
var settings = this._super('toJSON', arguments);
// update plugin settings
_.each(settings, (section, sectionName) => {
if (this.isPlugin(section) && section.metadata.versions) {
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');
}
});
if (!this.root) return settings;
return {[this.root]: settings};
},
initialize() {
// merge settings of newly installed plugins
this.on('sync', ({attributes}) => {
var newPlugins = _.compact(_.map(attributes, (section, sectionName) =>
this.isPlugin(section) && _.isEmpty(_.omit(section, 'metadata')) && sectionName
));
if (newPlugins.length) this.mergePluginSettings(newPlugins);
}, this);
},
validate(attrs, options) {
var errors = {};
var models = options ? options.models : {};
var checkRestrictions =
(setting) => this.checkRestrictions(models, ['disable', 'hide'], 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 = utils.makePath(groupName, settingName);
// support of custom controls
var CustomControl = customControls[setting.type];
if (CustomControl && _.isFunction(CustomControl.validate)) {
var error = CustomControl.validate(setting, models);
if (error) errors[path] = error;
return;
}
var inputError = Input.validate(setting);
if (inputError) errors[path] = inputError;
});
});
return _.isEmpty(errors) ? null : errors;
},
getValueAttribute(settingName) {
return settingName === 'metadata' ? 'enabled' : 'value';
},
hasChanges(datatoCheck, models) {
return _.some(this.attributes, (section, sectionName) => {
// settings (plugins) installed to already deployed environment
// are not presented in the environment deployed configuration
var sectionToCheck = datatoCheck[sectionName];
var {metadata} = section;
if (!sectionToCheck) return !metadata.toggleable || metadata.enabled;
if (metadata) {
if (!sectionToCheck.metadata) return false;
// restrictions with action = 'none' should not block checking of the setting section
if (this.checkRestrictions(models, ['disable', 'hide'], metadata).result) return false;
// check the section enableness
if (
!_.isUndefined(metadata.enabled) && metadata.enabled !== sectionToCheck.metadata.enabled
) return true;
// check a chosen plugin version
if (
this.isPlugin(section) && metadata.chosen_id !== sectionToCheck.metadata.chosen_id
) return true;
}
// do not check inactive setting sections
if ((metadata || {}).enabled === false) return false;
return _.some(_.omit(section, 'metadata'), (setting, settingName) => {
// restrictions with action = 'none' should not block checking of the setting
if (this.checkRestrictions(models, ['disable', 'hide'], setting).result) return false;
return !_.isEqual(setting.value, (sectionToCheck[settingName] || {}).value);
});
});
},
sanitizeGroup(group) {
return _.includes(this.groupList, group) ? group : 'other';
},
isSettingVisible(setting, settingName, configModels) {
return settingName !== 'metadata' &&
(setting.type !== 'hidden' || setting.description || setting.label) &&
!this.checkRestrictions(configModels, 'hide', setting).result;
},
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);
},
updateAttributes(newSettings, models, updateNetworkSettings) {
/*
* updateNetworkSettings (boolean):
* if true, update settings from 'network' group only
* if false, do not update settings from 'network' group
* if not specified (default), update all settings
**/
_.each(this.attributes, (section, sectionName) => {
var isNetworkGroup = section.metadata.group === 'network';
var shouldSectionBeUpdated = _.isUndefined(updateNetworkSettings) ||
updateNetworkSettings === isNetworkGroup;
if (shouldSectionBeUpdated) {
if (this.isPlugin(section)) {
if (newSettings.get(sectionName)) {
var pathToMetadata = utils.makePath(sectionName, 'metadata');
_.extend(
this.get(pathToMetadata),
_.pick(newSettings.get(pathToMetadata), 'enabled', 'chosen_id', 'versions')
);
this.mergePluginSettings(sectionName);
}
} else if (newSettings.get(sectionName)) {
_.each(section, (setting, settingName) => {
// do not update hidden settings (hack for #1442143)
var shouldSettingBeUpdated = setting.type !== 'hidden' && (
_.isUndefined(updateNetworkSettings) ||
isNetworkGroup ||
setting.group !== 'network'
);
if (shouldSettingBeUpdated) {
var path = utils.makePath(sectionName, settingName);
this.set(path, newSettings.get(path));
}
});
}
}
});
this.isValid({models});
}
});
models.NodeAttributes = models.Settings.extend({
constructorName: 'NodeAttributes',
root: null
});
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/',
editableAttributes: ['volumes', 'bootable'],
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').find({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(deepModelMixin)
.extend({
constructorName: 'Interface',
parse(response) {
response.assigned_networks = new models.InterfaceNetworks(response.assigned_networks);
response.assigned_networks.interface = this;
response.attributes = new models.InterfaceAttributes(response.attributes);
return response;
},
toJSON(options) {
return _.omit(_.extend(this.constructor.__super__.toJSON.call(this, options), {
assigned_networks: this.get('assigned_networks').toJSON(),
attributes: this.get('attributes').toJSON()
}), 'checked');
},
isBond() {
return this.get('type') === 'bond';
},
getSlaveInterfaces() {
if (!this.isBond()) return [this];
var slaveNames = _.map(this.get('slaves'), 'name');
return this.collection.filter((ifc) => _.includes(slaveNames, ifc.get('name')));
},
validate(attrs, options) {
var errors = {};
var networkErrors = [];
var networks = new models.Networks(this.get('assigned_networks')
.invokeMap('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.some({name: 'public'}) &&
networks.some({name: 'floating'}) ? 2 : 1;
if (untaggedNetworks.length > maxUntaggedNetworksCount) {
networkErrors.push(i18n(ns + 'too_many_untagged_networks'));
}
// check interface networks have the same vlan id
var vlans = _.reject(networks.map('vlan_start'), _.isNull);
if (_.uniq(vlans).length < vlans.length) {
networkErrors.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 (
_.some(vlanRanges,
(currentRange) => _.some(vlanRanges,
(range) => !_.isEqual(currentRange, range) &&
range[1] >= currentRange[0] && range[0] <= currentRange[1]
)
)
) networkErrors.push(i18n(ns + 'vlan_range_intersection'));
// network assignment validation based on interface attributes
var attributes = this.get('attributes');
if (
attributes.get('sriov.enabled.value') &&
networks.length
) networkErrors.push(i18n(ns + 'sriov_placement_error'));
if (
attributes.get('dpdk.enabled.value') &&
!_.isEqual(networks.map('name'), ['private'])
) networkErrors.push(i18n(ns + 'dpdk_placement_error'));
if (networkErrors.length) errors.networks = networkErrors;
attributes.isValid(options);
if (!_.isNull(attributes.validationError)) {
errors.attributes = attributes.validationError;
}
return _.isEmpty(errors) ? null : 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.some({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.find({name: this.get('name')});
}
});
models.InterfaceNetworks = BaseCollection.extend({
constructorName: 'InterfaceNetworks',
model: models.InterfaceNetwork,
comparator(network) {
return _.indexOf(networkPreferredOrder, network.get('name'));
}
});
models.InterfaceAttributes = models.Settings.extend({
constructorName: 'InterfaceAttributes',
root: null,
getValueAttribute() {
return 'value';
},
validate(attrs, options = {}) {
options.models = _.extend({nic_attributes: this, default: this}, options.configModels);
var errors = this._super('validate', [attrs, options]);
// FIXME(jkirnosova): the following hack should be removed after fix of sriov.numvfs null value
if (!((options.meta || {}).sriov || {}).available || !this.get('sriov.enabled.value')) {
// clear sriov errors if sriov is unavailable or not enabled
errors = _.omitBy(errors, (value, key) => _.startsWith(key, 'sriov.'));
}
// MTU has a special validation case which depends on DPDK and cant be moved to YAML
// SRIOV virtual functions number has maximum which is set into interface plain object metadata
_.extend(errors, this.validateMTU(options), this.validateSRIOV(options));
return _.isEmpty(errors) ? null : errors;
},
validateMTU() {
var mtu = this.get('mtu.value.value');
return this.get('dpdk.enabled.value') && _.isNumber(mtu) && mtu > 1500 ? {
'mtu.value': i18n('cluster_page.nodes_tab.configure_interfaces.validation.dpdk_mtu_error')
} : null;
},
validateSRIOV({meta = {}}) {
var totalVirtualFunctionsNumber = Number((meta.sriov || {}).totalvfs);
return (meta.sriov || {}).available && this.get('sriov.enabled.value') &&
_.isNumber(totalVirtualFunctionsNumber) &&
this.get('sriov.numvfs.value') > totalVirtualFunctionsNumber ? {
'sriov.numvfs': i18n(
'cluster_page.nodes_tab.configure_interfaces.validation.big_virtual_functions_number',
{max: totalVirtualFunctionsNumber}
)
} : null;
}
});
models.BondDefaultAttributes = BaseModel
.extend(cacheMixin)
.extend({
constructorName: 'BondDefaultAttributes',
url() {
return '/api/v1/nodes/' + this.nodeId + '/bonds/attributes/defaults';
}
});
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');
},
getEditableAttributes() {
if (!this.get('meta').configurable) return [];
// meta attributes are stored as a list
var editableAttributes = ['cidr', 'ip_ranges', 'vlan_start', ['notation']];
if (this.get('meta').use_gateway) editableAttributes.push('gateway');
return editableAttributes;
}
});
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;
},
updateEditableAttributes(newNetworkConfiguration, nodeNetworkGroups) {
this.get('networks').each((network) => {
var newNetwork = newNetworkConfiguration.get('networks').get(network.id);
if (newNetwork) {
_.each(network.getEditableAttributes(), (attribute) => {
if (_.isArray(attribute)) {
_.extend(network.get('meta'), _.pick(newNetwork.get('meta'), attribute));
network.set('meta', network.get('meta'));
} else {
network.set(attribute, newNetwork.get(attribute));
}
});
}
});
this.get('networking_parameters').set(
_.cloneDeep(newNetworkConfiguration.get('networking_parameters').attributes)
);
this.isValid({nodeNetworkGroups});
},
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;
},
validateFixedNetworksAmount(fixedNetworksAmount, fixedNetworkVlan) {
if (!utils.isNaturalNumber(parseInt(fixedNetworksAmount, 10))) {
return {fixed_networks_amount: i18n('cluster_page.network_tab.validation.invalid_amount')};
}
if (fixedNetworkVlan && fixedNetworksAmount > 4095 - fixedNetworkVlan) {
return {fixed_networks_amount: i18n('cluster_page.network_tab.validation.need_more_vlan')};
}
return null;
},
validateNeutronSegmentationIdRange([idStart, idEnd], isVlanSegmentation, vlans = []) {
var ns = 'cluster_page.network_tab.validation.';
var maxId = isVlanSegmentation ? 4094 : 65535;
var errors = _.map([idStart, idEnd], (id, index) => {
return !utils.isNaturalNumber(id) || id < 2 || id > maxId ?
i18n(ns + (index === 0 ? 'invalid_id_start' : 'invalid_id_end')) : '';
});
if (errors[0] || errors[1]) return errors;
errors[0] = errors[1] = idStart === idEnd ?
i18n(ns + 'not_enough_id')
:
idStart > idEnd ? i18n(ns + 'invalid_id_range') : '';
if (errors[0] || errors[1]) return errors;
if (isVlanSegmentation) {
if (_.some(vlans, (vlan) => utils.validateVlanRange(idStart, idEnd, vlan))) {
errors[0] = errors[1] = i18n(ns + 'vlan_intersection');
}
}
return errors;
},
validateNeutronFloatingRange(floatingRanges, networks, networkErrors, nodeNetworkGroups) {
var error = utils.validateIPRanges(floatingRanges, null);
if (!_.isEmpty(error)) return error;
var networksToCheck = networks.filter((network) => {
var cidrError;
try {
cidrError = !!networkErrors[network.get('group_id')][network.id].cidr;
} catch (ignore) {}
if (cidrError || !network.get('meta').floating_range_var) return false;
var [floatingRangeStart, floatingRangeEnd] = floatingRanges[0];
var cidr = network.get('cidr');
return utils.validateIpCorrespondsToCIDR(cidr, floatingRangeStart) &&
utils.validateIpCorrespondsToCIDR(cidr, floatingRangeEnd);
});
if (networksToCheck.length) {
_.each(networksToCheck, (network) => {
error = utils.validateIPRanges(
floatingRanges,
network.get('cidr'),
_.filter(network.get('ip_ranges'), (range, index) => {
var ipRangeError = false;
try {
ipRangeError = !_.every(range) || _.some(
networkErrors[network.get('group_id')][network.id].ip_ranges,
{index: index}
);
} catch (ignore) {}
return !ipRangeError;
}),
{
IP_RANGES_INTERSECTION: i18n(
'cluster_page.network_tab.validation.floating_and_public_ip_ranges_intersection',
{
cidr: network.get('cidr'),
network: _.capitalize(network.get('name')),
nodeNetworkGroup: nodeNetworkGroups.get(network.get('group_id')).get('name')
}
)
}
);
return _.isEmpty(error);
});
} else {
error = [{index: 0}];
error[0].start = error[0].end =
i18n('cluster_page.network_tab.validation.floating_range_is_not_in_public_cidr');
}
return error;
},
validateNetwork(network) {
var cidr = network.get('cidr');
var errors = {};
_.extend(errors, utils.validateCidr(cidr));
var cidrError = _.has(errors, 'cidr');
_.extend(errors, this.validateNetworkIpRanges(network, cidrError ? null : cidr));
if (network.get('meta').use_gateway) {
_.extend(
errors,
utils.validateGateway(network.get('gateway'), cidrError ? null : cidr)
);
}
_.extend(errors, utils.validateVlan(network.get('vlan_start')));
return errors;
},
validateNeutronParameters(parameters, networks, networkErrors, nodeNetworkGroups) {
var errors = {};
var isVlanSegmentation = parameters.get('segmentation_type') === 'vlan';
var idRangeAttributeName = isVlanSegmentation ? 'vlan_range' : 'gre_id_range';
var idRangeErrors = this.validateNeutronSegmentationIdRange(
_.map(parameters.get(idRangeAttributeName), Number),
isVlanSegmentation,
_.compact(networks.map('vlan_start'))
);
if (idRangeErrors[0] || idRangeErrors[1]) errors[idRangeAttributeName] = idRangeErrors;
if (!parameters.get('base_mac').match(utils.regexes.mac)) {
errors.base_mac = i18n('cluster_page.network_tab.validation.invalid_mac');
}
_.extend(errors, utils.validateCidr(parameters.get('internal_cidr'), 'internal_cidr'));
_.extend(
errors,
utils.validateGateway(
parameters.get('internal_gateway'),
parameters.get('internal_cidr'),
'internal_gateway'
)
);
_.each(['internal_name', 'floating_name'], (attribute) => {
if (!parameters.get(attribute).match(/^[a-z][\w\-]*$/i)) {
errors[attribute] = i18n('cluster_page.network_tab.validation.invalid_name');
}
});
var floatingRangeErrors = this.validateNeutronFloatingRange(
parameters.get('floating_ranges'),
networks,
networkErrors,
nodeNetworkGroups
);
if (floatingRangeErrors.length) errors.floating_ranges = floatingRangeErrors;
return errors;
},
validateBaremetalParameters(cidr, networkingParameters) {
var errors = {};
_.extend(
errors,
utils.validateGateway(
networkingParameters.get('baremetal_gateway'),
cidr,
'baremetal_gateway'
)
);
var baremetalRangeErrors = utils.validateIPRanges(
[networkingParameters.get('baremetal_range')],
cidr
);
if (baremetalRangeErrors.length) {
var [{start, end}] = baremetalRangeErrors;
errors.baremetal_range = [start, end];
}
return errors;
},
validateNameServers(nameservers) {
var errors = _.map(nameservers,
(nameserver) => !utils.validateIP(nameserver) &&
i18n('cluster_page.network_tab.validation.invalid_nameserver')
);
return _.compact(errors).length ? {dns_nameservers: errors} : null;
},
validate(attrs, options = {}) {
var networkingParameters = attrs.networking_parameters;
var errors = {};
// validate networks
var nodeNetworkGroupsErrors = {};
options.nodeNetworkGroups.map((nodeNetworkGroup) => {
var nodeNetworkGroupErrors = {};
var networksToCheck = new models.Networks(attrs.networks.filter((network) => {
return network.get('group_id') === nodeNetworkGroup.id && network.get('meta').configurable;
}));
networksToCheck.each((network) => {
var networkErrors = this.validateNetwork(network);
if (!_.isEmpty(networkErrors)) nodeNetworkGroupErrors[network.id] = networkErrors;
});
if (!_.isEmpty(nodeNetworkGroupErrors)) {
nodeNetworkGroupsErrors[nodeNetworkGroup.id] = nodeNetworkGroupErrors;
}
});
if (!_.isEmpty(nodeNetworkGroupsErrors)) errors.networks = nodeNetworkGroupsErrors;
// validate networking parameters
var networkingParametersErrors = this.validateNeutronParameters(
networkingParameters,
attrs.networks,
errors.networks,
options.nodeNetworkGroups
);
// it is only one baremetal network in environment
// so node network group filter is not needed here
var baremetalNetwork = attrs.networks.find({name: 'baremetal'});
if (baremetalNetwork) {
var baremetalCidrError = false;
try {
baremetalCidrError = errors
.networks[baremetalNetwork.get('group_id')][baremetalNetwork.id].cidr;
} catch (error) {}
_.extend(
networkingParametersErrors,
this.validateBaremetalParameters(
baremetalCidrError ? null : baremetalNetwork.get('cidr'),
networkingParameters
)
);
}
_.extend(
networkingParametersErrors,
this.validateNameServers(networkingParameters.get('dns_nameservers'))
);
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(cacheMixin).extend({
cacheFor: 60 * 1000,
constructorName: 'FuelVersion',
urlRoot: '/api/version'
});
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);
}
});
});
this.on('change:token', () => {
var token = this.get('token');
if (_.isUndefined(token)) {
Cookies.remove('token');
} else {
Cookies.set('token', token);
}
});
}
});
models.LogsPackage = BaseModel.extend({
constructorName: 'LogsPackage',
urlRoot: '/api/logs/package'
});
models.CapacityLog = BaseModel.extend({
constructorName: 'CapacityLog',
urlRoot: '/api/capacity'
});
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).some({name: newName})) {
return i18n('cluster_page.network_tab.node_network_group_duplicate_error');
}
return null;
}
});
models.NodeNetworkGroups = BaseCollection.extend({
constructorName: 'NodeNetworkGroups',
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 = _.includes(this.parts, '*');
}
match(componentName) {
if (!this.hasWildcard) return this.pattern === componentName;
var componentParts = componentName.split(':');
return componentParts.length >= this.parts.length &&
_.every(this.parts, (part, index) => part === '*' || part === componentParts[index]);
}
}
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)
});
},
predicates: {
one_of: (processedComponents = [], forthcomingComponents = []) => {
var enabledLength =
_.filter(processedComponents, (component) => component.get('enabled')).length;
var processedLength = processedComponents.length;
var forthcomingLength = forthcomingComponents.length;
return {
matched: (enabledLength === 0 && forthcomingLength > 0) || enabledLength === 1,
invalid: processedLength === 0 && forthcomingLength === 0
};
},
none_of: (processedComponents = []) => {
var enabledLength =
_.filter(processedComponents, (component) => component.get('enabled')).length;
return {
matched: enabledLength === 0,
invalid: false
};
},
any_of: (processedComponents = [], forthcomingComponents = []) => {
var enabledLength =
_.filter(processedComponents, (component) => component.get('enabled')).length;
var processedLength = processedComponents.length;
var forthcomingLength = forthcomingComponents.length;
return {
matched: (enabledLength === 0 && forthcomingLength > 0) || enabledLength >= 1,
invalid: processedLength === 0 && forthcomingLength === 0
};
},
all_of: (processedComponents = [], forthcomingComponents = []) => {
var processedLength = processedComponents.length;
var forthcomingLength = forthcomingComponents.length;
return {
matched: _.every(processedComponents, (component) => component.get('enabled')),
invalid: processedLength === 0 && forthcomingLength === 0
};
}
},
preprocessRequires(components) {
var componentIndex = {};
components.each((component) => {
componentIndex[component.id] = component;
});
var requires = this.get('requires');
if (requires && _.every(requires, (item) => item.name)) {
// convert old requires format to a new one
var newFormat = [
{
all_of: {
items: requires.map((require) => require.name),
message: _.last(requires).message
}
}
];
this.set({requires: newFormat});
}
var predicateNames = ['one_of', 'none_of', 'any_of', 'all_of'];
requires = _.map(this.get('requires'), (require) => {
var condition = {};
_.each(predicateNames, (predicate) => {
if (!_.isObject(require[predicate])) {
return true;
}
condition = _.extend(require[predicate], {predicate});
condition.items = _.map(condition.items, (name) => componentIndex[name]);
return false;
});
return condition;
});
this.set({requires});
},
processRequires(currentPaneIndex, paneMap) {
var result = _.reduce(this.get('requires'), (result, require) => {
var groupedComponents = _.groupBy(require.items, (item) => {
if (!item) {
return 'null';
}
var index = paneMap[item.get('type')];
return index <= currentPaneIndex ? 'processed' : 'forthcoming';
});
var predicate = this.predicates[require.predicate];
var predicateResult = predicate(groupedComponents.processed, groupedComponents.forthcoming);
var message = predicateResult.invalid ? require.message_invalid : require.message;
result.push(_.extend(predicateResult, {message: predicateResult.matched ? null : message}));
return result;
}, []);
var allMatched = _.every(result, (item) => item.matched);
this.set({
requireFail: !allMatched,
invalid: _.some(result, (item) => item.invalid)
});
return {
matched: allMatched,
warnings: _.compact(_.map(result, (item) => i18n(item.message))).join(' ')
};
},
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;
this.paneMap = {};
_.each(this.allTypes, (type, index) => {
this.paneMap[type] = index;
});
},
url() {
return '/api/v1/releases/' + this.releaseId + '/components';
},
parse(response) {
return _.isArray(response) ? response : [];
},
getComponentsByType(type, options = {sorted: true}) {
var components = this.filter({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) => _.includes(types, model.get('type')));
_.invokeMap(components, 'restoreDefaultValue');
},
toJSON() {
return _.compact(_.map(this.models, (model) => model.toJSON()));
},
processPaneRequires(paneType) {
var currentPaneIndex = this.paneMap[paneType];
this.each((component) => {
var componentPaneIndex = this.paneMap[component.get('type')];
if (component.get('disabled') || componentPaneIndex > currentPaneIndex) {
return;
}
var result = component.processRequires(currentPaneIndex, this.paneMap);
var isDisabled = !result.matched;
if (componentPaneIndex === currentPaneIndex) {
// current pane handling
component.set({
disabled: isDisabled,
warnings: isDisabled ? result.warnings : null,
enabled: isDisabled ? false : component.get('enabled'),
availability: 'incompatible'
});
} else if (!result.matched) {
// previous pane handling
component.set({
warnings: result.warnings
});
}
});
},
validate(paneType) {
// all the past panes should have all restrictions matched
// when not, errors dictionary is set
this.validationError = null;
var errors = [];
var currentPaneIndex = this.paneMap[paneType];
this.each((component) => {
var componentPaneIndex = this.paneMap[component.get('type')];
if (componentPaneIndex >= currentPaneIndex) {
return;
}
if (component.get('enabled') && component.get('requireFail')) {
errors.push(component.get('warnings'));
}
});
if (errors.length > 0) {
this.validationError = errors;
}
}
});
export default models;