Merge "Add load balancer flavor support"

This commit is contained in:
Zuul 2019-03-07 04:32:39 +00:00 committed by Gerrit Code Review
commit 9422dec8f0
14 changed files with 748 additions and 19 deletions

View File

@ -46,7 +46,7 @@ msgpack-python==0.4.0
munch==2.1.0
netaddr==0.7.18
netifaces==0.10.4
openstacksdk==0.11.2
openstacksdk==0.24.0
os-client-config==1.28.0
os-service-types==1.2.0
osc-lib==1.8.0

View File

@ -126,15 +126,28 @@ def poll_loadbalancer_status(request, loadbalancer_id, callback,
def create_loadbalancer(request):
data = request.DATA
flavor_id = data['loadbalancer'].get('flavor_id')
conn = _get_sdk_connection(request)
loadbalancer = conn.load_balancer.create_load_balancer(
project_id=request.user.project_id,
vip_subnet_id=data['loadbalancer']['vip_subnet_id'],
name=data['loadbalancer'].get('name'),
description=data['loadbalancer'].get('description'),
vip_address=data['loadbalancer'].get('vip_address'),
admin_state_up=data['loadbalancer'].get('admin_state_up')
)
if flavor_id:
loadbalancer = conn.load_balancer.create_load_balancer(
project_id=request.user.project_id,
vip_subnet_id=data['loadbalancer']['vip_subnet_id'],
name=data['loadbalancer'].get('name'),
description=data['loadbalancer'].get('description'),
vip_address=data['loadbalancer'].get('vip_address'),
admin_state_up=data['loadbalancer'].get('admin_state_up'),
flavor_id=flavor_id
)
else:
loadbalancer = conn.load_balancer.create_load_balancer(
project_id=request.user.project_id,
vip_subnet_id=data['loadbalancer']['vip_subnet_id'],
name=data['loadbalancer'].get('name'),
description=data['loadbalancer'].get('description'),
vip_address=data['loadbalancer'].get('vip_address'),
admin_state_up=data['loadbalancer'].get('admin_state_up'),
)
if data.get('listener'):
# There is work underway to add a new API to LBaaS v2 that will
@ -284,6 +297,39 @@ def create_health_monitor(request, **kwargs):
return _get_sdk_object_dict(health_mon)
def create_flavor(request, **kwargs):
"""Create a new flavor.
"""
data = request.DATA
conn = _get_sdk_connection(request)
flavor = conn.load_balancer.create_flavor(
name=data['flavor']['name'],
flavor_profile_id=data['flavor']['flavor_profile_id'],
description=data['flavor'].get('description'),
enabled=data['flavor'].get('enabled'),
)
return _get_sdk_object_dict(flavor)
def create_flavor_profile(request, **kwargs):
"""Create a new flavor profile.
"""
data = request.DATA
conn = _get_sdk_connection(request)
flavor_profile = conn.load_balancer.create_flavor(
name=data['flavor_profile']['name'],
provider_name=data['flavor_profile']['provider_name'],
flavor_data=data['flavor_profile']['flavor_data'],
)
return _get_sdk_object_dict(flavor_profile)
def add_member(request, **kwargs):
"""Add a member to a pool.
@ -534,6 +580,42 @@ def update_monitor(request, **kwargs):
return _get_sdk_object_dict(healthmonitor)
def update_flavor(request, **kwargs):
"""Update a flavor.
"""
data = request.DATA
flavor_id = data['flavor']['id']
conn = _get_sdk_connection(request)
flavor = conn.load_balancer.update_flavor(
flavor_id,
name=data['flavor'].get('name'),
description=data['flavor'].get('description'),
enabled=data['flavor'].get('enabled'),
)
return _get_sdk_object_dict(flavor)
def update_flavor_profile(request, **kwargs):
"""Update a flavor profile.
"""
data = request.DATA
flavor_profile_id = data['flavor_profile']['id']
conn = _get_sdk_connection(request)
flavor_profile = conn.load_balancer.update_flavor_profile(
flavor_profile_id,
name=data['flavor_profile'].get('name'),
provider_name=data['flavor_profile'].get('provider_name'),
flavor_data=data['flavor_profile'].get('flavor_data'),
)
return _get_sdk_object_dict(flavor_profile)
def update_member_list(request, **kwargs):
"""Update the list of members by adding or removing the necessary members.
@ -1183,3 +1265,134 @@ class HealthMonitor(generic.View):
"""
update_monitor(request)
@urls.register
class Flavors(generic.View):
"""API for load balancer flavors.
"""
url_regex = r'lbaas/flavors/$'
@rest_utils.ajax()
def get(self, request):
"""List of flavors for the current project.
The listing result is an object with property "items".
"""
conn = _get_sdk_connection(request)
flavor_list = _sdk_object_to_list(
conn.load_balancer.flavors()
)
return {'items': flavor_list}
@rest_utils.ajax()
def post(self, request):
"""Create a new flavor.
"""
kwargs = {
'flavor': request.DATA.get('flavor')
}
return create_flavor(request, **kwargs)
@urls.register
class Flavor(generic.View):
"""API for retrieving a single flavor.
"""
url_regex = r'lbaas/flavors/(?P<flavor_id>[^/]+)/$'
@rest_utils.ajax()
def get(self, request, flavor_id):
"""Get a specific flavor.
"""
conn = _get_sdk_connection(request)
flavor = conn.load_balancer.find_flavor(flavor_id)
return _get_sdk_object_dict(flavor)
@rest_utils.ajax()
def delete(self, request, flavor_id):
"""Delete a specific flavor.
http://localhost/api/lbaas/flavors/3971d368-ca9b-4770-929a-3adca5bf89eb
"""
conn = _get_sdk_connection(request)
conn.load_balancer.delete_flavor(flavor_id,
ignore_missing=True)
@rest_utils.ajax()
def put(self, request, flavor_id):
"""Edit a flavor.
"""
update_flavor(request)
@urls.register
class FlavorProfiles(generic.View):
"""API for load balancer flavor profiles.
"""
url_regex = r'lbaas/flavorprofiles/$'
@rest_utils.ajax()
def get(self, request):
"""List of flavor profiles for the current project.
The listing result is an object with property "items".
"""
conn = _get_sdk_connection(request)
flavor_profile_list = _sdk_object_to_list(
conn.load_balancer.flavor_profiles()
)
return {'items': flavor_profile_list}
@rest_utils.ajax()
def post(self, request):
"""Create a new flavor_profile.
"""
kwargs = {
'flavor_profile': request.DATA.get('flavor_profile')
}
return create_flavor_profile(request, **kwargs)
@urls.register
class FlavorProfile(generic.View):
"""API for retrieving a single flavor profile.
"""
url_regex = r'lbaas/flavorprofiles/(?P<flavor_profile_id>[^/]+)/$'
@rest_utils.ajax()
def get(self, request, flavor_profile_id):
"""Get a specific flavor profile.
"""
conn = _get_sdk_connection(request)
flavor_profile = conn.load_balancer.find_flavor_profile(
flavor_profile_id)
return _get_sdk_object_dict(flavor_profile)
@rest_utils.ajax()
def delete(self, request, flavor_profile_id):
"""Delete a specific flavor profile.
http://localhost/api/lbaas/flavorprofiles/e8150eab-aefa-42cc-867e-3fb336da52bd
"""
conn = _get_sdk_connection(request)
conn.load_balancer.delete_flavor_profile(flavor_profile_id,
ignore_missing=True)
@rest_utils.ajax()
def put(self, request, flavor_profile_id):
"""Edit a flavor profile.
"""
update_flavor_profile(request)

View File

@ -70,7 +70,17 @@
deleteHealthMonitor: deleteHealthMonitor,
createHealthMonitor: createHealthMonitor,
editHealthMonitor: editHealthMonitor,
updateMemberList: updateMemberList
updateMemberList: updateMemberList,
getFlavors: getFlavors,
getFlavor: getFlavor,
deleteFlavor: deleteFlavor,
createFlavor: createFlavor,
editFlavor: editFlavor,
getFlavorProfiles: getFlavorProfiles,
getFlavorProfile: getFlavorProfile,
deleteFlavorProfile: deleteFlavorProfile,
createFlavorProfile: createFlavorProfile,
editFlavorProfile: editFlavorProfile
};
return service;
@ -721,5 +731,169 @@
});
}
// Flavors
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.getFlavor
* @description
* Get a single load balancer flavor by ID.
* @param {string} flavorId
* Specifies the id of the flavor.
*/
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.getFlavors
* @description
* Get the list of flavors.
*
* The listing result is an object with property "items". Each item is
* a flavor.
*/
function getFlavors() {
var params = {params: {}};
return apiService.get('/api/lbaas/flavors/', params)
.error(function () {
toastService.add('error', gettext('Unable to retrieve flavors.'));
});
}
function getFlavor(flavorId) {
return apiService.get('/api/lbaas/flavors/' + flavorId + '/')
.error(function () {
toastService.add('error', gettext('Unable to retrieve flavor.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.editFlavor
* @description
* Edit a flavor
* @param {string} id
* Specifies the id of the flavor to update.
* @param {object} spec
* Specifies the data used to update the flavor.
*/
function editFlavor(id, spec) {
return apiService.put('/api/lbaas/flavors/' + id + '/', spec)
.error(function () {
toastService.add('error', gettext('Unable to update flavor.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.deleteFlavor
* @description
* Delete a single flavor by ID
* @param {string} id
* Specifies the id of the flavor to delete.
* @param {boolean} quiet
*/
function deleteFlavor(id, quiet) {
var promise = apiService.delete('/api/lbaas/flavors/' + id + '/');
return quiet ? promise : promise.error(function () {
toastService.add('error', gettext('Unable to delete flavor.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.createFlavor
* @description
* Create a new flavor
* @param {object} spec
* Specifies the data used to create the new flavor.
*/
function createFlavor(spec) {
return apiService.post('/api/lbaas/flavors/', spec)
.error(function () {
toastService.add('error', gettext('Unable to create flavor.'));
});
}
// Flavor Profiles
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.getFlavorProfile
* @description
* Get a single load balancer flavor profile by ID.
* @param {string} flavorProfileId
* Specifies the id of the flavor profile.
*/
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.getFlavorProfiles
* @description
* Get the list of flavor profiles.
*
* The listing result is an object with property "items". Each item is
* a flavor profile.
*/
function getFlavorProfiles() {
var params = {params: {}};
return apiService.get('/api/lbaas/flavorprofiles/', params)
.error(function () {
toastService.add('error', gettext('Unable to retrieve flavor profiles.'));
});
}
function getFlavorProfile(flavorProfileId) {
return apiService.get('/api/lbaas/flavorprofiles/' + flavorProfileId + '/')
.error(function () {
toastService.add('error', gettext('Unable to retrieve flavor profile.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.editFlavorProfile
* @description
* Edit a flavor Profile
* @param {string} id
* Specifies the id of the flavor profile to update.
* @param {object} spec
* Specifies the data used to update the flavor profile.
*/
function editFlavorProfile(id, spec) {
return apiService.put('/api/lbaas/flavorprofiles/' + id + '/', spec)
.error(function () {
toastService.add('error', gettext('Unable to update flavor profile.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.deleteFlavorProfile
* @description
* Delete a single flavor profile by ID
* @param {string} id
* Specifies the id of the flavor profile to delete.
* @param {boolean} quiet
*/
function deleteFlavorProfile(id, quiet) {
var promise = apiService.delete('/api/lbaas/flavorprofiles/' + id + '/');
return quiet ? promise : promise.error(function () {
toastService.add('error', gettext('Unable to delete flavor profile.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.createFlavorProfile
* @description
* Create a new flavor profile
* @param {object} spec
* Specifies the data used to create the new flavor profile.
*/
function createFlavorProfile(spec) {
return apiService.post('/api/lbaas/flavorprofiles/', spec)
.error(function () {
toastService.add('error', gettext('Unable to create flavor profile.'));
});
}
}
}());

View File

@ -269,6 +269,66 @@
error: 'Unable to delete health monitor.',
testInput: [ '1234' ]
},
{
func: 'getFlavors',
method: 'get',
path: '/api/lbaas/flavors/',
error: 'Unable to retrieve flavors.',
testInput: [],
data: { params: {} }
},
{
func: 'getFlavor',
method: 'get',
path: '/api/lbaas/flavors/1234/',
error: 'Unable to retrieve flavor.',
testInput: [ '1234' ]
},
{
func: 'editFlavor',
method: 'put',
path: '/api/lbaas/flavors/1234/',
error: 'Unable to update flavor.',
data: { name: 'flavor-1' },
testInput: [ '1234', { name: 'flavor-1' } ]
},
{
func: 'deleteFlavor',
method: 'delete',
path: '/api/lbaas/flavors/1234/',
error: 'Unable to delete flavor.',
testInput: [ '1234' ]
},
{
func: 'getFlavorProfiles',
method: 'get',
path: '/api/lbaas/flavorprofiles/',
error: 'Unable to retrieve flavor profiles.',
testInput: [],
data: { params: {} }
},
{
func: 'getFlavorProfile',
method: 'get',
path: '/api/lbaas/flavorprofiles/1234/',
error: 'Unable to retrieve flavor profile.',
testInput: [ '1234' ]
},
{
func: 'editFlavorProfile',
method: 'put',
path: '/api/lbaas/flavorprofiles/1234/',
error: 'Unable to update flavor profile.',
data: { name: 'flavorprofile-1' },
testInput: [ '1234', { name: 'flavorprofile-1' } ]
},
{
func: 'deleteFlavorProfile',
method: 'delete',
path: '/api/lbaas/flavorprofiles/1234/',
error: 'Unable to delete flavor profile.',
testInput: [ '1234' ]
},
{
func: 'createLoadBalancer',
method: 'post',
@ -364,6 +424,22 @@
data: { name: 'healthmonitor-1' },
testInput: [ { name: 'healthmonitor-1' } ]
},
{
func: 'createFlavor',
method: 'post',
path: '/api/lbaas/flavors/',
error: 'Unable to create flavor.',
data: { name: 'flavor-1' },
testInput: [ { name: 'flavor-1' } ]
},
{
func: 'createFlavorProfile',
method: 'post',
path: '/api/lbaas/flavorprofiles/',
error: 'Unable to create flavor profile.',
data: { name: 'flavorprofile-1' },
testInput: [ { name: 'flavorprofile-1' } ]
},
{
func: 'updateMemberList',
method: 'put',
@ -412,6 +488,16 @@
expect(service.deleteHealthMonitor("whatever", true)).toBe("promise");
});
it('supresses the error if instructed for deleteFlavor', function() {
spyOn(apiService, 'delete').and.returnValue("promise");
expect(service.deleteFlavor("whatever", true)).toBe("promise");
});
it('supresses the error if instructed for deleteFlavorProfile', function() {
spyOn(apiService, 'delete').and.returnValue("promise");
expect(service.deleteFlavorProfile("whatever", true)).toBe("promise");
});
});
})();

View File

@ -46,7 +46,7 @@
item="ctrl.loadbalancer"
property-groups="[[
'id', 'name', 'description', 'project_id', 'created_at', 'updated_at',
'vip_port_id', 'vip_subnet_id', 'vip_network_id', 'provider', 'flavor',
'vip_port_id', 'vip_subnet_id', 'vip_network_id', 'provider', 'flavor_id',
'floating_ip_address']]">
</hz-resource-property-list>
</div>

View File

@ -5,5 +5,5 @@
['name', 'id', 'project_id'],
['created_at', 'updated_at', 'description'],
['vip_network_id', 'vip_subnet_id', 'vip_port_id'],
['provider', 'floating_ip_address']]">
['flavor_id', 'provider', 'floating_ip_address']]">
</hz-resource-property-list>

View File

@ -178,8 +178,8 @@
listeners: gettext('Listeners'),
pools: gettext('Pools'),
provider: gettext('Provider'),
flavor: {
label: gettext('Flavor'),
flavor_id: {
label: gettext('Flavor ID'),
filters: ['noValue']
},
floating_ip_address: {

View File

@ -87,10 +87,52 @@
}
};
// Defines columns for the flavor selection filtered pop-up
ctrl.flavorColumns = [{
label: gettext('Flavor'),
value: 'name'
}, {
label: gettext('Flavor ID'),
value: 'id'
}, {
label: gettext('Flavor Description'),
value: 'description'
}, {
label: gettext('Provider'),
value: function(flavor) {
var flavorProfile = $scope.model.flavorProfiles[flavor.flavor_profile_id];
return flavorProfile ? flavorProfile.provider_name : '';
}
}];
ctrl.flavorOptions = [];
ctrl.flavorShorthand = function(flavor) {
var flavorProfile = $scope.model.flavorProfiles[flavor.flavor_profile_id];
var providerText =
flavorProfile
? flavorProfile.provider_name
: '';
var flavorText = flavor.name || flavor.id.substring(0, 10) + '...';
var flavorDescription = flavor.description || '';
return flavorText + ' (' + providerText + '): ' + flavorDescription;
};
ctrl.setFlavor = function(option) {
if (option) {
$scope.model.spec.loadbalancer.flavor_id = option;
} else {
$scope.model.spec.loadbalancer.flavor_id = null;
}
};
ctrl.dataLoaded = false;
ctrl._checkLoaded = function() {
if ($scope.model.initialized) {
ctrl.buildSubnetOptions();
ctrl.buildFlavorOptions();
ctrl.dataLoaded = true;
}
};
@ -112,6 +154,12 @@
$scope.$watchCollection('model.networks', function() {
ctrl._checkLoaded();
});
$scope.$watchCollection('model.flavors', function() {
ctrl._checkLoaded();
});
$scope.$watchCollection('model.flavorProfiles', function() {
ctrl._checkLoaded();
});
$scope.$watch('model.initialized', function() {
ctrl._checkLoaded();
});
@ -121,5 +169,13 @@
// Subnets are sliced to maintain data immutability
ctrl.subnetOptions = $scope.model.subnets.slice(0);
};
ctrl.buildFlavorOptions = function() {
ctrl.flavorOptions = Object.keys($scope.model.flavors).filter(function(key) {
return $scope.model.flavors[key].is_enabled;
}).map(function(key) {
return $scope.model.flavors[key];
});
};
}
})();

View File

@ -22,7 +22,7 @@
beforeEach(module('horizon.dashboard.project.lbaasv2'));
describe('LoadBalancerDetailsController', function() {
var ctrl, scope, mockSubnets;
var ctrl, scope, mockSubnets, mockFlavors;
beforeEach(inject(function($controller, $rootScope) {
mockSubnets = [{
id: '7262744a-e1e4-40d7-8833-18193e8de191',
@ -41,6 +41,24 @@
cidr: '2.2.2.2/16'
}];
mockFlavors = [{
id: '15d990e1-3438-4073-89b8-6e4706f0b176',
flavor_profile_id: '79e16118-daba-4255-9d1d-9cc7812e18a1',
name: 'flavor_1',
description: 'Flavor 1 description',
is_enabled: true
}, {
id: 'b0703ed4-dd30-4dbe-92bb-dccc945365e9',
flavor_profile_id: 'ace487e6-9946-4bdc-882e-c889af43fc3b',
name: 'flavor_2',
is_enabled: true
}, {
id: '94306089-567a-44ed-ab16-87653adbece3',
flavor_profile_id: 'b272d5fb-0021-4002-beb5-db758e59a763',
name: '',
is_enabled: true
}];
scope = $rootScope.$new();
scope.model = {
networks: {
@ -49,6 +67,21 @@
name: 'network_1'
}
},
flavors: {
'15d990e1-3438-4073-89b8-6e4706f0b176': {
id: '15d990e1-3438-4073-89b8-6e4706f0b176',
name: 'flavor_1',
flavor_profile_id: '79e16118-daba-4255-9d1d-9cc7812e18a1',
is_enabled: true
}
},
flavorProfiles: {
'79e16118-daba-4255-9d1d-9cc7812e18a1': {
id: '79e16118-daba-4255-9d1d-9cc7812e18a1',
name: 'flavor_profile_1',
provider_name: 'amphora'
}
},
subnets: [{}, {}],
spec: {
loadbalancer: {
@ -61,6 +94,7 @@
ctrl = $controller('LoadBalancerDetailsController', {$scope: scope});
spyOn(ctrl, 'buildSubnetOptions').and.callThrough();
spyOn(ctrl, 'buildFlavorOptions').and.callThrough();
spyOn(ctrl, '_checkLoaded').and.callThrough();
}));
@ -87,6 +121,18 @@
);
});
it('should create flavor shorthand text', function() {
expect(ctrl.flavorShorthand(mockFlavors[0])).toBe(
'flavor_1 (amphora): Flavor 1 description'
);
expect(ctrl.flavorShorthand(mockFlavors[1])).toBe(
'flavor_2 (): '
);
expect(ctrl.flavorShorthand(mockFlavors[2])).toBe(
'94306089-5... (): '
);
});
it('should set subnet', function() {
ctrl.setSubnet(mockSubnets[0]);
expect(scope.model.spec.loadbalancer.vip_subnet_id).toBe(mockSubnets[0]);
@ -94,6 +140,13 @@
expect(scope.model.spec.loadbalancer.vip_subnet_id).toBe(null);
});
it('should set flavor', function() {
ctrl.setFlavor(mockFlavors[0]);
expect(scope.model.spec.loadbalancer.flavor_id).toBe(mockFlavors[0]);
ctrl.setFlavor(null);
expect(scope.model.spec.loadbalancer.flavor_id).toBe(null);
});
it('should initialize watchers', function() {
ctrl.$onInit();
@ -105,6 +158,14 @@
scope.$apply();
expect(ctrl._checkLoaded).toHaveBeenCalled();
scope.model.flavors = {};
scope.$apply();
expect(ctrl._checkLoaded).toHaveBeenCalled();
scope.model.flavorProfiles = {};
scope.$apply();
expect(ctrl._checkLoaded).toHaveBeenCalled();
scope.model.initialized = true;
scope.$apply();
@ -158,6 +219,34 @@
expect(ctrl.buildSubnetOptions).toHaveBeenCalled();
expect(ctrl.dataLoaded).toBe(true);
});
it('should initialize flavors watcher', function() {
ctrl.$onInit();
scope.model.flavors = {};
scope.$apply();
//expect(ctrl.buildSubnetOptions).toHaveBeenCalled();
});
it('should produce flavor column data', function() {
expect(ctrl.flavorColumns).toBeDefined();
expect(ctrl.flavorColumns[0].label).toBe('Flavor');
expect(ctrl.flavorColumns[0].value).toBe('name');
expect(ctrl.flavorColumns[1].label).toBe('Flavor ID');
expect(ctrl.flavorColumns[1].value).toBe('id');
expect(ctrl.flavorColumns[2].label).toBe('Flavor Description');
expect(ctrl.flavorColumns[2].value).toBe('description');
expect(ctrl.flavorColumns[3].label).toBe('Provider');
var flavorLabel3 = ctrl.flavorColumns[3].value(mockFlavors[0]);
expect(flavorLabel3).toBe('amphora');
flavorLabel3 = ctrl.flavorColumns[3].value(mockFlavors[1]);
expect(flavorLabel3).toBe('');
});
});
});
})();

View File

@ -35,6 +35,28 @@
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12">
<div class="form-group">
<label class="control-label">
<translate>Flavor</translate>
</label>
<!-- value="model.spec.loadbalancer.flavor_id" -->
<filter-select
shorthand="ctrl.flavorShorthand"
on-select="ctrl.setFlavor(option)"
disabled="model.context.id"
columns="ctrl.flavorColumns"
options="ctrl.flavorOptions"
loaded="ctrl.dataLoaded"
ng-model="model.spec.loadbalancer.flavor_id"
></filter-select>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12">
<div class="form-group">

View File

@ -85,6 +85,8 @@
subnets: [],
members: [],
networks: {},
flavors: {},
flavorProfiles: {},
listenerProtocols: ['HTTP', 'TCP', 'TERMINATED_HTTPS', 'HTTPS'],
l7policyActions: ['REJECT', 'REDIRECT_TO_URL', 'REDIRECT_TO_POOL'],
l7ruleTypes: ['HOST_NAME', 'PATH', 'FILE_TYPE', 'HEADER', 'COOKIE'],
@ -149,6 +151,7 @@
description: null,
vip_address: null,
vip_subnet_id: null,
flavor_id: null,
admin_state_up: true
},
listener: {
@ -263,6 +266,8 @@
function initCreateLoadBalancer(keymanagerPromise) {
model.context.submit = createLoadBalancer;
return $q.all([
lbaasv2API.getFlavors().then(onGetFlavors),
lbaasv2API.getFlavorProfiles().then(onGetFlavorProfiles),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
neutronAPI.getNetworks().then(onGetNetworks),
@ -277,6 +282,18 @@
});
}
function onGetFlavors(response) {
angular.forEach(response.data.items, function(value) {
model.flavors[value.id] = value;
});
}
function onGetFlavorProfiles(response) {
angular.forEach(response.data.items, function(value) {
model.flavorProfiles[value.id] = value;
});
}
function initCreateListener(keymanagerPromise) {
model.context.submit = createListener;
return $q.all([
@ -337,10 +354,12 @@
function initEditLoadBalancer() {
model.context.submit = editLoadBalancer;
return $q.all([
lbaasv2API.getFlavors().then(onGetFlavors),
lbaasv2API.getFlavorProfiles().then(onGetFlavorProfiles),
lbaasv2API.getLoadBalancer(model.context.id).then(onGetLoadBalancer),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getNetworks().then(onGetNetworks)
]).then(initSubnet);
]).then(initSubnet).then(initFlavor);
}
function initEditListener() {
@ -457,6 +476,10 @@
function cleanFinalSpecLoadBalancer(finalSpec) {
var context = model.context;
if (angular.isObject(finalSpec.loadbalancer.flavor_id)) {
finalSpec.loadbalancer.flavor_id = finalSpec.loadbalancer.flavor_id.id;
}
// Load balancer requires vip_subnet_id
if (!finalSpec.loadbalancer.vip_subnet_id) {
delete finalSpec.loadbalancer;
@ -466,6 +489,7 @@
// Cannot edit the IP or subnet
if (context.resource === 'loadbalancer' && context.id) {
delete finalSpec.flavor_id;
delete finalSpec.vip_subnet_id;
delete finalSpec.vip_address;
}
@ -751,6 +775,7 @@
spec.description = loadbalancer.description;
spec.vip_address = loadbalancer.vip_address;
spec.vip_subnet_id = loadbalancer.vip_subnet_id;
spec.flavor_id = loadbalancer.flavor_id;
spec.admin_state_up = loadbalancer.admin_state_up;
}
@ -892,6 +917,10 @@
model.spec.loadbalancer.vip_subnet_id = subnet;
}
function initFlavor() {
model.spec.loadbalancer.flavor_id = model.flavors[model.spec.loadbalancer.flavor_id];
}
function mapSubnetObj(subnetId) {
var subnet = model.subnets.filter(function mapSubnet(subnet) {
return subnet.id === subnetId;

View File

@ -18,7 +18,7 @@
describe('LBaaS v2 Workflow Model Service', function() {
var model, $q, scope, listenerResources, barbicanEnabled,
certificatesError, mockNetworks;
certificatesError, mockNetworks, mockFlavors, mockFlavorProfiles;
var includeChildResources = true;
beforeEach(module('horizon.framework.util.i18n'));
@ -95,6 +95,26 @@
id: 'b2'
}
};
mockFlavors = {
f1: {
name: 'flavor_1',
id: 'f1'
},
f2: {
name: 'flavor_2',
id: 'f2'
}
};
mockFlavorProfiles = {
p1: {
name: 'flavor_profile_1',
id: 'p1'
},
p2: {
name: 'flavor_profile_2',
id: 'p2'
}
};
});
beforeEach(module(function($provide) {
@ -118,6 +138,7 @@
name: 'Load Balancer 1',
vip_address: '1.2.3.4',
vip_subnet_id: 'subnet-1',
flavor_id: 'flavor-1',
description: ''
};
@ -242,6 +263,32 @@
return deferred.promise;
},
getFlavors: function() {
var flavors = [{
name: 'flavor_1',
id: 'f1'
}, {
name: 'flavor_2',
id: 'f2'
}];
var deferred = $q.defer();
deferred.resolve({data: {items: flavors}});
return deferred.promise;
},
getFlavorProfiles: function() {
var flavorProfiles = [{
name: 'flavor_profile_1',
id: 'p1'
}, {
name: 'flavor_profile_2',
id: 'p2'
}];
var deferred = $q.defer();
deferred.resolve({data: {items: flavorProfiles}});
return deferred.promise;
},
createLoadBalancer: function(spec) {
return spec;
},
@ -486,6 +533,8 @@
expect(model.initialized).toBe(true);
expect(model.subnets.length).toBe(2);
expect(model.networks).toEqual(mockNetworks);
expect(model.flavors).toEqual(mockFlavors);
expect(model.flavorProfiles).toEqual(mockFlavorProfiles);
expect(model.members.length).toBe(2);
expect(model.certificates.length).toBe(2);
expect(model.listenerPorts.length).toBe(0);
@ -742,6 +791,8 @@
expect(model.initialized).toBe(true);
expect(model.subnets.length).toBe(2);
expect(model.networks).toEqual(mockNetworks);
expect(model.flavors).toEqual(mockFlavors);
expect(model.flavorProfiles).toEqual(mockFlavorProfiles);
expect(model.members.length).toBe(0);
expect(model.certificates.length).toBe(0);
expect(model.listenerPorts.length).toBe(0);
@ -1238,7 +1289,7 @@
// to implement tests for them.
it('has the right number of properties', function() {
expect(Object.keys(model.spec).length).toBe(11);
expect(Object.keys(model.spec.loadbalancer).length).toBe(5);
expect(Object.keys(model.spec.loadbalancer).length).toBe(6);
expect(Object.keys(model.spec.listener).length).toBe(14);
expect(Object.keys(model.spec.l7policy).length).toBe(8);
expect(Object.keys(model.spec.l7rule).length).toBe(7);
@ -1486,6 +1537,7 @@
it('should set final spec properties', function() {
model.spec.loadbalancer.vip_address = '1.2.3.4';
model.spec.loadbalancer.vip_subnet_id = model.subnets[0];
model.spec.loadbalancer.flavor_id = model.flavors[Object.keys(model.flavors)[0]];
model.spec.listener.protocol = 'TCP';
model.spec.listener.protocol_port = 80;
model.spec.listener.connection_limit = 999;
@ -1587,6 +1639,7 @@
it('should set final spec certificates', function() {
model.spec.loadbalancer.vip_address = '1.2.3.4';
model.spec.loadbalancer.vip_subnet_id = model.subnets[0];
model.spec.loadbalancer.flavor_id = model.flavors[Object.keys(model.flavors)[0]];
model.spec.listener.protocol = 'TERMINATED_HTTPS';
model.spec.listener.protocol_port = 443;
model.spec.listener.connection_limit = 9999;

View File

@ -0,0 +1,7 @@
---
features:
- |
Add load balancer flavor support.
issues:
- |
The octavia-dashboard requires openstacksdk > 0.24.0 for flavor support.

View File

@ -4,7 +4,7 @@
horizon>=14.0.0.0b3 # Apache-2.0
Babel!=2.4.0,>=2.3.4 # BSD
openstacksdk>=0.11.2 # Apache-2.0
openstacksdk>=0.24.0 # Apache-2.0
oslo.log>=3.36.0 # Apache-2.0
pbr!=2.1.0,>=2.0.0 # Apache-2.0
python-barbicanclient>=4.5.2 # Apache-2.0