Trunks panel: edit button

Trunk edit is a 2-step workflow:
  * Basic trunk attributes
  * Subports selector transfertable:
      Selects many ports with segmentation details (optional)

In the port selector step reused and built on port allocator
transfertable from launch instance.

The easiest way to test is to take the whole change series by taking the
last change in it, then build devstack with neutron trunk support. Eg:

  local.conf:
  enable_plugin neutron https://git.openstack.org/openstack/neutron
  enable_service q-trunk

If you want to test this change in isolation you also need the following
Horizon config:

  openstack_dashboard/enabled/_1500_project_trunks_panel.py:
  DISABLED = False  # or just remove this line

Co-Authored-By: Bence Romsics <bence.romsics@ericsson.com>
Change-Id: I4a9f3bd710c9f6bc9ed942b107c6720865b1bc73
Partially-Implements: blueprint neutron-trunk-ui
This commit is contained in:
Lajos Katona 2017-07-12 08:54:38 +02:00 committed by Bence Romsics
parent 1ab4b498f2
commit ec299ff301
18 changed files with 781 additions and 10 deletions

View File

@ -824,6 +824,13 @@ def list_resources_with_long_filters(list_method,
return resources
@profiler.trace
def trunk_show(request, trunk_id):
LOG.debug("trunk_show(): trunk_id=%s", trunk_id)
trunk = neutronclient(request).show_trunk(trunk_id).get('trunk')
return Trunk(trunk)
@profiler.trace
def trunk_list(request, **params):
LOG.debug("trunk_list(): params=%s", params)
@ -847,10 +854,107 @@ def trunk_delete(request, trunk_id):
neutronclient(request).delete_trunk(trunk_id)
def _prepare_body_update_trunk(prop_diff):
"""Prepare body for PUT /v2.0/trunks/TRUNK_ID."""
return {'trunk': prop_diff}
def _prepare_body_remove_subports(subports):
"""Prepare body for PUT /v2.0/trunks/TRUNK_ID/remove_subports."""
return {'sub_ports': [{'port_id': sp['port_id']} for sp in subports]}
def _prepare_body_add_subports(subports):
"""Prepare body for PUT /v2.0/trunks/TRUNK_ID/add_subports."""
return {'sub_ports': subports}
@profiler.trace
def trunk_show(request, trunk_id):
LOG.debug("trunk_show(): trunk_id=%s", trunk_id)
trunk = neutronclient(request).show_trunk(trunk_id).get('trunk')
def trunk_update(request, trunk_id, old_trunk, new_trunk):
"""Handle update to a trunk in (at most) three neutron calls.
The JavaScript side should know only about the old and new state of a
trunk. However it should not know anything about how the old and new are
meant to be diffed and sent to neutron. We handle that here.
This code was adapted from Heat, see: https://review.openstack.org/442496
Call #1) Update all changed properties but 'sub_ports'.
PUT /v2.0/trunks/TRUNK_ID
openstack network trunk set
Call #2) Delete subports not needed anymore.
PUT /v2.0/trunks/TRUNK_ID/remove_subports
openstack network trunk unset --subport
Call #3) Create new subports.
PUT /v2.0/trunks/TRUNK_ID/add_subports
openstack network trunk set --subport
A single neutron port cannot be two subports at the same time (ie.
have two segmentation (type, ID)s on the same trunk or to belong to
two trunks). Therefore we have to delete old subports before creating
new ones to avoid conflicts.
"""
LOG.debug("trunk_update(): trunk_id=%s", trunk_id)
# NOTE(bence romsics): We want to do set operations on the subports,
# however we receive subports represented as dicts. In Python
# mutable objects like dicts are not hashable so they cannot be
# inserted into sets. So we convert subport dicts to (immutable)
# frozensets in order to do the set operations.
def dict2frozenset(d):
"""Convert a dict to a frozenset.
Create an immutable equivalent of a dict, so it's hashable
therefore can be used as an element of a set or a key of another
dictionary.
"""
return frozenset(d.items())
# cf. neutron_lib/api/definitions/trunk.py
updatable_props = ('admin_state_up', 'description', 'name')
prop_diff = {
k: new_trunk[k]
for k in updatable_props
if old_trunk[k] != new_trunk[k]}
subports_old = {dict2frozenset(d): d
for d in old_trunk.get('sub_ports', [])}
subports_new = {dict2frozenset(d): d
for d in new_trunk.get('sub_ports', [])}
old_set = set(subports_old.keys())
new_set = set(subports_new.keys())
delete = old_set - new_set
create = new_set - old_set
dicts_delete = [subports_old[fs] for fs in delete]
dicts_create = [subports_new[fs] for fs in create]
trunk = old_trunk
if prop_diff:
LOG.debug('trunk_update(): update properties of trunk %s: %s',
trunk_id, prop_diff)
body = _prepare_body_update_trunk(prop_diff)
trunk = neutronclient(request).update_trunk(
trunk_id, body=body).get('trunk')
if dicts_delete:
LOG.debug('trunk_update(): delete subports of trunk %s: %s',
trunk_id, dicts_delete)
body = _prepare_body_remove_subports(dicts_delete)
trunk = neutronclient(request).trunk_remove_subports(
trunk_id, body=body)
if dicts_create:
LOG.debug('trunk_update(): create subports of trunk %s: %s',
trunk_id, dicts_create)
body = _prepare_body_add_subports(dicts_create)
trunk = neutronclient(request).trunk_add_subports(
trunk_id, body=body)
return Trunk(trunk)

View File

@ -153,6 +153,15 @@ class Trunk(generic.View):
trunk = api.neutron.trunk_show(request, trunk_id)
return trunk.to_dict()
@rest_utils.ajax(data_required=True)
def patch(self, request, trunk_id):
"""Update a specific trunk"""
old_trunk = request.DATA[0]
new_trunk = request.DATA[1]
return api.neutron.trunk_update(
request, trunk_id, old_trunk, new_trunk)
@urls.register
class Trunks(generic.View):

View File

@ -49,7 +49,8 @@
getSubnets: getSubnets,
getTrunk: getTrunk,
getTrunks: getTrunks,
updateProjectQuota: updateProjectQuota
updateProjectQuota: updateProjectQuota,
updateTrunk: updateTrunk
};
return service;
@ -456,5 +457,18 @@
});
return promise;
}
/**
* @name updateTrunk
* @description
* Update an existing trunk.
*/
function updateTrunk(oldTrunk, newTrunk) {
return apiService.patch('/api/neutron/trunks/' + oldTrunk.id + '/', [oldTrunk, newTrunk])
.error(function() {
toastService.add('error', gettext('Unable to update the trunk.'));
});
}
}
}());

