Add angular server group details page

This patch adds angular server group details page to show details of
the given server group, also shows the server group members under the
current server group.

Change-Id: I5b903972dd4fc5c9f1b52f97bdd7e0852d7d00d3
Partial-Implements: blueprint ng-server-groups
This commit is contained in:
wei.ying 2017-11-05 22:50:41 +08:00
parent 37647dd318
commit 940ff111a1
16 changed files with 483 additions and 12 deletions

View File

@ -31,7 +31,8 @@ MICROVERSION_FEATURES = {
"locked_attribute": ["2.9", "2.42"], "locked_attribute": ["2.9", "2.42"],
"instance_description": ["2.19", "2.42"], "instance_description": ["2.19", "2.42"],
"remote_console_mks": ["2.8", "2.53"], "remote_console_mks": ["2.8", "2.53"],
"servergroup_soft_policies": ["2.15", "2.60"] "servergroup_soft_policies": ["2.15", "2.60"],
"servergroup_user_info": ["2.13", "2.60"]
}, },
"cinder": { "cinder": {
"consistency_groups": ["2.0", "3.10"], "consistency_groups": ["2.0", "3.10"],

View File

@ -943,6 +943,13 @@ def server_group_delete(request, servergroup_id):
novaclient(request).server_groups.delete(servergroup_id) novaclient(request).server_groups.delete(servergroup_id)
@profiler.trace
def server_group_get(request, servergroup_id):
microversion = get_microversion(request, "servergroup_user_info")
return novaclient(request, version=microversion).server_groups.get(
servergroup_id)
@profiler.trace @profiler.trace
def service_list(request, binary=None): def service_list(request, binary=None):
return novaclient(request).services.list(binary=binary) return novaclient(request).services.list(binary=binary)

View File

@ -460,6 +460,14 @@ class ServerGroup(generic.View):
""" """
api.nova.server_group_delete(request, servergroup_id) api.nova.server_group_delete(request, servergroup_id)
@rest_utils.ajax()
def get(self, request, servergroup_id):
"""Get a specific server group
http://localhost/api/nova/servergroups/1
"""
return api.nova.server_group_get(request, servergroup_id).to_dict()
@urls.register @urls.register
class ServerMetadata(generic.View): class ServerMetadata(generic.View):

View File

@ -52,6 +52,7 @@
createServer: createServer, createServer: createServer,
getServer: getServer, getServer: getServer,
getServers: getServers, getServers: getServers,
getServerGroup: getServerGroup,
getServerGroups: getServerGroups, getServerGroups: getServerGroups,
createServerGroup: createServerGroup, createServerGroup: createServerGroup,
deleteServerGroup: deleteServerGroup, deleteServerGroup: deleteServerGroup,
@ -318,6 +319,21 @@
}); });
} }
/**
* @name getServerGroup
* @description
* Get a single server group by ID
* @param {string} id
* Specifies the id of the server group to request.
* @returns {Object} The result of the API call
*/
function getServerGroup(id) {
return apiService.get('/api/nova/servergroups/' + id)
.error(function () {
toastService.add('error', gettext('Unable to retrieve the server group.'));
});
}
/** /**
* @name getServerGroups * @name getServerGroups
* @description * @description

View File

@ -296,6 +296,15 @@
"path": "/api/nova/servers/", "path": "/api/nova/servers/",
"error": "Unable to retrieve instances." "error": "Unable to retrieve instances."
}, },
{
"func": "getServerGroup",
"method": "get",
"path": "/api/nova/servergroups/17",
"error": "Unable to retrieve the server group.",
"testInput": [
'17'
]
},
{ {
"func": 'getServerGroups', "func": 'getServerGroups',
"method": 'get', "method": 'get',

View File

@ -0,0 +1,54 @@
/*
* 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';
/**
* @ngdoc overview
* @ngname horizon.app.core.server_groups.details
*
* @description
* Provides details features for server groups.
*/
angular
.module('horizon.app.core.server_groups.details', [
'horizon.framework.conf',
'horizon.app.core'
])
.run(registerServerGroupDetails);
registerServerGroupDetails.$inject = [
'horizon.app.core.server_groups.basePath',
'horizon.app.core.server_groups.resourceType',
'horizon.app.core.server_groups.service',
'horizon.framework.conf.resource-type-registry.service'
];
function registerServerGroupDetails(
basePath,
serverGroupResourceType,
serverGroupService,
registry
) {
registry.getResourceType(serverGroupResourceType)
.setLoadFunction(serverGroupService.getServerGroupPromise)
.detailsViews.append({
id: 'serverGroupDetailsOverview',
name: gettext('Overview'),
template: basePath + 'details/overview.html'
});
}
})();

