Merge "Choose a server group when booting a VM with NG launch instance"
This commit is contained in:
commit
7ca5b3c0ff
@ -268,6 +268,22 @@ class Server(generic.View):
|
|||||||
return api.nova.server_get(request, server_id).to_dict()
|
return api.nova.server_get(request, server_id).to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@urls.register
|
||||||
|
class ServerGroups(generic.View):
|
||||||
|
"""API for nova server groups.
|
||||||
|
"""
|
||||||
|
url_regex = r'nova/servergroups/$'
|
||||||
|
|
||||||
|
@rest_utils.ajax()
|
||||||
|
def get(self, request):
|
||||||
|
"""Get a list of server groups.
|
||||||
|
|
||||||
|
The listing result is an object with property "items".
|
||||||
|
"""
|
||||||
|
result = api.nova.server_group_list(request)
|
||||||
|
return {'items': [u.to_dict() for u in result]}
|
||||||
|
|
||||||
|
|
||||||
@urls.register
|
@urls.register
|
||||||
class ServerMetadata(generic.View):
|
class ServerMetadata(generic.View):
|
||||||
"""API for server metadata.
|
"""API for server metadata.
|
||||||
|
@ -138,6 +138,7 @@
|
|||||||
novaLimits: {},
|
novaLimits: {},
|
||||||
profiles: [],
|
profiles: [],
|
||||||
securityGroups: [],
|
securityGroups: [],
|
||||||
|
serverGroups: [],
|
||||||
volumeBootable: false,
|
volumeBootable: false,
|
||||||
volumes: [],
|
volumes: [],
|
||||||
volumeSnapshots: [],
|
volumeSnapshots: [],
|
||||||
@ -172,8 +173,10 @@
|
|||||||
networks: [],
|
networks: [],
|
||||||
ports: [],
|
ports: [],
|
||||||
profile: {},
|
profile: {},
|
||||||
|
scheduler_hints: {},
|
||||||
// REQUIRED Server Key. May be empty.
|
// REQUIRED Server Key. May be empty.
|
||||||
security_groups: [],
|
security_groups: [],
|
||||||
|
server_groups: [],
|
||||||
// REQUIRED for JS logic (image | snapshot | volume | volume_snapshot)
|
// REQUIRED for JS logic (image | snapshot | volume | volume_snapshot)
|
||||||
source_type: null,
|
source_type: null,
|
||||||
source: [],
|
source: [],
|
||||||
@ -239,6 +242,7 @@
|
|||||||
// This provides supplemental data non-critical to launching
|
// This provides supplemental data non-critical to launching
|
||||||
// an instance. Therefore we load it only if the critical data
|
// an instance. Therefore we load it only if the critical data
|
||||||
// all loads successfully.
|
// all loads successfully.
|
||||||
|
getServerGroups();
|
||||||
getMetadataDefinitions();
|
getMetadataDefinitions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,6 +280,7 @@
|
|||||||
setFinalSpecPorts(finalSpec);
|
setFinalSpecPorts(finalSpec);
|
||||||
setFinalSpecKeyPairs(finalSpec);
|
setFinalSpecKeyPairs(finalSpec);
|
||||||
setFinalSpecSecurityGroups(finalSpec);
|
setFinalSpecSecurityGroups(finalSpec);
|
||||||
|
setFinalSpecServerGroup(finalSpec);
|
||||||
setFinalSpecSchedulerHints(finalSpec);
|
setFinalSpecSchedulerHints(finalSpec);
|
||||||
setFinalSpecMetadata(finalSpec);
|
setFinalSpecMetadata(finalSpec);
|
||||||
|
|
||||||
@ -389,6 +394,26 @@
|
|||||||
finalSpec.security_groups = securityGroupIds;
|
finalSpec.security_groups = securityGroupIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server Groups
|
||||||
|
|
||||||
|
function getServerGroups() {
|
||||||
|
if (policy.check(stepPolicy.serverGroups)) {
|
||||||
|
return novaAPI.getServerGroups().then(onGetServerGroups, noop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGetServerGroups(data) {
|
||||||
|
model.serverGroups.length = 0;
|
||||||
|
push.apply(model.serverGroups, data.data.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFinalSpecServerGroup(finalSpec) {
|
||||||
|
if (finalSpec.server_groups.length > 0) {
|
||||||
|
finalSpec.scheduler_hints.group = finalSpec.server_groups[0].id;
|
||||||
|
}
|
||||||
|
delete finalSpec.server_groups;
|
||||||
|
}
|
||||||
|
|
||||||
// Networks
|
// Networks
|
||||||
|
|
||||||
function getNetworks() {
|
function getNetworks() {
|
||||||
@ -624,9 +649,8 @@
|
|||||||
var hints = model.hintsTree.getExisting();
|
var hints = model.hintsTree.getExisting();
|
||||||
if (!angular.equals({}, hints)) {
|
if (!angular.equals({}, hints)) {
|
||||||
angular.forEach(hints, function(value, key) {
|
angular.forEach(hints, function(value, key) {
|
||||||
hints[key] = value + '';
|
finalSpec.scheduler_hints[key] = value + '';
|
||||||
});
|
});
|
||||||
finalSpec.scheduler_hints = hints;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,14 @@
|
|||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: limits });
|
deferred.resolve({ data: limits });
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
},
|
||||||
|
getServerGroups: function() {
|
||||||
|
var serverGroups = [ {'id': 'group-1'}, {'id': 'group-2'} ];
|
||||||
|
|
||||||
|
var deferred = $q.defer();
|
||||||
|
deferred.resolve({ data: { items: serverGroups } });
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -201,6 +209,13 @@
|
|||||||
|
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
},
|
||||||
|
check: function() {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
deferred.resolve();
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -281,7 +296,8 @@
|
|||||||
it('has empty arrays for all data', function() {
|
it('has empty arrays for all data', function() {
|
||||||
var datasets = ['availabilityZones', 'flavors', 'allowedBootSources',
|
var datasets = ['availabilityZones', 'flavors', 'allowedBootSources',
|
||||||
'images', 'imageSnapshots', 'keypairs', 'networks',
|
'images', 'imageSnapshots', 'keypairs', 'networks',
|
||||||
'profiles', 'securityGroups', 'volumes', 'volumeSnapshots'];
|
'profiles', 'securityGroups', 'serverGroups', 'volumes',
|
||||||
|
'volumeSnapshots'];
|
||||||
|
|
||||||
datasets.forEach(function(name) {
|
datasets.forEach(function(name) {
|
||||||
expect(model[name]).toEqual([]);
|
expect(model[name]).toEqual([]);
|
||||||
@ -499,7 +515,7 @@
|
|||||||
// This is here to ensure that as people add/change items, they
|
// This is here to ensure that as people add/change items, they
|
||||||
// don't forget to implement tests for them.
|
// don't forget to implement tests for them.
|
||||||
it('has the right number of properties', function() {
|
it('has the right number of properties', function() {
|
||||||
expect(Object.keys(model.newInstanceSpec).length).toBe(19);
|
expect(Object.keys(model.newInstanceSpec).length).toBe(21);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets availability zone to null', function() {
|
it('sets availability zone to null', function() {
|
||||||
@ -554,6 +570,10 @@
|
|||||||
expect(model.newInstanceSpec.security_groups).toEqual([]);
|
expect(model.newInstanceSpec.security_groups).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sets scheduler hints to an empty object', function() {
|
||||||
|
expect(model.newInstanceSpec.scheduler_hints).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
it('sets source type to null', function() {
|
it('sets source type to null', function() {
|
||||||
expect(model.newInstanceSpec.source_type).toBeNull();
|
expect(model.newInstanceSpec.source_type).toBeNull();
|
||||||
});
|
});
|
||||||
@ -584,10 +604,12 @@
|
|||||||
model.newInstanceSpec.key_pair = [ { name: 'keypair1' } ];
|
model.newInstanceSpec.key_pair = [ { name: 'keypair1' } ];
|
||||||
model.newInstanceSpec.security_groups = [ { id: 'adminId', name: 'admin' },
|
model.newInstanceSpec.security_groups = [ { id: 'adminId', name: 'admin' },
|
||||||
{ id: 'demoId', name: 'demo' } ];
|
{ id: 'demoId', name: 'demo' } ];
|
||||||
|
model.newInstanceSpec.scheduler_hints = {};
|
||||||
model.newInstanceSpec.vol_create = true;
|
model.newInstanceSpec.vol_create = true;
|
||||||
model.newInstanceSpec.vol_delete_on_instance_delete = true;
|
model.newInstanceSpec.vol_delete_on_instance_delete = true;
|
||||||
model.newInstanceSpec.vol_device_name = "volTestName";
|
model.newInstanceSpec.vol_device_name = "volTestName";
|
||||||
model.newInstanceSpec.vol_size = 10;
|
model.newInstanceSpec.vol_size = 10;
|
||||||
|
model.newInstanceSpec.server_groups = [];
|
||||||
|
|
||||||
metadata = {'foo': 'bar'};
|
metadata = {'foo': 'bar'};
|
||||||
model.metadataTree = {
|
model.metadataTree = {
|
||||||
@ -596,7 +618,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
hints = {'group': 'group1'};
|
hints = {'hint1': 'val1'};
|
||||||
model.hintsTree = {
|
model.hintsTree = {
|
||||||
getExisting: function() {
|
getExisting: function() {
|
||||||
return hints;
|
return hints;
|
||||||
@ -756,21 +778,34 @@
|
|||||||
expect(finalSpec.meta).toBe(metadata);
|
expect(finalSpec.meta).toBe(metadata);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not have scheduler_hints property if no scheduler hints specified', function() {
|
it('should have only group for scheduler_hints if no other hints specified', function() {
|
||||||
hints = {};
|
hints = {};
|
||||||
|
model.newInstanceSpec.server_groups = [{'id': 'group1'}];
|
||||||
|
var finalHints = {'group': model.newInstanceSpec.server_groups[0].id};
|
||||||
|
|
||||||
var finalSpec = model.createInstance();
|
var finalSpec = model.createInstance();
|
||||||
expect(finalSpec.scheduler_hints).toBeUndefined();
|
expect(finalSpec.scheduler_hints).toEqual(finalHints);
|
||||||
|
|
||||||
model.hintsTree = null;
|
model.hintsTree = null;
|
||||||
|
|
||||||
finalSpec = model.createInstance();
|
finalSpec = model.createInstance();
|
||||||
expect(finalSpec.scheduler_hints).toBeUndefined();
|
expect(finalSpec.scheduler_hints).toEqual(finalHints);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have scheduler_hints property if scheduler hints specified', function() {
|
it('should have scheduler_hints property if scheduler hints specified', function() {
|
||||||
|
var finalHints = hints;
|
||||||
|
finalHints.group = 'group1';
|
||||||
|
|
||||||
var finalSpec = model.createInstance();
|
var finalSpec = model.createInstance();
|
||||||
expect(finalSpec.scheduler_hints).toBe(hints);
|
expect(finalSpec.scheduler_hints).toEqual(finalHints);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have no scheduler_hints if no scheduler hints specified', function() {
|
||||||
|
hints = {};
|
||||||
|
model.newInstanceSpec.server_groups = [];
|
||||||
|
|
||||||
|
var finalSpec = model.createInstance();
|
||||||
|
expect(finalSpec.scheduler_hints).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -89,6 +89,14 @@
|
|||||||
helpUrl: basePath + 'configuration/configuration.help.html',
|
helpUrl: basePath + 'configuration/configuration.help.html',
|
||||||
formName: 'launchInstanceConfigurationForm'
|
formName: 'launchInstanceConfigurationForm'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'servergroups',
|
||||||
|
title: gettext('Server Groups'),
|
||||||
|
templateUrl: basePath + 'server-groups/server-groups.html',
|
||||||
|
helpUrl: basePath + 'server-groups/server-groups.help.html',
|
||||||
|
formName: 'launchInstanceServerGroupsForm',
|
||||||
|
policy: stepPolicy.serverGroups
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'hints',
|
id: 'hints',
|
||||||
title: gettext('Scheduler Hints'),
|
title: gettext('Scheduler Hints'),
|
||||||
|
@ -40,9 +40,9 @@
|
|||||||
expect(launchInstanceWorkflow.title).toBeDefined();
|
expect(launchInstanceWorkflow.title).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have 10 steps defined', function () {
|
it('should have 11 steps defined', function () {
|
||||||
expect(launchInstanceWorkflow.steps).toBeDefined();
|
expect(launchInstanceWorkflow.steps).toBeDefined();
|
||||||
expect(launchInstanceWorkflow.steps.length).toBe(10);
|
expect(launchInstanceWorkflow.steps.length).toBe(11);
|
||||||
|
|
||||||
var forms = [
|
var forms = [
|
||||||
'launchInstanceDetailsForm',
|
'launchInstanceDetailsForm',
|
||||||
@ -53,6 +53,7 @@
|
|||||||
'launchInstanceAccessAndSecurityForm',
|
'launchInstanceAccessAndSecurityForm',
|
||||||
'launchInstanceKeypairForm',
|
'launchInstanceKeypairForm',
|
||||||
'launchInstanceConfigurationForm',
|
'launchInstanceConfigurationForm',
|
||||||
|
'launchInstanceServerGroupsForm',
|
||||||
'launchInstanceSchedulerHintsForm',
|
'launchInstanceSchedulerHintsForm',
|
||||||
'launchInstanceMetadataForm'
|
'launchInstanceMetadataForm'
|
||||||
];
|
];
|
||||||
@ -70,8 +71,12 @@
|
|||||||
expect(launchInstanceWorkflow.steps[4].requiredServiceTypes).toEqual(['network']);
|
expect(launchInstanceWorkflow.steps[4].requiredServiceTypes).toEqual(['network']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has a policy rule for the server groups step', function() {
|
||||||
|
expect(launchInstanceWorkflow.steps[8].policy).toEqual(stepPolicy.serverGroups);
|
||||||
|
});
|
||||||
|
|
||||||
it('has a policy rule for the scheduler hints step', function() {
|
it('has a policy rule for the scheduler hints step', function() {
|
||||||
expect(launchInstanceWorkflow.steps[8].policy).toEqual(stepPolicy.schedulerHints);
|
expect(launchInstanceWorkflow.steps[9].policy).toEqual(stepPolicy.schedulerHints);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -46,7 +46,9 @@
|
|||||||
.constant('horizon.dashboard.project.workflow.launch-instance.step-policy', {
|
.constant('horizon.dashboard.project.workflow.launch-instance.step-policy', {
|
||||||
// This policy determines if the scheduler hints extension is discoverable when listing
|
// This policy determines if the scheduler hints extension is discoverable when listing
|
||||||
// available extensions. It's possible the extension is installed but not discoverable.
|
// available extensions. It's possible the extension is installed but not discoverable.
|
||||||
schedulerHints: { rules: [['compute', 'os_compute_api:os-scheduler-hints:discoverable']] }
|
schedulerHints: { rules: [['compute', 'os_compute_api:os-scheduler-hints:discoverable']] },
|
||||||
|
// Determine if the server groups extension is discoverable.
|
||||||
|
serverGroups: { rules: [['compute', 'os_compute_api:os-server-groups:discoverable']] }
|
||||||
})
|
})
|
||||||
|
|
||||||
.filter('diskFormat', diskFormat);
|
.filter('diskFormat', diskFormat);
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
<table st-table="row.policies"
|
||||||
|
class="table table-condensed table-rsp server-group-details">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th st-sort="policy" st-sort-default translate>Policy</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="policy in row.policies">
|
||||||
|
<td>{$ policy | noValue $}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Symantec Corp.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular
|
||||||
|
.module('horizon.dashboard.project.workflow.launch-instance')
|
||||||
|
.controller('LaunchInstanceServerGroupsController', LaunchInstanceServerGroupsController);
|
||||||
|
|
||||||
|
LaunchInstanceServerGroupsController.$inject = [
|
||||||
|
'launchInstanceModel',
|
||||||
|
'horizon.dashboard.project.workflow.launch-instance.basePath'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc controller
|
||||||
|
* @name LaunchInstanceServerGroupsController
|
||||||
|
* @param {Object} launchInstanceModel
|
||||||
|
* @param {string} basePath
|
||||||
|
* @description
|
||||||
|
* Allows selection of server groups.
|
||||||
|
* @returns {undefined} No return value
|
||||||
|
*/
|
||||||
|
function LaunchInstanceServerGroupsController(launchInstanceModel, basePath) {
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
ctrl.tableData = {
|
||||||
|
available: launchInstanceModel.serverGroups,
|
||||||
|
allocated: launchInstanceModel.newInstanceSpec.server_groups,
|
||||||
|
displayedAvailable: [],
|
||||||
|
displayedAllocated: []
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.tableDetails = basePath + 'server-groups/server-group-details.html';
|
||||||
|
|
||||||
|
ctrl.tableHelp = {
|
||||||
|
/*eslint-disable max-len */
|
||||||
|
noneAllocText: gettext('Select a server group from the available groups below.'),
|
||||||
|
/*eslint-enable max-len */
|
||||||
|
availHelpText: gettext('Select one')
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.tableLimits = {
|
||||||
|
maxAllocation: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.filterFacets = [
|
||||||
|
{
|
||||||
|
label: gettext('Name'),
|
||||||
|
name: 'name',
|
||||||
|
singleton: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,11 @@
|
|||||||
|
<div>
|
||||||
|
<p translate>
|
||||||
|
Server groups define collections of VM's so that the entire
|
||||||
|
collection can be given specific properties. For example, the policy of a
|
||||||
|
server group may specify that VM's in this group should not be placed on
|
||||||
|
the same physical hardware due to availability requirements.
|
||||||
|
</p>
|
||||||
|
<p translate>
|
||||||
|
Server groups are project-specific and cannot be shared across projects.
|
||||||
|
</p>
|
||||||
|
</div>
|
@ -0,0 +1,73 @@
|
|||||||
|
<div ng-controller="LaunchInstanceServerGroupsController as ctrl">
|
||||||
|
<p class="step-description" translate>
|
||||||
|
Select the server group to launch the instance in.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<transfer-table tr-model="ctrl.tableData"
|
||||||
|
help-text="ctrl.tableHelp"
|
||||||
|
limits="ctrl.tableLimits"
|
||||||
|
clone-content>
|
||||||
|
|
||||||
|
<table st-table="$displayedItems"
|
||||||
|
st-safe-src="$sourceItems"
|
||||||
|
hz-table class="table table-striped table-rsp table-detail">
|
||||||
|
<thead>
|
||||||
|
<tr ng-show="$isAvailableTable">
|
||||||
|
<th class="search-header" colspan="9">
|
||||||
|
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
|
||||||
|
</hz-search-bar>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="expander"></th>
|
||||||
|
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-if="$isAllocatedTable && ctrl.tableData.allocated.length === 0">
|
||||||
|
<td colspan="8">
|
||||||
|
<div class="no-rows-help">
|
||||||
|
{$ ::trCtrl.helpText.noneAllocText $}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="$isAvailableTable && trCtrl.numAvailable() === 0">
|
||||||
|
<td colspan="8">
|
||||||
|
<div class="no-rows-help">
|
||||||
|
{$ ::trCtrl.helpText.noneAvailText $}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-repeat-start="row in $displayedItems track by row.id"
|
||||||
|
ng-if="$isAllocatedTable || ($isAvailableTable && !trCtrl.allocatedIds[row.id])">
|
||||||
|
<td class="expander">
|
||||||
|
<span class="fa fa-chevron-right" hz-expand-detail
|
||||||
|
title="{$ ::trCtrl.helpText.expandDetailsText $}"></span>
|
||||||
|
</td>
|
||||||
|
<td class="rsp-p1">{$ row.name $}</td>
|
||||||
|
<td class="actions_column">
|
||||||
|
<action-list>
|
||||||
|
<action ng-if="$isAllocatedTable"
|
||||||
|
action-classes="'btn btn-default'"
|
||||||
|
callback="trCtrl.deallocate" item="row">
|
||||||
|
<span class="fa fa-minus"></span>
|
||||||
|
</action>
|
||||||
|
<action ng-if="$isAvailableTable"
|
||||||
|
action-classes="'btn btn-default'"
|
||||||
|
callback="trCtrl.allocate" item="row">
|
||||||
|
<span class="fa fa-plus"></span>
|
||||||
|
</action>
|
||||||
|
</action-list>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-repeat-end class="detail-row">
|
||||||
|
<td></td>
|
||||||
|
<td class="detail" colspan="3" ng-include="ctrl.tableDetails">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</transfer-table> <!-- End Server Groups Transfer Table -->
|
||||||
|
|
||||||
|
</div> <!-- End Controller -->
|
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 Symantec Corp.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('Launch Instance Server Groups Step', function() {
|
||||||
|
|
||||||
|
describe('LaunchInstanceServerGroupsController', function() {
|
||||||
|
var ctrl;
|
||||||
|
|
||||||
|
beforeEach(module('horizon.dashboard.project'));
|
||||||
|
|
||||||
|
beforeEach(inject(function($controller) {
|
||||||
|
var model = {
|
||||||
|
newInstanceSpec: {
|
||||||
|
server_groups: [ 'server group 1' ]
|
||||||
|
},
|
||||||
|
serverGroups: [ 'server group 1', 'server group 2' ]
|
||||||
|
};
|
||||||
|
ctrl = $controller(
|
||||||
|
'LaunchInstanceServerGroupsController',
|
||||||
|
{
|
||||||
|
launchInstanceModel: model,
|
||||||
|
'horizon.dashboard.project.workflow.launch-instance.basePath': ''
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('contains its table labels', function() {
|
||||||
|
expect(ctrl.tableData).toBeDefined();
|
||||||
|
expect(Object.keys(ctrl.tableData).length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets table data to appropriate scoped items', function() {
|
||||||
|
expect(ctrl.tableData).toBeDefined();
|
||||||
|
expect(Object.keys(ctrl.tableData).length).toBe(4);
|
||||||
|
expect(ctrl.tableData.available).toEqual([ 'server group 1', 'server group 2' ]);
|
||||||
|
expect(ctrl.tableData.allocated).toEqual([ 'server group 1' ]);
|
||||||
|
expect(ctrl.tableData.displayedAvailable).toEqual([]);
|
||||||
|
expect(ctrl.tableData.displayedAllocated).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defines table details template', function() {
|
||||||
|
expect(ctrl.tableDetails).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defines table help', function() {
|
||||||
|
expect(ctrl.tableHelp).toBeDefined();
|
||||||
|
expect(Object.keys(ctrl.tableHelp).length).toBe(2);
|
||||||
|
expect(ctrl.tableHelp.noneAllocText).toBeDefined();
|
||||||
|
expect(ctrl.tableHelp.availHelpText).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows only one allocation', function() {
|
||||||
|
expect(ctrl.tableLimits).toBeDefined();
|
||||||
|
expect(Object.keys(ctrl.tableLimits).length).toBe(1);
|
||||||
|
expect(ctrl.tableLimits.maxAllocation).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
})();
|
@ -46,6 +46,7 @@
|
|||||||
createServer: createServer,
|
createServer: createServer,
|
||||||
getServer: getServer,
|
getServer: getServer,
|
||||||
getServers: getServers,
|
getServers: getServers,
|
||||||
|
getServerGroups: getServerGroups,
|
||||||
getExtensions: getExtensions,
|
getExtensions: getExtensions,
|
||||||
getFlavors: getFlavors,
|
getFlavors: getFlavors,
|
||||||
getFlavor: getFlavor,
|
getFlavor: getFlavor,
|
||||||
@ -246,6 +247,22 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name getServerGroups
|
||||||
|
* @description
|
||||||
|
* Get a list of server groups.
|
||||||
|
*
|
||||||
|
* The listing result is an object with property "items". Each item is
|
||||||
|
* a server group.
|
||||||
|
* @returns {Object} The result of the API call
|
||||||
|
*/
|
||||||
|
function getServerGroups() {
|
||||||
|
return apiService.get('/api/nova/servergroups/')
|
||||||
|
.error(function () {
|
||||||
|
toastService.add('error', gettext('Unable to retrieve server groups.'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name getExtensions
|
* @name getExtensions
|
||||||
* @param {Object} config - A configuration object
|
* @param {Object} config - A configuration object
|
||||||
|
@ -233,6 +233,23 @@ class NovaRestTestCase(test.TestCase):
|
|||||||
self.assertStatusCode(response, 200)
|
self.assertStatusCode(response, 200)
|
||||||
nc.server_get.assert_called_once_with(request, "1")
|
nc.server_get.assert_called_once_with(request, "1")
|
||||||
|
|
||||||
|
#
|
||||||
|
# Server Groups
|
||||||
|
#
|
||||||
|
@mock.patch.object(nova.api, 'nova')
|
||||||
|
def test_server_group_list(self, nc):
|
||||||
|
request = self.mock_rest_request()
|
||||||
|
nc.server_group_list.return_value = [
|
||||||
|
mock.Mock(**{'to_dict.return_value': {'id': '1'}}),
|
||||||
|
mock.Mock(**{'to_dict.return_value': {'id': '2'}}),
|
||||||
|
]
|
||||||
|
|
||||||
|
response = nova.ServerGroups().get(request)
|
||||||
|
self.assertStatusCode(response, 200)
|
||||||
|
self.assertEqual(response.json,
|
||||||
|
{'items': [{'id': '1'}, {'id': '2'}]})
|
||||||
|
nc.server_group_list.assert_called_once_with(request)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Server Metadata
|
# Server Metadata
|
||||||
#
|
#
|
||||||
|
Loading…
Reference in New Issue
Block a user