View File

@ -38,6 +38,58 @@
expect(service).toBeDefined();
});
it('converts created_at and updated_at to human readable if calling getTrunk', function() {
var data = {
id: 1,
created_at: '2017-11-16',
updated_at: '2017-11-16'
};
spyOn(apiService, 'get').and.callFake(function() {
return {
success: function(c) {
c(data);
return this;
},
error: function(c) {
c();
return this;
}
};
});
service.getTrunk(data.id, true).success(function(result) {
expect(result.id).toEqual(data.id);
expect(result.created_at).toEqual(new Date(data.created_at));
expect(result.updated_at).toEqual(new Date(data.updated_at));
});
});
it('converts created_at and updated_at to human readable if calling getTrunks', function() {
var data = {items: [{
id: 1,
created_at: '2017-11-16',
updated_at: '2017-11-16'
}]};
spyOn(apiService, 'get').and.callFake(function() {
return {
success: function(c) {
c(data);
return this;
},
error: function(c) {
c();
return this;
}
};
});
service.getTrunks().success(function(result) {
result.items.forEach(function(trunk) {
expect(trunk.id).toEqual(data.items[0].id);
expect(trunk.created_at).toEqual(new Date(data.items[0].created_at));
expect(trunk.updated_at).toEqual(new Date(data.items[0].updated_at));
});
});
});
var tests = [
{
@ -181,6 +233,20 @@
42
]
},
{
"func": "updateTrunk",
"method": "patch",
"path": "/api/neutron/trunks/42/",
"error": "Unable to update the trunk.",
"data": [
{"id": 42, "name": "trunk1"},
{"name": "trunk2"}
],
"testInput": [
{"id": 42, "name": "trunk1"},
{"name": "trunk2"}
]
},
{
"func": "getQosPolicy",
"method": "get",

View File

@ -34,6 +34,7 @@
registerTrunkActions.$inject = [
'horizon.framework.conf.resource-type-registry.service',
'horizon.app.core.trunks.actions.create.service',
'horizon.app.core.trunks.actions.edit.service',
'horizon.app.core.trunks.actions.delete.service',
'horizon.app.core.trunks.resourceType'
];
@ -41,6 +42,7 @@
function registerTrunkActions(
registry,
createService,
editService,
deleteService,
trunkResourceTypeCode
) {
@ -57,6 +59,13 @@
});
trunkResourceType.itemActions
.append({
id: 'editTrunkAction',
service: editService,
template: {
text: gettext('Edit Trunk')
}
})
.append({
id: 'deleteTrunkAction',
service: deleteService,

View File

@ -0,0 +1,148 @@
/*
* Copyright 2017 Ericsson
*
* 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.trunks')
.factory('horizon.app.core.trunks.actions.edit.service', editService);
editService.$inject = [
'$q',
'horizon.app.core.openstack-service-api.neutron',
'horizon.app.core.openstack-service-api.policy',
'horizon.app.core.openstack-service-api.userSession',
'horizon.app.core.trunks.actions.editWorkflow',
'horizon.app.core.trunks.actions.ports-extra.service',
'horizon.app.core.trunks.resourceType',
'horizon.framework.util.actions.action-result.service',
'horizon.framework.widgets.modal-wait-spinner.service',
'horizon.framework.widgets.modal.wizard-modal.service',
'horizon.framework.widgets.toast.service'
];
/**
* @ngDoc factory
* @name horizon.app.core.trunks.actions.editService
* @Description A service to handle the Edit Trunk modal.
*/
function editService(
$q,
neutron,
policy,
userSession,
editWorkflow,
portsExtra,
resourceType,
actionResultService,
spinnerService,
wizardModalService,
toast
) {
var service = {
perform: perform,
allowed: allowed
};
return service;
////////////
function allowed() {
return policy.ifAllowed(
{rules: [
['network', 'add_subports'],
['network', 'remove_subports']
]}
);
}
function perform(selected) {
// See also at perform() in create action.
spinnerService.showModalSpinner(gettext('Please Wait'));
return $q.all({
getNetworks: neutron.getNetworks(),
getPorts: userSession.get().then(function(session) {
return neutron.getPorts({project_id: session.project_id});
}),
getTrunk: neutron.getTrunk(selected.id)
}).then(function(responses) {
var networks = responses.getNetworks.data.items;
var ports = responses.getPorts.data.items;
var trunk = responses.getTrunk.data;
return {
subportCandidates: portsExtra.addNetworkAndSubnetInfo(
ports.filter(portsExtra.isSubportCandidate),
networks),
subportsOfInitTrunk: portsExtra.addNetworkAndSubnetInfo(
ports.filter(portsExtra.isSubportOfTrunk.bind(null, selected.id)),
networks),
trunk: trunk
};
}).then(openModal);
function openModal(params) {
spinnerService.hideModalSpinner();
return wizardModalService.modal({
workflow: editWorkflow,
submit: submit,
data: {
initTrunk: params.trunk,
ports: {
parentPortCandidates: [],
subportCandidates: params.subportCandidates.sort(
portsExtra.cmpPortsByNameAndId),
subportsOfInitTrunk: params.subportsOfInitTrunk.sort(
portsExtra.cmpSubportsBySegmentationTypeAndId)
},
// There's no point of cross-hiding ports between the parent port
// and subports steps since the edit workflow cannot have a parent
// port step.
crossHide: false
}
}).result;
}
}
function submit(stepModels) {
// See also at submit() in create action.
var oldTrunk = stepModels.initTrunk;
var trunk = angular.copy(oldTrunk);
var stepName, getTrunkSlice;
for (stepName in stepModels.trunkSlices) {
if (stepModels.trunkSlices.hasOwnProperty(stepName)) {
getTrunkSlice = stepModels.trunkSlices[stepName];
angular.extend(trunk, getTrunkSlice());
}
}
return neutron.updateTrunk(oldTrunk, trunk).then(onSuccess);
function onSuccess(response) {
var trunk = response.data;
// We show this green toast on a no-op update too, but meh.
toast.add('success', interpolate(
gettext('Trunk %s was successfully edited.'), [trunk.name]));
return actionResultService.getActionResult()
.updated(resourceType, trunk.id)
.result;
}
}
}
})();