View File

@ -0,0 +1,35 @@
/*
* 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('server group details module', function() {
it('should exist', function() {
expect(angular.module('horizon.app.core.server_groups.details')).toBeDefined();
});
var registry, resource;
beforeEach(module('horizon.framework'));
beforeEach(module('horizon.app.core.server_groups'));
beforeEach(inject(function($injector) {
registry = $injector.get('horizon.framework.conf.resource-type-registry.service');
}));
it('should be loaded', function() {
resource = registry.getResourceType('OS::Nova::ServerGroup');
expect(resource.detailsViews[0].id).toBe('serverGroupDetailsOverview');
});
});
})();

View File

@ -0,0 +1,107 @@
/*
* 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.app.core.server_groups')
.controller('ServerGroupOverviewController', ServerGroupOverviewController);
ServerGroupOverviewController.$inject = [
'$scope',
'horizon.app.core.openstack-service-api.nova',
'horizon.app.core.openstack-service-api.userSession',
'horizon.app.core.server_groups.resourceType',
'horizon.app.core.server_groups.service',
'horizon.framework.conf.resource-type-registry.service'
];
function ServerGroupOverviewController(
$scope,
nova,
userSession,
serverGroupResourceTypeCode,
serverGroupsService,
registry
) {
var ctrl = this;
nova.isFeatureSupported(
'servergroup_user_info').then(onGetServerGroupProperties);
function onGetServerGroupProperties(response) {
var properties = ['id', 'name', 'policy'];
if (response.data) {
properties.splice(2, 0, 'project_id', 'user_id');
}
ctrl.properties = properties;
}
ctrl.resourceType = registry.getResourceType(serverGroupResourceTypeCode);
ctrl.tableConfig = {
selectAll: false,
expand: false,
trackId: 'id',
columns: [
{
id: 'name',
title: gettext('Instance Name'),
priority: 1,
sortDefault: true,
urlFunction: serverGroupsService.getInstanceDetailsPath
},
{
id: 'id',
title: gettext('Instance ID'),
priority: 1
}
]
};
$scope.context.loadPromise.then(onGetServerGroup);
function onGetServerGroup(servergroup) {
ctrl.servergroup = servergroup.data;
if (ctrl.servergroup.members.length) {
// The server group members only contains the instance id,
// does not contain other information of the instance, we
// need to get the list of instances and then get the specified
// instance from the list based on id.
nova.getServers().then(function getServer(servers) {
var members = [];
ctrl.servergroup.members.forEach(function(member) {
for (var i = 0; i < servers.data.items.length; i++) {
var server = servers.data.items[i];
if (member === server.id) {
members.push(server);
break;
}
}
});
ctrl.members = members;
});
} else {
ctrl.members = [];
}
userSession.get().then(setProject);
function setProject(session) {
ctrl.projectId = session.project_id;
}
}
}
})();

View File

@ -0,0 +1,80 @@
/*
* 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('server group overview controller', function() {
var $controller, $q, $timeout, nova, session;
var sessionObj = {project_id: '10'};
beforeEach(module('horizon.app.core.server_groups'));
beforeEach(module('horizon.framework.conf'));
beforeEach(inject(function($injector) {
$controller = $injector.get('$controller');
$q = $injector.get('$q');
$timeout = $injector.get('$timeout');
session = $injector.get('horizon.app.core.openstack-service-api.userSession');
nova = $injector.get('horizon.app.core.openstack-service-api.nova');
}));
it('sets ctrl.members when server group members length === 0', inject(function() {
var deferred = $q.defer();
var serverGroupDeferred = $q.defer();
var serversDeferred = $q.defer();
var sessionDeferred = $q.defer();
deferred.resolve({data: { data: true}});
serverGroupDeferred.resolve({data: {members: ['1', '2']}});
serversDeferred.resolve({data: { items: [{'id': '1'}, {'id': '2'}]}});
sessionDeferred.resolve(sessionObj);
spyOn(nova, 'isFeatureSupported').and.returnValue(deferred.promise);
spyOn(nova, 'getServerGroup').and.returnValue(serverGroupDeferred.promise);
spyOn(nova, 'getServers').and.returnValue(serversDeferred.promise);
spyOn(session, 'get').and.returnValue(sessionDeferred.promise);
var ctrl = $controller('ServerGroupOverviewController',
{
'$scope': {context: {loadPromise: serverGroupDeferred.promise}}
}
);
$timeout.flush();
expect(ctrl.properties).toBeDefined();
expect(ctrl.members[0]).toEqual({'id': '1'});
expect(ctrl.projectId).toBe(sessionObj.project_id);
}));
it('sets ctrl.members when server group members length > 0', inject(function() {
var deferred = $q.defer();
var serverGroupDeferred = $q.defer();
var serversDeferred = $q.defer();
var sessionDeferred = $q.defer();
deferred.resolve({data: { data: true}});
serverGroupDeferred.resolve({data: {members: []}});
serversDeferred.resolve({data: { items: [{'id': '1'}, {'id': '2'}]}});
sessionDeferred.resolve(sessionObj);
spyOn(nova, 'isFeatureSupported').and.returnValue(deferred.promise);
spyOn(nova, 'getServerGroup').and.returnValue(serverGroupDeferred.promise);
spyOn(nova, 'getServers').and.returnValue(serversDeferred.promise);
spyOn(session, 'get').and.returnValue(sessionDeferred.promise);
var ctrl = $controller('ServerGroupOverviewController',
{
'$scope': {context: {loadPromise: serverGroupDeferred.promise}}
}
);
$timeout.flush();
expect(ctrl.properties).toBeDefined();
expect(ctrl.members).toEqual([]);
expect(ctrl.projectId).toBe(sessionObj.project_id);
}));
});
})();

View File

@ -0,0 +1,17 @@
<div ng-controller="ServerGroupOverviewController as ctrl">
<hz-resource-property-list
resource-type-name="OS::Nova::ServerGroup"
cls="dl-horizontal"
item="ctrl.servergroup"
property-groups="[ctrl.properties]">
</hz-resource-property-list>
<h2 class="h4">{$ ::'Server Group Members' | translate $}</h2>
<hr>
<dl class="dl-horizontal">
<hz-dynamic-table
config="ctrl.tableConfig"
items="ctrl.members"
table="ctrl">
</hz-dynamic-table>
</dl>
</div>

View File

@ -27,7 +27,8 @@
.module('horizon.app.core.server_groups', [ .module('horizon.app.core.server_groups', [
'horizon.framework.conf', 'horizon.framework.conf',
'horizon.app.core', 'horizon.app.core',
'horizon.app.core.server_groups.actions' 'horizon.app.core.server_groups.actions',
'horizon.app.core.server_groups.details'
]) ])
.constant('horizon.app.core.server_groups.resourceType', 'OS::Nova::ServerGroup') .constant('horizon.app.core.server_groups.resourceType', 'OS::Nova::ServerGroup')
.run(run) .run(run)
@ -50,7 +51,8 @@
.append({ .append({
id: 'name', id: 'name',
priority: 1, priority: 1,
sortDefault: true sortDefault: true,
urlFunction: serverGroupsService.getDetailsPath
}) })
// The name is not unique, so we need to show the ID to // The name is not unique, so we need to show the ID to
// distinguish. // distinguish.
@ -89,28 +91,43 @@
return { return {
name: gettext('Name'), name: gettext('Name'),
id: gettext('ID'), id: gettext('ID'),
policy: gettext('Policy') policy: gettext('Policy'),
project_id: gettext('Project ID'),
user_id: gettext('User ID')
}; };
} }
config.$inject = [ config.$inject = [
'$provide',
'$windowProvider', '$windowProvider',
'$routeProvider' '$routeProvider',
'horizon.app.core.detailRoute'
]; ];
/** /**
* @name config * @name config
* @param {Object} $provide
* @param {Object} $windowProvider * @param {Object} $windowProvider
* @param {Object} $routeProvider * @param {Object} $routeProvider
* @param {String} detailRoute
* @description Routes used by this module. * @description Routes used by this module.
* @returns {undefined} Returns nothing * @returns {undefined} Returns nothing
*/ */
function config($windowProvider, $routeProvider) { function config($provide, $windowProvider, $routeProvider, detailRoute) {
var path = $windowProvider.$get().STATIC_URL + 'app/core/server_groups/'; var path = $windowProvider.$get().STATIC_URL + 'app/core/server_groups/';
$provide.constant('horizon.app.core.server_groups.basePath', path);
$routeProvider.when('/project/server_groups', { $routeProvider.when('/project/server_groups', {
templateUrl: path + 'panel.html' templateUrl: path + 'panel.html'
}); });
$routeProvider.when('/project/server_groups/:id', {
redirectTo: goToAngularDetails
});
function goToAngularDetails(params) {
return detailRoute + 'OS::Nova::ServerGroup/' + params.id;
}
} }
})(); })();

