Availability zone support

Ability to create a LB in a specific AZ and see AZ attribute
of load balancers.

Depends-On: https://review.opendev.org/#/c/714345/
Change-Id: Id4457d164e6899ffe4b9e4a0332b771d0ee33770
This commit is contained in:
Sam Morrison 2020-03-24 11:11:27 +11:00
parent b456fecd76
commit 761408e3b8
11 changed files with 254 additions and 25 deletions

View File

@ -126,29 +126,23 @@ 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)
build_kwargs = dict(
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 = data['loadbalancer'].get('flavor_id')
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'),
)
build_kwargs['flavor_id'] = flavor_id
availability_zone = data['loadbalancer'].get('availability_zone')
if availability_zone:
build_kwargs['availability_zone'] = availability_zone
loadbalancer = conn.load_balancer.create_load_balancer(**build_kwargs)
if data.get('listener'):
# There is work underway to add a new API to LBaaS v2 that will
# allow us to pass in all information at once. Until that is
@ -1396,3 +1390,24 @@ class FlavorProfile(generic.View):
"""
update_flavor_profile(request)
@urls.register
class AvailabilityZones(generic.View):
"""API for load balancer availability zones.
"""
url_regex = r'lbaas/availabilityzones/$'
@rest_utils.ajax()
def get(self, request):
"""List of availability zones for the current project.
The listing result is an object with property "items".
"""
conn = _get_sdk_connection(request)
availability_zone_list = _sdk_object_to_list(
conn.load_balancer.availability_zones()
)
return {'items': availability_zone_list}

View File

@ -71,6 +71,7 @@
createHealthMonitor: createHealthMonitor,
editHealthMonitor: editHealthMonitor,
updateMemberList: updateMemberList,
getAvailabilityZones: getAvailabilityZones,
getFlavors: getFlavors,
getFlavor: getFlavor,
deleteFlavor: deleteFlavor,
@ -895,5 +896,24 @@
});
}
// Availability Zones
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.getAvailabilityZones
* @description
* Get the list of availability zones.
*
* The listing result is an object with property "items". Each item is
* an availability zone.
*/
function getAvailabilityZones() {
var params = {params: {}};
return apiService.get('/api/lbaas/availabilityzones/', params)
.error(function () {
toastService.add('error', gettext('Unable to retrieve availability zones.'));
});
}
}
}());

View File

@ -329,6 +329,14 @@
error: 'Unable to delete flavor profile.',
testInput: [ '1234' ]
},
{
func: 'getAvailabilityZones',
method: 'get',
path: '/api/lbaas/availabilityzones/',
error: 'Unable to retrieve availability zones.',
testInput: [],
data: { params: {} }
},
{
func: 'createLoadBalancer',
method: 'post',

View File

@ -12,6 +12,10 @@
<strong translate>IP Address</strong>
{$ ::ctrl.loadbalancer.vip_address $}
</li>
<li>
<strong translate>Availability Zone</strong>
{$ ::ctrl.loadbalancer.availability_zone $}
</li>
<li>
<strong translate>Operating Status</strong>
{$ ctrl.loadbalancer.operating_status | decode:ctrl.operatingStatus $}
@ -47,7 +51,7 @@
property-groups="[[
'id', 'name', 'description', 'project_id', 'created_at', 'updated_at',
'vip_port_id', 'vip_subnet_id', 'vip_network_id', 'provider', 'flavor_id',
'floating_ip_address']]">
'availability_zone', 'floating_ip_address']]">
</hz-resource-property-list>
</div>
</uib-tab>

View File

@ -74,6 +74,10 @@
id: 'vip_address',
priority: 1
})
.append({
id: 'availability_zone',
priority: 1
})
.append({
id: 'operating_status',
priority: 1
@ -178,6 +182,10 @@
listeners: gettext('Listeners'),
pools: gettext('Pools'),
provider: gettext('Provider'),
availability_zone: {
label: gettext('Availability Zone'),
filters: ['noValue']
},
flavor_id: {
label: gettext('Flavor ID'),
filters: ['noValue']

View File

@ -116,11 +116,32 @@
}
};
// Defines columns for the availability_zone selection filtered pop-up
ctrl.availabilityZoneColumns = [{
label: gettext('Availability Zone'),
value: 'name'
}];
ctrl.availabilityZoneOptions = [];
ctrl.availabilityZoneShorthand = function(availabilityZone) {
return availabilityZone.name;
};
ctrl.setAvailabilityZone = function(option) {
if (option) {
$scope.model.spec.loadbalancer.availability_zone = option.name;
} else {
$scope.model.spec.loadbalancer.availability_zone = null;
}
};
ctrl.dataLoaded = false;
ctrl._checkLoaded = function() {
if ($scope.model.initialized) {
ctrl.buildSubnetOptions();
ctrl.buildFlavorOptions();
ctrl.buildAvailabilityZoneOptions();
ctrl.dataLoaded = true;
}
};
@ -145,6 +166,9 @@
$scope.$watchCollection('model.flavors', function() {
ctrl._checkLoaded();
});
$scope.$watchCollection('model.availability_zones', function() {
ctrl._checkLoaded();
});
$scope.$watch('model.initialized', function() {
ctrl._checkLoaded();
});
@ -162,5 +186,13 @@
return $scope.model.flavors[key];
});
};
ctrl.buildAvailabilityZoneOptions = function() {
ctrl.availabilityZoneOptions = Object.keys(
$scope.model.availability_zones).filter(function(key) {
return $scope.model.availability_zones[key].is_enabled;
}).map(function(key) {
return $scope.model.availability_zones[key];
});
};
}
})();

View File

@ -22,7 +22,7 @@
beforeEach(module('horizon.dashboard.project.lbaasv2'));
describe('LoadBalancerDetailsController', function() {
var ctrl, scope, mockSubnets, mockFlavors;
var ctrl, scope, mockSubnets, mockFlavors, mockAvailabilityZones;
beforeEach(inject(function($controller, $rootScope) {
mockSubnets = [{
id: '7262744a-e1e4-40d7-8833-18193e8de191',
@ -59,6 +59,26 @@
is_enabled: true
}];
mockAvailabilityZones = [{
id: '11eddb23-1f01-4926-9af7-36d9a8938ae4',
availability_zone_profile_id: 'f44f46ee-5f19-4515-b930-b62c9649081d',
name: 'az_1',
description: 'AZ 1 description',
is_enabled: true
}, {
id: '2a83c5f3-c2e6-44cb-ac02-e06617c2b7ca',
availability_zone_profile_id: '52dacdb9-c20d-4f49-9c0a-a957befaf27a',
name: 'az_2',
description: 'AZ 2 description',
is_enabled: true
}, {
id: 'ff89a83c-3819-44e6-8383-f42d3a270f5f',
availability_zone_profile_id: '9fe93b65-85cc-4f86-a2d9-45f78eb909d0',
name: 'az_3',
description: 'AZ 3 description',
is_enabled: true
}];
scope = $rootScope.$new();
scope.model = {
networks: {
@ -75,6 +95,15 @@
is_enabled: true
}
},
availability_zones: {
'11eddb23-1f01-4926-9af7-36d9a8938ae4': {
id: '11eddb23-1f01-4926-9af7-36d9a8938ae4',
availability_zone_profile_id: 'f44f46ee-5f19-4515-b930-b62c9649081d',
name: 'az_1',
description: 'AZ 1 description',
is_enabled: true
}
},
subnets: [{}, {}],
spec: {
loadbalancer: {
@ -88,6 +117,7 @@
spyOn(ctrl, 'buildSubnetOptions').and.callThrough();
spyOn(ctrl, 'buildFlavorOptions').and.callThrough();
spyOn(ctrl, 'buildAvailabilityZoneOptions').and.callThrough();
spyOn(ctrl, '_checkLoaded').and.callThrough();
}));
@ -126,6 +156,18 @@
);
});
it('should create az shorthand text', function() {
expect(ctrl.availabilityZoneShorthand(mockAvailabilityZones[0])).toBe(
'az_1'
);
expect(ctrl.availabilityZoneShorthand(mockAvailabilityZones[1])).toBe(
'az_2'
);
expect(ctrl.availabilityZoneShorthand(mockAvailabilityZones[2])).toBe(
'az_3'
);
});
it('should set subnet', function() {
ctrl.setSubnet(mockSubnets[0]);
expect(scope.model.spec.loadbalancer.vip_subnet_id).toBe(mockSubnets[0]);
@ -140,6 +182,13 @@
expect(scope.model.spec.loadbalancer.flavor_id).toBe(null);
});
it('should set availability zone', function() {
ctrl.setAvailabilityZone(mockAvailabilityZones[0]);
expect(scope.model.spec.loadbalancer.availability_zone).toBe(mockAvailabilityZones[0].name);
ctrl.setAvailabilityZone(null);
expect(scope.model.spec.loadbalancer.availability_zone).toBe(null);
});
it('should initialize watchers', function() {
ctrl.$onInit();
@ -155,6 +204,10 @@
scope.$apply();
expect(ctrl._checkLoaded).toHaveBeenCalled();
scope.model.availability_zones = {};
scope.$apply();
expect(ctrl._checkLoaded).toHaveBeenCalled();
scope.model.initialized = true;
scope.$apply();
@ -217,6 +270,14 @@
//expect(ctrl.buildSubnetOptions).toHaveBeenCalled();
});
it('should initialize availability zone watcher', function() {
ctrl.$onInit();
scope.model.availability_zones = {};
scope.$apply();
//expect(ctrl.buildSubnetOptions).toHaveBeenCalled();
});
it('should produce flavor column data', function() {
expect(ctrl.flavorColumns).toBeDefined();
@ -230,6 +291,13 @@
expect(ctrl.flavorColumns[2].value).toBe('description');
});
it('should produce availability zone column data', function() {
expect(ctrl.availabilityZoneColumns).toBeDefined();
expect(ctrl.availabilityZoneColumns[0].label).toBe('Availability Zone');
expect(ctrl.availabilityZoneColumns[0].value).toBe('name');
});
});
});
})();

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>Availability Zone</translate>
</label>
<!-- value="model.spec.loadbalancer.availability_zone" -->
<filter-select
shorthand="ctrl.availabilityZoneShorthand"
on-select="ctrl.setAvailabilityZone(option)"
disabled="model.context.id"
columns="ctrl.availabilityZoneColumns"
options="ctrl.availabilityZoneOptions"
loaded="ctrl.dataLoaded"
ng-model="model.spec.loadbalancer.availability_zone"
></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

@ -87,6 +87,7 @@
networks: {},
flavors: {},
listenerProtocols: ['HTTP', 'TCP', 'TERMINATED_HTTPS', 'HTTPS', 'UDP'],
availability_zones: {},
l7policyActions: ['REJECT', 'REDIRECT_TO_URL', 'REDIRECT_TO_POOL'],
l7ruleTypes: ['HOST_NAME', 'PATH', 'FILE_TYPE', 'HEADER', 'COOKIE'],
l7ruleCompareTypes: ['REGEX', 'EQUAL_TO', 'STARTS_WITH', 'ENDS_WITH', 'CONTAINS'],
@ -151,6 +152,7 @@
vip_address: null,
vip_subnet_id: null,
flavor_id: null,
availability_zone: null,
admin_state_up: true
},
listener: {
@ -266,6 +268,7 @@
model.context.submit = createLoadBalancer;
return $q.all([
lbaasv2API.getFlavors().then(onGetFlavors),
lbaasv2API.getAvailabilityZones().then(onGetAvailabilityZones),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
neutronAPI.getNetworks().then(onGetNetworks),
@ -286,6 +289,12 @@
});
}
function onGetAvailabilityZones(response) {
angular.forEach(response.data.items, function(value) {
model.availability_zones[value.name] = value;
});
}
function initCreateListener(keymanagerPromise) {
model.context.submit = createListener;
return $q.all([
@ -347,10 +356,11 @@
model.context.submit = editLoadBalancer;
return $q.all([
lbaasv2API.getFlavors().then(onGetFlavors),
lbaasv2API.getAvailabilityZones().then(onGetAvailabilityZones),
lbaasv2API.getLoadBalancer(model.context.id).then(onGetLoadBalancer),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getNetworks().then(onGetNetworks)
]).then(initSubnet).then(initFlavor);
]).then(initSubnet).then(initFlavor).then(initAvailabilityZone);
}
function initEditListener() {
@ -481,6 +491,7 @@
// Cannot edit the IP or subnet
if (context.resource === 'loadbalancer' && context.id) {
delete finalSpec.flavor_id;
delete finalSpec.availability_zone;
delete finalSpec.vip_subnet_id;
delete finalSpec.vip_address;
}
@ -767,6 +778,7 @@
spec.vip_address = loadbalancer.vip_address;
spec.vip_subnet_id = loadbalancer.vip_subnet_id;
spec.flavor_id = loadbalancer.flavor_id;
spec.availability_zone = loadbalancer.availability_zone;
spec.admin_state_up = loadbalancer.admin_state_up;
}
@ -929,6 +941,11 @@
model.spec.loadbalancer.flavor_id = model.flavors[model.spec.loadbalancer.flavor_id];
}
function initAvailabilityZone() {
model.spec.loadbalancer.availability_zone = model.availability_zones[
model.spec.loadbalancer.availability_zone];
}
function mapSubnetObj(subnetId) {
var subnet = model.subnets.filter(function mapSubnet(subnet) {
return subnet.id === subnetId;

View File

@ -18,7 +18,8 @@
describe('LBaaS v2 Workflow Model Service', function() {
var model, $q, scope, listenerResources, barbicanEnabled,
certificatesError, mockNetworks, mockFlavors;
certificatesError, mockNetworks, mockFlavors,
mockAvailabilityZones;
var includeChildResources = true;
beforeEach(module('horizon.framework.util.i18n'));
@ -105,6 +106,16 @@
id: 'f2'
}
};
mockAvailabilityZones = {
az_1: {
name: 'az_1',
id: 'az1'
},
az_2: {
name: 'az_2',
id: 'az2'
}
};
});
beforeEach(module(function($provide) {
@ -129,6 +140,7 @@
vip_address: '1.2.3.4',
vip_subnet_id: 'subnet-1',
flavor_id: 'flavor-1',
availability_zone: 'az-1',
description: ''
};
@ -266,6 +278,19 @@
deferred.resolve({data: {items: flavors}});
return deferred.promise;
},
getAvailabilityZones: function() {
var availabilityZones = [{
name: 'az_1',
id: 'az1'
}, {
name: 'az_2',
id: 'az2'
}];
var deferred = $q.defer();
deferred.resolve({data: {items: availabilityZones}});
return deferred.promise;
},
createLoadBalancer: function(spec) {
return spec;
},
@ -517,6 +542,7 @@
expect(model.subnets.length).toBe(2);
expect(model.networks).toEqual(mockNetworks);
expect(model.flavors).toEqual(mockFlavors);
expect(model.availability_zones).toEqual(mockAvailabilityZones);
expect(model.members.length).toBe(2);
expect(model.certificates.length).toBe(3);
expect(model.listenerPorts.length).toBe(0);
@ -774,6 +800,7 @@
expect(model.subnets.length).toBe(2);
expect(model.networks).toEqual(mockNetworks);
expect(model.flavors).toEqual(mockFlavors);
expect(model.availability_zones).toEqual(mockAvailabilityZones);
expect(model.members.length).toBe(0);
expect(model.certificates.length).toBe(0);
expect(model.listenerPorts.length).toBe(0);
@ -1270,7 +1297,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(6);
expect(Object.keys(model.spec.loadbalancer).length).toBe(7);
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);
@ -1519,6 +1546,8 @@
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.loadbalancer.availability_zone = model.availability_zones[
Object.keys(model.availability_zones)[0]];
model.spec.listener.protocol = 'TCP';
model.spec.listener.protocol_port = 80;
model.spec.listener.connection_limit = 999;
@ -1621,6 +1650,8 @@
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.loadbalancer.availability_zone = model.availability_zones[
Object.keys(model.availability_zones)[0]];
model.spec.listener.protocol = 'TERMINATED_HTTPS';
model.spec.listener.protocol_port = 443;
model.spec.listener.connection_limit = 9999;

View File

@ -0,0 +1,4 @@
---
features:
- |
Added support for availability zones. Can now create a LB in a specific AZ.