View File

@ -0,0 +1,180 @@
/*
* Copyright 2017 Ericsson
*
* 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('horizon.app.core.trunks.actions.edit.service', function() {
var $q, $scope, service, modalWaitSpinnerService, deferred, $timeout;
var policyAPI = {
ifAllowed: function() {
return {
success: function(callback) {
callback({allowed: true});
}
};
}
};
var wizardModalService = {
modal: function () {
return {
result: undefined
};
}
};
var neutronAPI = {
getNetworks: function() {
return $q.when(
{data: {items: []}}
);
},
getPorts: function() {
return $q.when(
{data: {items: []}}
);
},
getTrunk: function() {
return $q.when(
{data: {items: []}}
);
},
updateTrunk: function(oldTrunk, newTrunk) {
return $q.when(
{data: newTrunk}
);
}
};
var userSession = {
isCurrentProject: function() {
deferred.resolve();
return deferred.promise;
},
get: function() {
return $q.when({'project_id': '1'});
}
};
////////////
beforeEach(module('horizon.app.core'));
beforeEach(module(function($provide) {
$provide.value('horizon.framework.widgets.modal.wizard-modal.service',
wizardModalService);
$provide.value('horizon.app.core.openstack-service-api.policy',
policyAPI);
$provide.value('horizon.app.core.openstack-service-api.neutron',
neutronAPI);
$provide.value('horizon.app.core.openstack-service-api.userSession',
userSession);
}));
beforeEach(inject(function($injector, $rootScope, _$q_, _$timeout_) {
$q = _$q_;
$timeout = _$timeout_;
$scope = $rootScope.$new();
deferred = $q.defer();
service = $injector.get('horizon.app.core.trunks.actions.edit.service');
modalWaitSpinnerService = $injector.get(
'horizon.framework.widgets.modal-wait-spinner.service'
);
}));
it('should check the policy if the user is allowed to update trunks', function() {
spyOn(policyAPI, 'ifAllowed').and.callThrough();
var allowed = service.allowed();
expect(allowed).toBeTruthy();
expect(policyAPI.ifAllowed).toHaveBeenCalledWith(
{ rules: [['network', 'add_subports'], ['network', 'remove_subports']] }
);
});
it('open the modal with the correct parameters', function() {
spyOn(wizardModalService, 'modal').and.callThrough();
spyOn(modalWaitSpinnerService, 'showModalSpinner');
spyOn(modalWaitSpinnerService, 'hideModalSpinner');
service.perform({id: 1});
expect(modalWaitSpinnerService.showModalSpinner).toHaveBeenCalledWith('Please Wait');
$timeout.flush();
expect(wizardModalService.modal).toHaveBeenCalled();
expect(modalWaitSpinnerService.hideModalSpinner).toHaveBeenCalled();
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
expect(modalArgs.scope).toBeUndefined();
expect(modalArgs.workflow).toBeDefined();
expect(modalArgs.submit).toBeDefined();
expect(modalArgs.data.initTrunk).toBeDefined();
expect(modalArgs.data.ports).toBeDefined();
});
it('should submit edit trunk request to neutron', function() {
spyOn(neutronAPI, 'updateTrunk').and.callThrough();
spyOn(wizardModalService, 'modal').and.callThrough();
spyOn(modalWaitSpinnerService, 'showModalSpinner');
spyOn(modalWaitSpinnerService, 'hideModalSpinner');
service.perform({id: 1});
$timeout.flush();
var modalArgs = wizardModalService.modal.calls.argsFor(0)[0];
modalArgs.submit(
{
initTrunk: {
name: 'oldtrunk',
id: 1
},
trunkSlices: {
step1: function () {
return {
name: 'newtrunk'
};
},
step3: function () {
return {
sub_ports: [
{port_id: 'subport uuid', segmentation_type: 'vlan', segmentation_id: 100}
]
};
}
}
}
);
$scope.$apply();
expect(neutronAPI.updateTrunk).toHaveBeenCalled();
expect(neutronAPI.updateTrunk.calls.argsFor(0)[0]).toEqual({
name: 'oldtrunk',
id: 1
});
expect(neutronAPI.updateTrunk.calls.argsFor(0)[1]).toEqual({
name: 'newtrunk',
id: 1,
sub_ports: [
{port_id: 'subport uuid', segmentation_type: 'vlan', segmentation_id: 100}
]
});
});
});
})();

View File

@ -0,0 +1,62 @@
/*
* Copyright 2017 Ericsson
*
* 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.trunks')
.factory('horizon.app.core.trunks.actions.editWorkflow', editWorkflow);
editWorkflow.$inject = [
'horizon.app.core.trunks.basePath',
'horizon.app.core.workflow.factory',
'horizon.framework.util.i18n.gettext'
];
/**
* @ngdoc factory
* @name horizon.app.core.trunks.editWorkflow
* @description A workflow for the edit trunk action.
*/
function editWorkflow(
basePath,
workflowService,
gettext
) {
var workflow = workflowService({
title: gettext('Edit Trunk'),
btnText: {finish: gettext('Edit Trunk')},
steps: [
{
title: gettext('Details'),
templateUrl: basePath + 'steps/trunk-details.html',
helpUrl: basePath + 'steps/trunk-details.help.html',
formName: 'detailsForm'
},
{
title: gettext('Subports'),
templateUrl: basePath + 'steps/trunk-subports.html',
helpUrl: basePath + 'steps/trunk-subports.help.html',
formName: 'subportsForm'
}
]
});
return workflow;
}
})();

