diff --git a/lower-constraints.txt b/lower-constraints.txt index 332ce2aa..3c321c4a 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -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 diff --git a/octavia_dashboard/api/rest/lbaasv2.py b/octavia_dashboard/api/rest/lbaasv2.py index b47408e9..eba4b372 100644 --- a/octavia_dashboard/api/rest/lbaasv2.py +++ b/octavia_dashboard/api/rest/lbaasv2.py @@ -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[^/]+)/$' + + @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[^/]+)/$' + + @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) 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 ae2d410c..390c71ef 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 @@ -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.')); + }); + } + } }()); 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 4a6e4e86..fd9d826f 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 @@ -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"); + }); + }); })(); 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 41ed0001..415d84c1 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/details/detail.html +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/details/detail.html @@ -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']]"> diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/details/drawer.html b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/details/drawer.html index c94a628b..9f540092 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/details/drawer.html +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/details/drawer.html @@ -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']]"> 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 b39cd9fd..ccea4691 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/loadbalancers.module.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/loadbalancers.module.js @@ -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: { 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 b56cf964..aec12e4f 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 @@ -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]; + }); + }; } })(); 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 0daa74a9..1c0a0c15 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; + 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(''); + }); + }); }); })(); 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 4bc0b005..64a5940a 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 0f7a4e07..5a450428 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/workflow/model.service.js @@ -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; 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 d07d5b86..ae1a18d2 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,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; diff --git a/releasenotes/notes/flavor-support-0195a486faa16b7f.yaml b/releasenotes/notes/flavor-support-0195a486faa16b7f.yaml new file mode 100644 index 00000000..4c386c85 --- /dev/null +++ b/releasenotes/notes/flavor-support-0195a486faa16b7f.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add load balancer flavor support. +issues: + - | + The octavia-dashboard requires openstacksdk > 0.24.0 for flavor support. diff --git a/requirements.txt b/requirements.txt index 0495f9ab..c953f1f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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