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()
|
||||
|
||||
|
||||
@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
|
||||
class ServerMetadata(generic.View):
|
||||
"""API for server metadata.
|
||||
|
@ -138,6 +138,7 @@
|
||||
novaLimits: {},
|
||||
profiles: [],
|
||||
securityGroups: [],
|
||||
serverGroups: [],
|
||||
volumeBootable: false,
|
||||
volumes: [],
|
||||
volumeSnapshots: [],
|
||||
@ -172,8 +173,10 @@
|
||||
networks: [],
|
||||
ports: [],
|
||||
profile: {},
|
||||
scheduler_hints: {},
|
||||
// REQUIRED Server Key. May be empty.
|
||||
security_groups: [],
|
||||
server_groups: [],
|
||||
// REQUIRED for JS logic (image | snapshot | volume | volume_snapshot)
|
||||
source_type: null,
|
||||
source: [],
|
||||
@ -239,6 +242,7 @@
|
||||
// This provides supplemental data non-critical to launching
|
||||
// an instance. Therefore we load it only if the critical data
|
||||
// all loads successfully.
|
||||
getServerGroups();
|
||||
getMetadataDefinitions();
|
||||
}
|
||||
|
||||
@ -276,6 +280,7 @@
|
||||
setFinalSpecPorts(finalSpec);
|
||||
setFinalSpecKeyPairs(finalSpec);
|
||||
setFinalSpecSecurityGroups(finalSpec);
|
||||
setFinalSpecServerGroup(finalSpec);
|
||||
setFinalSpecSchedulerHints(finalSpec);
|
||||
setFinalSpecMetadata(finalSpec);
|
||||
|
||||
@ -389,6 +394,26 @@
|
||||
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
|
||||
|
||||
function getNetworks() {
|
||||
@ -624,9 +649,8 @@
|
||||
var hints = model.hintsTree.getExisting();
|
||||
if (!angular.equals({}, hints)) {
|
||||
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();
|
||||
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;
|
||||
}
|
||||
};
|
||||
@ -201,6 +209,13 @@
|
||||
|
||||
deferred.resolve();
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
check: function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
deferred.resolve();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
});
|
||||
@ -281,7 +296,8 @@
|
||||
it('has empty arrays for all data', function() {
|
||||
var datasets = ['availabilityZones', 'flavors', 'allowedBootSources',
|
||||
'images', 'imageSnapshots', 'keypairs', 'networks',
|
||||
'profiles', 'securityGroups', 'volumes', 'volumeSnapshots'];
|
||||
'profiles', 'securityGroups', 'serverGroups', 'volumes',
|
||||
'volumeSnapshots'];
|
||||
|
||||
datasets.forEach(function(name) {
|
||||
expect(model[name]).toEqual([]);
|
||||
@ -499,7 +515,7 @@
|
||||
// This is here to ensure that as people add/change items, they
|
||||
// don't forget to implement tests for them.
|
||||
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() {
|
||||
@ -554,6 +570,10 @@
|
||||
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() {
|
||||
expect(model.newInstanceSpec.source_type).toBeNull();
|
||||
});
|
||||
@ -584,10 +604,12 @@
|
||||
model.newInstanceSpec.key_pair = [ { name: 'keypair1' } ];
|
||||
model.newInstanceSpec.security_groups = [ { id: 'adminId', name: 'admin' },
|
||||
{ id: 'demoId', name: 'demo' } ];
|
||||
model.newInstanceSpec.scheduler_hints = {};
|
||||
model.newInstanceSpec.vol_create = true;
|
||||
model.newInstanceSpec.vol_delete_on_instance_delete = true;
|
||||
model.newInstanceSpec.vol_device_name = "volTestName";
|
||||
model.newInstanceSpec.vol_size = 10;
|
||||
model.newInstanceSpec.server_groups = [];
|
||||
|
||||
metadata = {'foo': 'bar'};
|
||||
model.metadataTree = {
|
||||
@ -596,7 +618,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
hints = {'group': 'group1'};
|
||||
hints = {'hint1': 'val1'};
|
||||
model.hintsTree = {
|
||||
getExisting: function() {
|
||||
return hints;
|
||||
@ -756,21 +778,34 @@
|
||||
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 = {};
|
||||
model.newInstanceSpec.server_groups = [{'id': 'group1'}];
|
||||
var finalHints = {'group': model.newInstanceSpec.server_groups[0].id};
|
||||
|
||||
var finalSpec = model.createInstance();
|
||||
expect(finalSpec.scheduler_hints).toBeUndefined();
|
||||
expect(finalSpec.scheduler_hints).toEqual(finalHints);
|
||||
|
||||
model.hintsTree = null;
|
||||
|
||||
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() {
|
||||
var finalHints = hints;
|
||||
finalHints.group = 'group1';
|
||||
|
||||
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',
|
||||
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',
|
||||
title: gettext('Scheduler Hints'),
|
||||
|
@ -40,9 +40,9 @@
|
||||
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.length).toBe(10);
|
||||
expect(launchInstanceWorkflow.steps.length).toBe(11);
|
||||
|
||||
var forms = [
|
||||
'launchInstanceDetailsForm',
|
||||
@ -53,6 +53,7 @@
|
||||
'launchInstanceAccessAndSecurityForm',
|
||||
'launchInstanceKeypairForm',
|
||||
'launchInstanceConfigurationForm',
|
||||
'launchInstanceServerGroupsForm',
|
||||
'launchInstanceSchedulerHintsForm',
|
||||
'launchInstanceMetadataForm'
|
||||
];
|
||||
@ -70,8 +71,12 @@
|
||||
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() {
|
||||
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', {
|
||||
// This policy determines if the scheduler hints extension is discoverable when listing
|
||||
// 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);
|
||||
|
@ -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,
|
||||
getServer: getServer,
|
||||
getServers: getServers,
|
||||
getServerGroups: getServerGroups,
|
||||
getExtensions: getExtensions,
|
||||
getFlavors: getFlavors,
|
||||
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
|
||||
* @param {Object} config - A configuration object
|
||||
|
@ -233,6 +233,23 @@ class NovaRestTestCase(test.TestCase):
|
||||
self.assertStatusCode(response, 200)
|
||||
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
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user