View File

@ -33,6 +33,7 @@
var service = {
addNetworkAndSubnetInfo: addNetworkAndSubnetInfo,
cmpPortsByNameAndId: cmpPortsByNameAndId,
cmpSubportsBySegmentationTypeAndId: cmpSubportsBySegmentationTypeAndId,
isParentPortCandidate: isParentPortCandidate,
isSubportCandidate: isSubportCandidate,
isSubportOfTrunk: isSubportOfTrunk
@ -92,6 +93,15 @@
);
}
function cmpSubportsBySegmentationTypeAndId(a, b) {
return (
// primary key: segmentation type
a.segmentation_type.localeCompare(b.segmentation_type) ||
// secondary key: segmentation id
(a.segmentation_id - b.segmentation_id)
);
}
function addNetworkAndSubnetInfo(inPorts, networks) {
var networksDict = {};
networks.forEach(function(network) {

View File

@ -95,6 +95,18 @@
expect(service.isParentPortCandidate(port2)).toBe(false);
});
it('should compare subports by segmentation type', function() {
var port1 = {segmentation_type: 'vlan'};
var port2 = {segmentation_type: 'inherit'};
expect(service.cmpSubportsBySegmentationTypeAndId(port1, port2)).toBe(1);
});
it('should compare subports by segmentation id', function() {
var port1 = {segmentation_type: 'vlan', segmentation_id: 100};
var port2 = {segmentation_type: 'vlan', segmentation_id: 1000};
expect(service.cmpSubportsBySegmentationTypeAndId(port1, port2)).toBeLessThan(0);
});
});
})();

