Add Network Port selection to new instance launch

Adds a new step to the new instance launch
with the available ports.
The operator can add or remove ports to be
attached to the instance on boot.
This allows the operator to use ports
at launch instead or together with a network.
Adds tests and modifies other tests to take
into account the new number of steps and the
new addition to the model.

Change-Id: I29fdce433a5a8a9e3ebedbd1a1d96399890e4c6c
Implements: blueprint allow-launching-ports
This commit is contained in:
Itxaka 2015-12-04 17:11:25 +01:00 committed by Rob Cresswell
parent a44085d914
commit 2aeb99a145
12 changed files with 571 additions and 65 deletions

View File

@ -119,6 +119,7 @@
instance: null
},
networks: [],
ports: [],
neutronEnabled: false,
novaLimits: {},
profiles: [],
@ -154,6 +155,7 @@
// REQUIRED
name: null,
networks: [],
ports: [],
profile: {},
// REQUIRED Server Key. May be empty.
security_groups: [],
@ -254,6 +256,7 @@
setFinalSpecBootsource(finalSpec);
setFinalSpecFlavor(finalSpec);
setFinalSpecNetworks(finalSpec);
setFinalSpecPorts(finalSpec);
setFinalSpecKeyPairs(finalSpec);
setFinalSpecSecurityGroups(finalSpec);
setFinalSpecMetadata(finalSpec);
@ -359,13 +362,14 @@
// Networks
function getNetworks() {
return neutronAPI.getNetworks().then(onGetNetworks, noop);
return neutronAPI.getNetworks().then(onGetNetworks, noop).then(getPorts, noop);
}
function onGetNetworks(data) {
model.neutronEnabled = true;
model.networks.length = 0;
push.apply(model.networks, data.data.items);
return data;
}
function setFinalSpecNetworks(finalSpec) {
@ -380,6 +384,55 @@
delete finalSpec.networks;
}
function getPorts(networks) {
model.ports.length = 0;
networks.data.items.forEach(function(network) {
return neutronAPI.getPorts({network_id: network.id}).then(
function(ports) {
onGetPorts(ports, network);
}, noop
);
});
}
function onGetPorts(networkPorts, network) {
var ports = [];
networkPorts.data.items.forEach(function(port) {
// no device_owner means that the port can be attached
if (port.device_owner === "" && port.admin_state === "UP") {
port.subnet_names = getPortSubnets(port, network.subnets);
port.network_name = network.name;
ports.push(port);
}
});
push.apply(model.ports, ports);
}
// helper function to return an object of IP:NAME pairs for subnet mapping
function getPortSubnets(port, subnets) {
var subnetNames = {};
port.fixed_ips.forEach(function (ip) {
subnets.forEach(function (subnet) {
if (ip.subnet_id === subnet.id) {
subnetNames[ip.ip_address] = subnet.name;
}
});
});
return subnetNames;
}
function setFinalSpecPorts(finalSpec) {
// nics should already be filled so we only append to it
finalSpec.ports.forEach(function (port) {
finalSpec.nics.push(
{
"port-id": port.id
});
});
delete finalSpec.ports;
}
// Boot Source
function getImages() {

View File

@ -23,6 +23,53 @@
var cinderEnabled = false;
var neutronEnabled = false;
var novaExtensionsEnabled = false;
var novaApi = {
createServer: function(finalSpec) {
return {
then: function () {
return finalSpec;
}
};
},
getAvailabilityZones: function() {
var zones = [
{ zoneName: 'zone-1', zoneState: { available: true } },
{ zoneName: 'zone-2', zoneState: { available: true } },
{ zoneName: 'invalid-zone-1' },
{ zoneName: 'invalid-zone-2' }
];
var deferred = $q.defer();
deferred.resolve({ data: { items: zones } });
return deferred.promise;
},
getFlavors: function() {
var flavors = [ 'flavor-1', 'flavor-2' ];
var deferred = $q.defer();
deferred.resolve({ data: { items: flavors } });
return deferred.promise;
},
getKeypairs: function() {
var keypairs = [ { keypair: { name: 'key-1' } },
{ keypair: { name: 'key-2' } } ];
var deferred = $q.defer();
deferred.resolve({ data: { items: keypairs } });
return deferred.promise;
},
getLimits: function() {
var limits = { maxTotalInstances: 10, totalInstancesUsed: 0 };
var deferred = $q.defer();
deferred.resolve({ data: limits });
return deferred.promise;
}
};
beforeEach(module('horizon.dashboard.project.workflow.launch-instance'));
@ -61,53 +108,7 @@
};
});
$provide.value('horizon.app.core.openstack-service-api.nova', {
createServer: function(finalSpec) {
return {
then: function () {
return finalSpec;
}
};
},
getAvailabilityZones: function() {
var zones = [
{ zoneName: 'zone-1', zoneState: { available: true } },
{ zoneName: 'zone-2', zoneState: { available: true } },
{ zoneName: 'invalid-zone-1' },
{ zoneName: 'invalid-zone-2' }
];
var deferred = $q.defer();
deferred.resolve({ data: { items: zones } });
return deferred.promise;
},
getFlavors: function() {
var flavors = [ 'flavor-1', 'flavor-2' ];
var deferred = $q.defer();
deferred.resolve({ data: { items: flavors } });
return deferred.promise;
},
getKeypairs: function() {
var keypairs = [ { keypair: { name: 'key-1' } },
{ keypair: { name: 'key-2' } } ];
var deferred = $q.defer();
deferred.resolve({ data: { items: keypairs } });
return deferred.promise;
},
getLimits: function() {
var limits = { maxTotalInstances: 10, totalInstancesUsed: 0 };
var deferred = $q.defer();
deferred.resolve({ data: limits });
return deferred.promise;
}
});
$provide.value('horizon.app.core.openstack-service-api.nova', novaApi);
$provide.value('horizon.app.core.openstack-service-api.security-group', {
query: function() {
@ -130,6 +131,23 @@
var deferred = $q.defer();
deferred.resolve({ data: { items: networks } });
return deferred.promise;
},
getPorts: function(network) {
var ports = {
'net-1': [
{ name: 'port-1', device_owner: '', fixed_ips: [], admin_state: 'UP' },
{ name: 'port-2', device_owner: '', fixed_ips: [], admin_state: 'DOWN' }
],
'net-2': [
{ name: 'port-3', device_owner: 'owner', fixed_ips: [], admin_state: 'DOWN' },
{ name: 'port-4', device_owner: '', fixed_ips: [], admin_state: 'DOWN' }
]
};
var deferred = $q.defer();
deferred.resolve({ data: { items: ports[network.network_id] } });
return deferred.promise;
}
});
@ -351,6 +369,24 @@
expect(model.newInstanceSpec.config_drive).toBe(true);
});
it('should not set availability zone if the zone list is empty', function () {
spyOn(novaApi, 'getAvailabilityZones').and.callFake(function () {
var deferred = $q.defer();
deferred.resolve({ data: { items: [] } });
return deferred.promise;
});
model.initialize(true);
scope.$apply();
expect(model.availabilityZones.length).toBe(0);
expect(model.newInstanceSpec.availability_zone).toBe(null);
});
it('sets the ports properly based on device_owner', function () {
model.initialize(true);
scope.$apply();
expect(model.ports.length).toBe(1);
});
});
describe('Post Initialization Model - Initializing', function() {
@ -363,7 +399,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(18);
expect(Object.keys(model.newInstanceSpec).length).toBe(19);
});
it('sets availability zone to null', function() {
@ -406,6 +442,10 @@
expect(model.newInstanceSpec.networks).toEqual([]);
});
it('sets ports to an empty array', function() {
expect(model.newInstanceSpec.ports).toEqual([]);
});
it('sets profile to an empty object', function() {
expect(model.newInstanceSpec.profile).toEqual({});
});
@ -440,6 +480,7 @@
model.newInstanceSpec.source = [ { id: 'cirros' } ];
model.newInstanceSpec.flavor = { id: 'm1.tiny' };
model.newInstanceSpec.networks = [ { id: 'public' }, { id: 'private' } ];
model.newInstanceSpec.ports = [ ];
model.newInstanceSpec.key_pair = [ { name: 'keypair1' } ];
model.newInstanceSpec.security_groups = [ { id: 'adminId', name: 'admin' },
{ id: 'demoId', name: 'demo' } ];
@ -569,6 +610,19 @@
expect(finalSpec.useless).toBeUndefined();
});
it('should set final spec in format required if ports are used', function() {
model.newInstanceSpec.ports = [{id: 'port1'}];
var finalSpec = model.createInstance();
var finalNetworks = [
{ 'net-id': 'public', 'v4-fixed-ip': '' },
{ 'net-id': 'private', 'v4-fixed-ip': '' },
{ 'port-id': 'port1' }
];
expect(finalSpec.nics).toEqual(finalNetworks);
});
it('provides null for device_name when falsy', function() {
model.newInstanceSpec.source_type.type = 'image';
model.newInstanceSpec.vol_device_name = false;

View File

@ -59,6 +59,14 @@
formName: 'launchInstanceNetworkForm',
requiredServiceTypes: ['network']
},
{
id: 'ports',
title: gettext('Network Ports'),
templateUrl: basePath + 'networkports/ports.html',
helpUrl: basePath + 'networkports/ports.help.html',
formName: 'launchInstanceNetworkPortForm',
requiredServiceTypes: ['network']
},
{
id: 'secgroups',
title: gettext('Security Groups'),

View File

@ -49,15 +49,16 @@
expect(launchInstanceWorkflow.title).toBeDefined();
});
it('should have the eight steps defined', function () {
it('should have the nine steps defined', function () {
expect(launchInstanceWorkflow.steps).toBeDefined();
expect(launchInstanceWorkflow.steps.length).toBe(8);
expect(launchInstanceWorkflow.steps.length).toBe(9);
var forms = [
'launchInstanceDetailsForm',
'launchInstanceSourceForm',
'launchInstanceFlavorForm',
'launchInstanceNetworkForm',
'launchInstanceNetworkPortForm',
'launchInstanceAccessAndSecurityForm',
'launchInstanceKeypairForm',
'launchInstanceConfigurationForm',
@ -72,6 +73,10 @@
it('specifies that the network step requires the network service type', function() {
expect(launchInstanceWorkflow.steps[3].requiredServiceTypes).toEqual(['network']);
});
it('specifies that the network port step requires the network service type', function() {
expect(launchInstanceWorkflow.steps[4].requiredServiceTypes).toEqual(['network']);
});
});
})();

View File

@ -28,10 +28,11 @@
LaunchInstanceNetworkController.$inject = [
'$scope',
'horizon.framework.widgets.action-list.button-tooltip.row-warning.service'
'horizon.framework.widgets.action-list.button-tooltip.row-warning.service',
'launchInstanceModel'
];
function LaunchInstanceNetworkController($scope, tooltipService) {
function LaunchInstanceNetworkController($scope, tooltipService, launchInstanceModel) {
var ctrl = this;
ctrl.networkStatuses = {
@ -48,7 +49,8 @@
available: $scope.model.networks,
allocated: $scope.model.newInstanceSpec.networks,
displayedAvailable: [],
displayedAllocated: []
displayedAllocated: [],
minItems: 1
};
ctrl.tableLimits = {
@ -101,6 +103,28 @@
]
}
];
function getPorts() {
return launchInstanceModel.newInstanceSpec.ports;
}
function toggleNetworksRequirement(newValue) {
// if there is a port selected, remove the validate-number-min
// for networks table
if (newValue.length > 0) {
ctrl.tableDataMulti.minItems = 0;
}
// if no port is selected restore the validate-number-min value
if (newValue.length === 0) {
ctrl.tableDataMulti.minItems = 1;
}
}
// If a port is selected, then networks are not required
var portWatcher = $scope.$watch(getPorts, toggleNetworksRequirement, true);
$scope.$on('$destroy', function() {
portWatcher();
});
}
})();

