diff --git a/octavia_dashboard/api/rest/lbaasv2.py b/octavia_dashboard/api/rest/lbaasv2.py index c4444a01..a0c89b2f 100644 --- a/octavia_dashboard/api/rest/lbaasv2.py +++ b/octavia_dashboard/api/rest/lbaasv2.py @@ -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} diff --git a/octavia_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js b/octavia_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js index 390c71ef..71fdf63a 100644 --- a/octavia_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js +++ b/octavia_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js @@ -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.')); + }); + } + } }()); diff --git a/octavia_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js b/octavia_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js index fd9d826f..64d6d066 100644 --- a/octavia_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js +++ b/octavia_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js @@ -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', diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/details/detail.html b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/details/detail.html index 415d84c1..935d89b0 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/details/detail.html +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/details/detail.html @@ -12,6 +12,10 @@ IP Address {$ ::ctrl.loadbalancer.vip_address $} +
  • + Availability Zone + {$ ::ctrl.loadbalancer.availability_zone $} +
  • Operating Status {$ 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']]"> diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/loadbalancers.module.js b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/loadbalancers.module.js index ccea4691..570e4a89 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/loadbalancers.module.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/loadbalancers.module.js @@ -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'] diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.controller.js b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.controller.js index b1289ce7..40d67287 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.controller.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.controller.js @@ -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]; + }); + }; } })(); diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.controller.spec.js b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.controller.spec.js index 313a8fca..a3c3ad67 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.controller.spec.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.controller.spec.js @@ -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'); + }); + }); }); })(); diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.html b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.html index 64a5940a..a364ea70 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.html +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.html @@ -35,6 +35,28 @@ +
    +
    +
    + + + +
    +
    +
    +
    diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.js b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.js index 078bb331..3c5a4eaa 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.js @@ -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; diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.spec.js b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.spec.js index b3299f80..59cba731 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.spec.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.spec.js @@ -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; diff --git a/releasenotes/notes/add-az-support-efdd4e7c5dccef21.yaml b/releasenotes/notes/add-az-support-efdd4e7c5dccef21.yaml new file mode 100644 index 00000000..956d29b2 --- /dev/null +++ b/releasenotes/notes/add-az-support-efdd4e7c5dccef21.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added support for availability zones. Can now create a LB in a specific AZ.