View File

@ -1,3 +1,4 @@
<hz-resource-panel resource-type-name="OS::Neutron::Trunk">
<hz-resource-table resource-type-name="OS::Neutron::Trunk"></hz-resource-table>
</hz-resource-panel>
<hz-resource-table resource-type-name="OS::Neutron::Trunk"
track-by="trackBy"></hz-resource-table>
</hz-resource-panel>

View File

@ -131,6 +131,38 @@
expect(ctrl.subportsTables.available).toEqual([{id: 1}, {id: 2}]);
});
it('should add to allocated list the subports of the edited trunk', function() {
inject(function($rootScope, $controller) {
scope = $rootScope.$new();
scope.crossHide = true;
scope.ports = {
subportCandidates: [{id: 1}, {id: 4}],
subportsOfInitTrunk: [{id: 4, segmentation_id: 2, segmentation_type: 'vlan'}]
};
scope.stepModels = {};
scope.initTrunk = {
sub_ports: [{port_id: 4, segmentation_type: 'vlan', segmentation_id: 2}]
};
ctrl = $controller('TrunkSubPortsController', {
$scope: scope
});
});
expect(ctrl.subportsDetails).toBeDefined();
expect(ctrl.subportsDetails).toEqual({
4: {
segmentation_id: 2,
segmentation_type: 'vlan'
}
});
var subports = scope.stepModels.trunkSlices.getSubports();
expect(subports).toEqual({
sub_ports: [
{port_id: 4, segmentation_id: 2, segmentation_type: 'vlan'}
]
});
});
});
});

