Browse Source

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
tags/5.0.0.0rc1^0
Sam Morrison 3 months ago
parent
commit
761408e3b8
11 changed files with 254 additions and 25 deletions
  1. +35
    -20
      octavia_dashboard/api/rest/lbaasv2.py
  2. +20
    -0
      octavia_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js
  3. +8
    -0
      octavia_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js
  4. +5
    -1
      octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/details/detail.html
  5. +8
    -0
      octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/loadbalancers.module.js
  6. +32
    -0
      octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.controller.js
  7. +69
    -1
      octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.controller.spec.js
  8. +22
    -0
      octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.html
  9. +18
    -1
      octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.js
  10. +33
    -2
      octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.spec.js
  11. +4
    -0
      releasenotes/notes/add-az-support-efdd4e7c5dccef21.yaml

+ 35
- 20
octavia_dashboard/api/rest/lbaasv2.py 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}

+ 20
- 0
octavia_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js 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.'));
});
}

}
}());

+ 8
- 0
octavia_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js 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',


+ 5
- 1
octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/details/detail.html 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>


+ 8
- 0
octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/loadbalancers.module.js 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']


+ 32
- 0
octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.controller.js 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];
});
};
}
})();

+ 69
- 1
octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.controller.spec.js 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');
});

});
});
})();

+ 22
- 0
octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.html 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">


+ 18
- 1
octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.js 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;


+ 33
- 2
octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.spec.js 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;


+ 4
- 0
releasenotes/notes/add-az-support-efdd4e7c5dccef21.yaml View File

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

Loading…
Cancel
Save