View File

@ -23,22 +23,44 @@
beforeEach(module('horizon.dashboard.project.workflow.launch-instance'));
describe('LaunchInstanceNetworkController', function() {
var scope, ctrl;
var scope, ctrl, model;
beforeEach(inject(function($controller) {
scope = {
model: {
newInstanceSpec: {
networks: ['net-a']
},
networks: ['net-a', 'net-b']
}
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
model = {
newInstanceSpec: {
networks: ['net-a'],
ports: []
},
networks: ['net-a', 'net-b']
};
scope.model = model;
spyOn(scope, '$watch').and.callThrough();
spyOn(scope, '$watchCollection').and.callThrough();
ctrl = $controller('LaunchInstanceNetworkController', {
$scope: scope
$scope: scope,
launchInstanceModel: model
});
}));
it("establishes one watch", function () {
expect(scope.$watch.calls.count()).toBe(1);
});
it("changes the network items required based on the ports", function() {
expect(ctrl.tableDataMulti.minItems).toEqual(1);
model.newInstanceSpec.ports = [{name: "1", id: "1"}];
scope.$apply();
expect(ctrl.tableDataMulti.minItems).toEqual(0);
model.newInstanceSpec.ports = [];
scope.$apply();
expect(ctrl.tableDataMulti.minItems).toEqual(1);
});
it('has correct network statuses', function() {
expect(ctrl.networkStatuses).toBeDefined();
expect(ctrl.networkStatuses.ACTIVE).toBeDefined();
@ -70,6 +92,7 @@
expect(ctrl.tableDataMulti.allocated).toEqual(['net-a']);
expect(ctrl.tableDataMulti.displayedAllocated).toEqual([]);
expect(ctrl.tableDataMulti.displayedAvailable).toEqual([]);
expect(ctrl.tableDataMulti.minItems).toEqual(1);
});
it('should set facets for search', function() {

View File

@ -11,7 +11,7 @@
</div>
<transfer-table tr-model="ctrl.tableDataMulti" help-text="ctrl.tableHelpText" limits="ctrl.tableLimits">
<allocated validate-number-min="1" ng-model="ctrl.tableDataMulti.allocated.length">
<allocated validate-number-min="{$ ctrl.tableDataMulti.minItems $}" ng-model="ctrl.tableDataMulti.allocated.length">
<table st-table="ctrl.tableDataMulti.displayedAllocated" st-safe-src="ctrl.tableDataMulti.allocated" hz-table
class="table table-striped table-rsp table-detail">
<thead>

View File

@ -0,0 +1,74 @@
/*
* (c) Copyright 2016 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
/**
* @ngdoc controller
* @name LaunchInstanceNetworkPortController
* @description
* Controller for the Launch Instance - Network Step.
*/
angular
.module('horizon.dashboard.project.workflow.launch-instance')
.controller('LaunchInstanceNetworkPortController', LaunchInstanceNetworkPortController);
LaunchInstanceNetworkPortController.$inject = [
'$scope',
'horizon.framework.widgets.action-list.button-tooltip.row-warning.service'
];
function LaunchInstanceNetworkPortController($scope, tooltipService) {
var ctrl = this;
ctrl.portStatuses = {
'ACTIVE': gettext('Active'),
'DOWN': gettext('Down')
};
ctrl.portAdminStates = {
'UP': gettext('Up'),
'DOWN': gettext('Down')
};
ctrl.vnicTypes = {
'normal': gettext('Normal'),
'direct': gettext('Direct'),
'macvtap': gettext('MacVTap')
};
ctrl.tableDataMulti = {
available: $scope.model.ports,
allocated: $scope.model.newInstanceSpec.ports,
displayedAvailable: [],
displayedAllocated: []
};
ctrl.tableLimits = {
maxAllocation: -1
};
ctrl.tableHelpText = {
allocHelpText: gettext('Select ports from those listed below.')
};
ctrl.tooltipModel = tooltipService;
ctrl.nameOrID = function nameOrId(data) {
return angular.isDefined(data.name) && data.name !== '' ? data.name : data.id;
};
}
})();

View File

@ -0,0 +1,23 @@
<p translate>
A port represents a virtual switch port on a logical network switch.
</p>
<p translate>
Ports can be created under a network by administrators.
</p>
<p translate>
Virtual instances attach their interfaces to ports.
</p>
<p translate>
The logical port also defines the MAC address and the IP address(es) to be assigned to the interfaces plugged into them.
</p>
<p translate>
When IP addresses are associated to a port, this also implies the port is associated with a subnet, as the IP address was taken from the allocation pool for a specific subnet.
</p>
<p translate>
When the <b>Admin State</b> for a port is set to <b>Up</b> and it has no <b>Device Owner</b>,
then the port is available for use. You can set the <b>Admin State</b> to <b>Down</b>
if you are not ready for other users to use the port.
</p>
<p translate>
The status indicates whether the port has an active connection.
</p>

View File

@ -0,0 +1,154 @@
<div ng-controller="LaunchInstanceNetworkPortController as ctrl">
<p translate>
Ports provide extra communication channels to your instances. You can select ports instead of networks or a mix of both.
</p>
<transfer-table tr-model="ctrl.tableDataMulti" help-text="ctrl.tableHelpText" limits="ctrl.tableLimits">
<allocated>
<table st-table="ctrl.tableDataMulti.displayedAllocated" st-safe-src="ctrl.tableDataMulti.allocated"
hz-table class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th class="reorder"></th>
<th class="expander"></th>
<th st-sort="name" st-sort-default class="rsp-p1" translate>Name</th>
<th class="rsp-p2" translate>IP</th>
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
<th st-sort="status" class="rsp-p1" translate>Status</th>
<th class="action-col"></th>
</tr>
</thead>
<tbody>
<tr ng-if="ctrl.tableDataMulti.allocated.length === 0">
<td colspan="7">
<div class="no-rows-help" translate>
Select an item from Available items below
</div>
</td>
</tr>
<tr ng-repeat-start="item in ctrl.tableDataMulti.displayedAllocated track by item.id"
lr-drag-data="ctrl.tableDataMulti.displayedAllocated" lr-drag-src="reorder"
lr-drop-target="reorder" lr-drop-success="trCtrl.updateAllocated(e, item, collection)">
<td class="reorder">
<span class="fa fa-sort" title="{$ 'Re-order items using drag and drop'|translate $}"></span>
{$ $index + 1 $}
</td>
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p2">
<div ng-repeat="ip in item.fixed_ips">
{$ ip.ip_address $} on subnet: {$ item.subnet_names[ip.ip_address] $}
</div>
</td>
<td class="rsp-p1">{$ item.admin_state | decode:ctrl.portAdminStates $}</td>
<td class="rsp-p1">{$ item.status | decode:ctrl.portStatuses $}</td>
<td class="action-col">
<action-list>
<action action-classes="'btn btn-sm btn-default'"
callback="trCtrl.deallocate" item="item">
<span class="fa fa-minus"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td colspan="7" class="detail">
<dl class="dl-horizontal">
<dt translate>ID</dt>
<dd>{$ item.id $}</dd>
<dt translate>Project ID</dt>
<dd>{$ item.tenant_id $}</dd>
<dt translate>Network ID</dt>
<dd>{$ item.network_id $}</dd>
<dt translate>Network</dt>
<dd>{$ item.network_name $}</dd>
<dt translate>VNIC type</dt>
<dd>{$ item['binding:vnic_type'] | decode:ctrl.vnicTypes $}</dd>
<div ng-if="item['binding:host_id']">
<dt translate>Host ID</dt>
<dd>{$ item['binding:host_id'] $}</dd>
</div>
</dl>
</td>
</tr>
</tbody>
</table>
</allocated>
<available>
<table st-table="ctrl.tableDataMulti.displayedAvailable" st-safe-src="ctrl.tableDataMulti.available"
hz-table class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th class="search-header" colspan="6">
<hz-search-bar 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 class="rsp-p2" translate>IP</th>
<th st-sort="admin_state" class="rsp-p1" translate>Admin State</th>
<th st-sort="status" class="rsp-p1" translate>Status</th>
<th class="action-col"></th>
</tr>
</thead>
<tbody>
<tr ng-if="trCtrl.numAvailable() === 0">
<td colspan="6">
<div class="no-rows-help" translate>
No available items
</div>
</td>
</tr>
<tr ng-repeat-start="item in ctrl.tableDataMulti.displayedAvailable track by item.id"
ng-if="!trCtrl.allocatedIds[item.id]">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p2">
<div ng-repeat="ip in item.fixed_ips">
{$ ip.ip_address $} on subnet: {$ item.subnet_names[ip.ip_address] $}
</div>
</td>
<td class="rsp-p1">{$ item.admin_state | decode:ctrl.portAdminStates $}</td>
<td class="rsp-p1">{$ item.status | decode:ctrl.portStatuses $}</td>
<td class="action-col">
<action-list>
<action action-classes="'btn btn-sm btn-default'"
callback="trCtrl.allocate" item="item">
<span class="fa fa-plus"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td colspan="6" class="detail">
<dl class="dl-horizontal">
<dt translate>ID</dt>
<dd>{$ item.id $}</dd>
<dt translate>Project ID</dt>
<dd>{$ item.tenant_id $}</dd>
<dt translate>Network ID</dt>
<dd>{$ item.network_id $}</dd>
<dt translate>Network</dt>
<dd>{$ item.network_name $}</dd>
<dt translate>VNIC type</dt>
<dd>{$ item['binding:vnic_type'] | decode:ctrl.vnicTypes $}</dd>
<div ng-if="item['binding:host_id']">
<dt translate>Host ID</dt>
<dd>{$ item['binding:host_id'] $}</dd>
</div>
</dl>
</td>
</tr>
</tbody>
</table>
</available>
</transfer-table>
</div>

View File

@ -0,0 +1,87 @@
/*
* (c) Copyright 2016 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
describe('Launch Instance Ports Step', function() {
beforeEach(module('horizon.framework.widgets'));
beforeEach(module('horizon.framework.widgets.action-list'));
beforeEach(module('horizon.dashboard.project.workflow.launch-instance'));
describe('LaunchInstanceNetworkPortController', function() {
var scope, ctrl;
beforeEach(inject(function($controller) {
scope = {
model: {
newInstanceSpec: {
ports: ['port-a']
},
ports: ['port-a', 'port-b']
}
};
ctrl = $controller('LaunchInstanceNetworkPortController', {
$scope: scope
});
}));
it('has correct ports statuses', function() {
expect(ctrl.portStatuses).toBeDefined();
expect(ctrl.portStatuses.ACTIVE).toBeDefined();
expect(ctrl.portStatuses.DOWN).toBeDefined();
expect(Object.keys(ctrl.portStatuses).length).toBe(2);
});
it('has correct network admin states', function() {
expect(ctrl.portAdminStates).toBeDefined();
expect(ctrl.portAdminStates.UP).toBeDefined();
expect(ctrl.portAdminStates.DOWN).toBeDefined();
expect(Object.keys(ctrl.portAdminStates).length).toBe(2);
});
it('defines a multiple-allocation table', function() {
expect(ctrl.tableLimits).toBeDefined();
expect(ctrl.tableLimits.maxAllocation).toBe(-1);
});
it('contains help text for the table', function() {
expect(ctrl.tableHelpText).toBeDefined();
expect(ctrl.tableHelpText.allocHelpText).toBeDefined();
});
it('nameOrId return the name', function() {
var obj = {name: 'test_name', id: 'test_id'};
expect(ctrl.nameOrID).toBeDefined();
expect(ctrl.nameOrID(obj)).toBe('test_name');
});
it('nameOrId return the id if the name is missing', function() {
expect(ctrl.nameOrID).toBeDefined();
expect(ctrl.nameOrID({'id': 'testid'})).toBe('testid');
});
it('uses scope to set table data', function() {
expect(ctrl.tableDataMulti).toBeDefined();
expect(ctrl.tableDataMulti.available).toEqual(['port-a', 'port-b']);
expect(ctrl.tableDataMulti.allocated).toEqual(['port-a']);
expect(ctrl.tableDataMulti.displayedAllocated).toEqual([]);
expect(ctrl.tableDataMulti.displayedAvailable).toEqual([]);
});
});
});
})();

View File

@ -303,6 +303,7 @@
});
});
});
});
describe('diskFormatFilter', function() {