View File

@ -69,7 +69,27 @@
function getTrunksForProject(userSession) {
params.project_id = userSession.project_id;
return neutron.getTrunks(params);
return neutron.getTrunks(params).then(addTrackBy);
}
// Unless we add a composite 'trackBy' field, hz-resource-table of the
// trunks panel will not get refreshed after editing a trunk.
// hz-resource-table needs to be told where to expect this information.
// See also the track-by attribute of hz-resource-table element in the
// trunks panel template.
function addTrackBy(response) {
return {data: {items: response.data.items.map(function(trunk) {
trunk.trackBy = [
trunk.id,
trunk.revision_number,
// It is weird but there are trunk updates when the revision number
// does not increase. Eg. if you only update the description of a
// trunk. So we also add 'updated_at' to the composite.
trunk.updated_at.toISOString()
].join('/');
return trunk;
})}};
}
}

View File

@ -57,14 +57,15 @@
var session = $injector.get('horizon.app.core.openstack-service-api.userSession');
var deferred = $q.defer();
var deferredSession = $q.defer();
var updatedAt = new Date('November 15, 2017');
spyOn(neutron, 'getTrunks').and.returnValue(deferred.promise);
spyOn(session, 'get').and.returnValue(deferredSession.promise);
var result = service.getTrunksPromise({});
deferred.resolve({data: {items: [{id: 1, updated_at: 'Apr10'}]}});
deferred.resolve({data: {items: [{id: 1, updated_at: updatedAt}]}});
deferredSession.resolve({project_id: '42'});
$timeout.flush();
expect(neutron.getTrunks).toHaveBeenCalled();
expect(result.$$state.value.data.items[0].updated_at).toBe('Apr10');
expect(result.$$state.value.data.items[0].updated_at).toBe(updatedAt);
expect(result.$$state.value.data.items[0].id).toBe(1);
}));

View File

@ -180,6 +180,18 @@ class NeutronTrunkTestCase(test.TestCase):
client.trunk_show.assert_called_once_with(
request, trunk_id)
@mock.patch.object(neutron.api, 'neutron')
def test_trunk_patch(self, client):
request = self.mock_rest_request(body='''
[{"name": "trunk1"}, {"name": "trunk2"}]
''')
response = neutron.Trunk().patch(request, '1')
self.assertStatusCode(response, 200)
client.trunk_update.assert_called_once_with(
request, '1', {'name': 'trunk1'}, {'name': 'trunk2'}
)
class NeutronTrunksTestCase(test.TestCase):
def setUp(self):

View File