View File

@ -19,25 +19,56 @@
.factory('horizon.app.core.server_groups.service', serverGroupsService); .factory('horizon.app.core.server_groups.service', serverGroupsService);
serverGroupsService.$inject = [ serverGroupsService.$inject = [
'$window',
'horizon.app.core.detailRoute',
'horizon.app.core.openstack-service-api.nova' 'horizon.app.core.openstack-service-api.nova'
]; ];
/* /*
* @ngdoc factory * @ngdoc factory
* @name horizon.app.core.server_groups.service * @name horizon.app.core.server_groups.service
*
* @description * @description
* This service provides functions that are used through the Server Groups * This service provides functions that are used through the Server Groups
* features. These are primarily used in the module registrations * features. These are primarily used in the module registrations
* but do not need to be restricted to such use. Each exposed function * but do not need to be restricted to such use. Each exposed function
* is documented below. * is documented below.
*/ */
function serverGroupsService(nova) { function serverGroupsService($window, detailRoute, nova) {
return { return {
getServerGroupsPromise: getServerGroupsPromise, getDetailsPath: getDetailsPath,
getServerGroupPolicies: getServerGroupPolicies getInstanceDetailsPath: getInstanceDetailsPath,
getServerGroupPolicies: getServerGroupPolicies,
getServerGroupPromise: getServerGroupPromise,
getServerGroupsPromise: getServerGroupsPromise
}; };
/*
* @ngdoc function
* @name getDetailsPath
* @param item {Object} - The server group object
* @description
* Given a server group object, returns the relative path to the details
* view.
*/
function getDetailsPath(item) {
return detailRoute + 'OS::Nova::ServerGroup/' + item.id;
}
/*
* @ngdoc function
* @name getInstanceDetailsPath
* @param item {Object} - The instance object
* @description
* Given an instance object, returns the relative path to the details
* view.
*/
function getInstanceDetailsPath(item) {
// The current instances page only contains the django
// version, if we add angular instances pages in the future,
// the url here also needs to be modified accordingly.
return $window.WEBROOT + 'project/instances/' + item.id + '/';
}
/* /*
* @ngdoc function * @ngdoc function
* @name getServerGroupPolicies * @name getServerGroupPolicies
@ -61,6 +92,25 @@
} }
} }
/*
* @ngdoc function
* @name getServerGroupPromise
* @description
* Given an id, returns a promise for the server group data.
*/
function getServerGroupPromise(identifier) {
return nova.getServerGroup(identifier).then(modifyResponse);
function modifyResponse(response) {
return getServerGroupPolicies().then(modifyItem);
function modifyItem(policies) {
response.data.policy = policies[response.data.policies[0]];
return {data: response.data};
}
}
}
/* /*
* @ngdoc function * @ngdoc function
* @name getServerGroupsPromise * @name getServerGroupsPromise

View File

@ -16,16 +16,61 @@
"use strict"; "use strict";
describe('server groups service', function() { describe('server groups service', function() {
var $q, $timeout, nova, service; var $q, $timeout, $window, detailRoute, nova, service;
beforeEach(module('horizon.app.core.server_groups')); beforeEach(module('horizon.app.core.server_groups'));
beforeEach(inject(function($injector) { beforeEach(inject(function($injector) {
$q = $injector.get('$q'); $q = $injector.get('$q');
$timeout = $injector.get('$timeout'); $timeout = $injector.get('$timeout');
$window = $injector.get('$window');
detailRoute = $injector.get('horizon.app.core.detailRoute');
nova = $injector.get('horizon.app.core.openstack-service-api.nova'); nova = $injector.get('horizon.app.core.openstack-service-api.nova');
service = $injector.get('horizon.app.core.server_groups.service'); service = $injector.get('horizon.app.core.server_groups.service');
})); }));
it("getDetailsPath creates urls using the item's ID", function() {
var item = {id: '11'};
expect(service.getDetailsPath(item)).toBe(detailRoute + 'OS::Nova::ServerGroup/11');
});
it("getInstanceDetailsPath creates urls using the item's ID", function() {
$window.WEBROOT = '/dashboard/';
var item = {id: "12"};
expect(service.getInstanceDetailsPath(item)).toBe('/dashboard/project/instances/12/');
});
describe('getServerGroupPromise', function() {
it("provides a promise when soft policies are supported", inject(function() {
var deferred = $q.defer();
var deferredPolicies = $q.defer();
spyOn(nova, 'getServerGroup').and.returnValue(deferred.promise);
spyOn(nova, 'isFeatureSupported').and.returnValue(deferredPolicies.promise);
var result = service.getServerGroupPromise({});
deferred.resolve({data: {id: '13', policies: ['affinity']}});
deferredPolicies.resolve({data: true});
$timeout.flush();
expect(nova.getServerGroup).toHaveBeenCalled();
expect(nova.isFeatureSupported).toHaveBeenCalled();
expect(result.$$state.value.data.id).toBe('13');
}));
it("provides a promise when soft policies are not supported", inject(function() {
var deferred = $q.defer();
var deferredPolicies = $q.defer();
spyOn(nova, 'getServerGroup').and.returnValue(deferred.promise);
spyOn(nova, 'isFeatureSupported').and.returnValue(deferredPolicies.promise);
var result = service.getServerGroupPromise({});
deferred.resolve({data: {id: '15', policies: ['affinity']}});
deferredPolicies.resolve({data: false});
$timeout.flush();
expect(nova.getServerGroup).toHaveBeenCalled();
expect(nova.isFeatureSupported).toHaveBeenCalled();
expect(result.$$state.value.data.id).toBe('15');
}));
});
describe('getServerGroupsPromise', function() { describe('getServerGroupsPromise', function() {
it("provides a promise when soft policies are supported", inject(function() { it("provides a promise when soft policies are supported", inject(function() {
var deferred = $q.defer(); var deferred = $q.defer();

View File

@ -395,6 +395,18 @@ class NovaRestTestCase(test.TestCase):
nova.ServerGroup().delete(request, "1") nova.ServerGroup().delete(request, "1")
self.mock_server_group_delete.assert_called_once_with(request, "1") self.mock_server_group_delete.assert_called_once_with(request, "1")
@test.create_mocks({api.nova: ['server_group_get']})
def test_server_group_get_single(self):
request = self.mock_rest_request()
servergroup = self.server_groups.first()
self.mock_server_group_get.return_value = servergroup
response = nova.ServerGroup().get(request, "1")
self.assertStatusCode(response, 200)
self.assertEqual(servergroup.to_dict(), response.json)
self.mock_server_group_get.assert_called_once_with(request, "1")
# #
# Server Metadata # Server Metadata
# #

View File

@ -667,3 +667,15 @@ class ComputeApiTests(test.APIMockTestCase):
self.assertIsNone(api_val) self.assertIsNone(api_val)
novaclient.server_groups.delete.assert_called_once_with(servergroup_id) novaclient.server_groups.delete.assert_called_once_with(servergroup_id)
def test_server_group_get(self):
servergroup = self.server_groups.first()
novaclient = self.stub_novaclient()
self._mock_current_version(novaclient, '2.45')
novaclient.server_groups.get.return_value = servergroup
ret_val = api.nova.server_group_get(self.request, servergroup.id)
self.assertEqual(ret_val.id, servergroup.id)
novaclient.versions.get_current.assert_called_once_with()
novaclient.server_groups.get.assert_called_once_with(servergroup.id)

View File

@ -5,5 +5,6 @@ features:
This blueprint add angular server groups panel below the This blueprint add angular server groups panel below the
Project->Compute panel group. The panel turns on if Nova API Project->Compute panel group. The panel turns on if Nova API
extension 'ServerGroups' is available. It displays information about extension 'ServerGroups' is available. It displays information about
server groups. server groups. The details page for each server group also shows
information about instances of that server group.
Supported actions: create, delete. Supported actions: create, delete.