@ -557,6 +557,96 @@ class NeutronApiTests(test.APITestCase):
api.neutron.trunk_delete(self.request, trunk_id)
def test_trunk_update_details(self):
trunk_data = self.api_trunks.first()
trunk_id = trunk_data['id']
old_trunk = {'name': trunk_data['name'],
'description': trunk_data['description'],
'id': trunk_data['id'],
'port_id': trunk_data['port_id'],
'admin_state_up': trunk_data['admin_state_up']}
new_trunk = {'name': 'foo',
'description': trunk_data['description'],
'id': trunk_data['id'],
'port_id': trunk_data['port_id'],
'admin_state_up': trunk_data['admin_state_up']}
neutronclient = self.stub_neutronclient()
neutronclient.update_trunk(trunk_id, body={'trunk': {'name': 'foo'}})\
.AndReturn({'trunk': new_trunk})
self.mox.ReplayAll()
ret_val = api.neutron.trunk_update(self.request, trunk_id,
old_trunk, new_trunk)
self.assertIsInstance(ret_val, api.neutron.Trunk)
self.assertEqual(api.neutron.Trunk(trunk_data).id, ret_val.id)
self.assertEqual(ret_val.name, new_trunk['name'])
def test_trunk_update_add_subports(self):
trunk_data = self.api_trunks.first()
trunk_id = trunk_data['id']
old_trunk = {'name': trunk_data['name'],
'description': trunk_data['description'],
'id': trunk_data['id'],
'port_id': trunk_data['port_id'],
'sub_ports': trunk_data['sub_ports'],
'admin_state_up': trunk_data['admin_state_up']}
new_trunk = {'name': trunk_data['name'],
'description': trunk_data['description'],
'id': trunk_data['id'],
'port_id': trunk_data['port_id'],
'sub_ports': [
{'port_id': 1,
'segmentation_id': 100,
'segmentation_type': 'vlan'}],
'admin_state_up': trunk_data['admin_state_up']}
neutronclient = self.stub_neutronclient()
neutronclient.trunk_add_subports(trunk_id, body={
'sub_ports': [
{'port_id': 1, 'segmentation_id': 100,
'segmentation_type': 'vlan'}
]})\
.AndReturn({'trunk': new_trunk})
self.mox.ReplayAll()
ret_val = api.neutron.trunk_update(self.request, trunk_id,
old_trunk, new_trunk)
self.assertIsInstance(ret_val, api.neutron.Trunk)
self.assertEqual(api.neutron.Trunk(trunk_data).id, ret_val.trunk['id'])
self.assertEqual(ret_val.trunk['sub_ports'], new_trunk['sub_ports'])
def test_trunk_update_remove_subports(self):
trunk_data = self.api_trunks.first()
trunk_id = trunk_data['id']
old_trunk = {'name': trunk_data['name'],
'description': trunk_data['description'],
'id': trunk_data['id'],
'port_id': trunk_data['port_id'],
'sub_ports': [
{'port_id': 1,
'segmentation_id': 100,
'segmentation_type': 'vlan'}],
'admin_state_up': trunk_data['admin_state_up']}
new_trunk = {'name': trunk_data['name'],
'description': trunk_data['description'],
'id': trunk_data['id'],
'port_id': trunk_data['port_id'],
'sub_ports': [],
'admin_state_up': trunk_data['admin_state_up']}
neutronclient = self.stub_neutronclient()
neutronclient.trunk_remove_subports(trunk_id, body={
'sub_ports': [{'port_id': old_trunk['sub_ports'][0]['port_id']}]})\
.AndReturn({'trunk': new_trunk})
self.mox.ReplayAll()
ret_val = api.neutron.trunk_update(self.request, trunk_id,
old_trunk, new_trunk)
self.assertIsInstance(ret_val, api.neutron.Trunk)
self.assertEqual(api.neutron.Trunk(trunk_data).id, ret_val.trunk['id'])
self.assertEqual(ret_val.trunk['sub_ports'], new_trunk['sub_ports'])
def test_router_list(self):
routers = {'routers': self.api_routers.list()}

View File

@ -388,6 +388,7 @@ def data(TEST):
trunk_dict = {'status': 'UP',
'sub_ports': [],
'name': 'trunk1',
'description': 'blah',
'admin_state_up': True,
'tenant_id': '1',
'project_id': '1',

View File

@ -6,4 +6,4 @@ features:
panel turns on if Neutron API extension 'trunk' is available. It
displays information about trunks. The details page for each trunk
also shows information about subports of that trunk.
Supported actions: create, delete.
Supported actions: create, edit, delete.