Add Octavia (LBaaS) into quota management

* Turned off by default but can be configured

Change-Id: Ifa5a6e5b7a2c44cea5f799dc291c436eaa13d756
Implements: blueprint octavia-quota-service
This commit is contained in:
Amelia Cordwell 2017-12-15 14:26:08 +13:00 committed by Adrian Turjak
parent 7f33678642
commit 6508cc3804
8 changed files with 349 additions and 39 deletions

View File

@ -25,7 +25,7 @@ from adjutant.common.tests.utils import modify_dict_settings
from adjutant.common.tests.fake_clients import (
FakeManager, setup_identity_cache, get_fake_neutron, get_fake_novaclient,
get_fake_cinderclient, setup_neutron_cache, neutron_cache, cinder_cache,
nova_cache, setup_mock_caches)
nova_cache, setup_mock_caches, get_fake_octaviaclient, octavia_cache)
@mock.patch('adjutant.common.user_store.IdentityManager',
@ -35,13 +35,13 @@ from adjutant.common.tests.fake_clients import (
'openstack_clients.get_neutronclient',
get_fake_neutron)
@mock.patch(
'adjutant.common.quota.get_neutronclient',
'adjutant.common.openstack_clients.get_neutronclient',
get_fake_neutron)
@mock.patch(
'adjutant.common.quota.get_novaclient',
'adjutant.common.openstack_clients.get_novaclient',
get_fake_novaclient)
@mock.patch(
'adjutant.common.quota.get_cinderclient',
'adjutant.common.openstack_clients.get_cinderclient',
get_fake_cinderclient)
class ProjectSetupActionTests(TestCase):
@ -468,14 +468,17 @@ class ProjectSetupActionTests(TestCase):
'adjutant.common.user_store.IdentityManager',
FakeManager)
@mock.patch(
'adjutant.common.quota.get_neutronclient',
'adjutant.common.openstack_clients.get_neutronclient',
get_fake_neutron)
@mock.patch(
'adjutant.common.quota.get_novaclient',
'adjutant.common.openstack_clients.get_novaclient',
get_fake_novaclient)
@mock.patch(
'adjutant.common.quota.get_cinderclient',
'adjutant.common.openstack_clients.get_cinderclient',
get_fake_cinderclient)
@mock.patch(
'adjutant.common.openstack_clients.get_octaviaclient',
get_fake_octaviaclient)
class QuotaActionTests(TestCase):
def test_update_quota(self):
@ -639,3 +642,107 @@ class QuotaActionTests(TestCase):
self.assertEqual(novaquota['ram'], 655360)
neutronquota = neutron_cache['RegionTwo']['test_project_id']['quota']
self.assertEqual(neutronquota['network'], 10)
@modify_dict_settings(QUOTA_SERVICES={
'operation': 'append',
'key_list': ['*'],
'value': 'octavia'
})
def test_update_quota_octavia(self):
"""Tests the quota update of the octavia service"""
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_identity_cache(projects=[project], users=[user])
setup_mock_caches('RegionOne', project.id)
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
data = {
'project_id': 'test_project_id',
'size': 'large',
'domain_id': 'default',
'regions': ['RegionOne'],
}
action = UpdateProjectQuotasAction(data, task=task, order=1)
action.pre_approve()
self.assertEqual(action.valid, True)
action.post_approve()
self.assertEqual(action.valid, True)
# check the quotas were updated
# This relies on test_settings heavily.
cinderquota = cinder_cache['RegionOne']['test_project_id']['quota']
self.assertEqual(cinderquota['gigabytes'], 50000)
novaquota = nova_cache['RegionOne']['test_project_id']['quota']
self.assertEqual(novaquota['ram'], 655360)
neutronquota = neutron_cache['RegionOne']['test_project_id']['quota']
self.assertEqual(neutronquota['network'], 10)
octaviaquota = octavia_cache['RegionOne']['test_project_id']['quota']
self.assertEqual(octaviaquota['load_balancer'], 10)
@modify_dict_settings(QUOTA_SERVICES={
'operation': 'append',
'key_list': ['*'],
'value': 'octavia'
})
def test_update_quota_octavia_over_usage(self):
"""When octavia usage is higher than new quota it won't be changed"""
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
user = mock.Mock()
user.id = 'user_id'
user.name = "test@example.com"
user.email = "test@example.com"
user.domain = 'default'
user.password = "test_password"
setup_identity_cache(projects=[project], users=[user])
setup_mock_caches('RegionOne', project.id)
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
data = {
'project_id': 'test_project_id',
'size': 'small',
'domain_id': 'default',
'regions': ['RegionOne'],
}
# setup 2 load balancers
octavia_cache['RegionOne'][project.id]['load_balancer'] = [
{'id': 'fake_id'},
{'id': 'fake_id2'}]
action = UpdateProjectQuotasAction(data, task=task, order=1)
action.pre_approve()
self.assertEqual(action.valid, False)
action.post_approve()
self.assertEqual(action.valid, False)
# check the quotas were updated
# This relies on test_settings heavily.
octaviaquota = octavia_cache['RegionOne']['test_project_id']['quota']
# Still set to default
self.assertEqual(octaviaquota['load_balancer'], 1)

View File

@ -15,7 +15,6 @@
import mock
from rest_framework import status
from rest_framework.test import APITestCase
from django.conf import settings
from django.test.utils import modify_settings
@ -26,16 +25,18 @@ from adjutant.api.models import Token, Task
from adjutant.common.tests import fake_clients
from adjutant.common.tests.fake_clients import (
FakeManager, setup_identity_cache, get_fake_neutron, get_fake_novaclient,
get_fake_cinderclient, cinder_cache, nova_cache, neutron_cache,
setup_mock_caches, setup_quota_cache, FakeResource)
from adjutant.common.tests.utils import modify_dict_settings
get_fake_cinderclient, get_fake_octaviaclient, cinder_cache, nova_cache,
neutron_cache, octavia_cache, setup_mock_caches, setup_quota_cache,
FakeResource)
from adjutant.common.tests.utils import (
modify_dict_settings, AdjutantAPITestCase)
from datetime import timedelta
@mock.patch('adjutant.common.user_store.IdentityManager',
FakeManager)
class OpenstackAPITests(APITestCase):
class OpenstackAPITests(AdjutantAPITestCase):
"""
TaskView tests specific to the openstack style urls.
Many of the original TaskView tests are valid and need
@ -421,34 +422,26 @@ class OpenstackAPITests(APITestCase):
'adjutant.common.user_store.IdentityManager',
FakeManager)
@mock.patch(
'adjutant.common.quota.get_novaclient',
'adjutant.common.openstack_clients.get_novaclient',
get_fake_novaclient)
@mock.patch(
'adjutant.common.quota.get_neutronclient',
'adjutant.common.openstack_clients.get_neutronclient',
get_fake_neutron)
@mock.patch(
'adjutant.common.quota.get_cinderclient',
'adjutant.common.openstack_clients.get_cinderclient',
get_fake_cinderclient)
@mock.patch(
'adjutant.actions.v1.resources.' +
'openstack_clients.get_neutronclient',
get_fake_neutron)
@mock.patch(
'adjutant.actions.v1.resources.' +
'openstack_clients.get_novaclient',
get_fake_novaclient)
@mock.patch(
'adjutant.actions.v1.resources.' +
'openstack_clients.get_cinderclient',
get_fake_cinderclient)
class QuotaAPITests(APITestCase):
'adjutant.common.openstack_clients.get_octaviaclient',
get_fake_octaviaclient)
class QuotaAPITests(AdjutantAPITestCase):
def setUp(self):
super(QuotaAPITests, self).setUp()
setup_mock_caches('RegionOne', 'test_project_id')
setup_mock_caches('RegionTwo', 'test_project_id')
def check_quota_cache(self, region_name, project_id, size):
def check_quota_cache(self, region_name, project_id, size,
extra_services=[]):
"""
Helper function to check if the global quota caches now match the size
defined in the config
@ -465,6 +458,12 @@ class QuotaAPITests(APITestCase):
network = settings.PROJECT_QUOTA_SIZES[size]['neutron']['network']
self.assertEqual(neutronquota['network'], network)
if 'octavia' in extra_services:
octaviaquota = octavia_cache[region_name][project_id]['quota']
load_balancer = settings.PROJECT_QUOTA_SIZES.get(
size)['octavia']['load_balancer']
self.assertEqual(octaviaquota['load_balancer'], load_balancer)
def test_update_quota_no_history(self):
""" Update the quota size of a project with no history """
@ -1286,3 +1285,42 @@ class QuotaAPITests(APITestCase):
self.assertEqual(
response.data['regions'][0]['quota_change_options'],
['small', 'medium'])
@modify_dict_settings(QUOTA_SERVICES={
'operation': 'append',
'key_list': ['*'],
'value': 'octavia'
})
def test_update_quota_no_history_with_octavia(self):
""" Update quota for octavia."""
project = fake_clients.FakeProject(
name="test_project", id='test_project_id')
user = fake_clients.FakeUser(
name="test@example.com", password="123", email="test@example.com")
setup_identity_cache(projects=[project], users=[user])
admin_headers = {
'project_name': "test_project",
'project_id': project.id,
'roles': "project_admin,_member_,project_mod",
'username': "test@example.com",
'user_id': "user_id",
'authenticated': True
}
url = "/v1/openstack/quotas/"
data = {'size': 'medium',
'regions': ['RegionOne']}
response = self.client.post(url, data,
headers=admin_headers, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Then check to see the quotas have changed
self.check_quota_cache(
'RegionOne', project.id, 'medium', extra_services=['octavia'])

View File

@ -22,6 +22,7 @@ from keystoneclient import client as ks_client
from cinderclient import client as cinderclient
from neutronclient.v2_0 import client as neutronclient
from novaclient import client as novaclient
from octaviaclient.api.v2 import octavia
# Defined for use locally
DEFAULT_COMPUTE_VERSION = "2"
@ -79,3 +80,14 @@ def get_cinderclient(region, version=DEFAULT_VOLUME_VERSION):
version,
session=get_auth_session(),
region_name=region)
def get_octaviaclient(region):
ks = get_keystoneclient()
service = ks.services.list(name='octavia')[0]
endpoint = ks.endpoints.list(service=service,
region=region, interface='public')[0]
return octavia.OctaviaAPI(
session=get_auth_session(),
endpoint=endpoint.url)

View File

@ -13,8 +13,7 @@
# under the License.
from adjutant.common.openstack_clients import (
get_novaclient, get_cinderclient, get_neutronclient)
from adjutant.common import openstack_clients
from django.conf import settings
@ -33,7 +32,7 @@ class QuotaManager(object):
class ServiceQuotaCinderHelper(ServiceQuotaHelper):
def __init__(self, region_name, project_id):
self.client = get_cinderclient(
self.client = openstack_clients.get_cinderclient(
region=region_name)
self.project_id = project_id
@ -59,7 +58,7 @@ class QuotaManager(object):
class ServiceQuotaNovaHelper(ServiceQuotaHelper):
def __init__(self, region_name, project_id):
self.client = get_novaclient(
self.client = openstack_clients.get_novaclient(
region=region_name)
self.project_id = project_id
@ -85,7 +84,7 @@ class QuotaManager(object):
class ServiceQuotaNeutronHelper(ServiceQuotaHelper):
def __init__(self, region_name, project_id):
self.client = get_neutronclient(
self.client = openstack_clients.get_neutronclient(
region=region_name)
self.project_id = project_id
@ -123,10 +122,57 @@ class QuotaManager(object):
def get_quota(self):
return self.client.show_quota(self.project_id)['quota']
class ServiceQuotaOctaviaHelper(ServiceQuotaNeutronHelper):
def __init__(self, region_name, project_id):
self.client = openstack_clients.get_octaviaclient(
region=region_name)
self.project_id = project_id
def get_quota(self):
project_quota = self.client.quota_show(
project_id=self.project_id)
# NOTE(amelia): Instead of returning the default quota if ANY
# of the quotas are the default, the endpoint
# returns None
default_quota = None
for name, quota in project_quota.items():
if quota is None:
if not default_quota:
default_quota = self.client.quota_defaults_show()[
'quota']
project_quota[name] = default_quota[name]
return project_quota
def set_quota(self, values):
self.client.quota_set(self.project_id, json={'quota': values})
def get_usage(self):
usage = {}
usage['load_balancer'] = len(self.client.load_balancer_list(
project_id=self.project_id)['loadbalancers'])
usage['listener'] = len(self.client.listener_list(
project_id=self.project_id)['listeners'])
pools = self.client.pool_list(
project_id=self.project_id)['pools']
usage['pool'] = len(pools)
members = []
for pool in pools:
members += pool['members']
usage['member'] = len(members)
usage['health_monitor'] = len(self.client.health_monitor_list(
project_id=self.project_id)['healthmonitors'])
return usage
_quota_updaters = {
'cinder': ServiceQuotaCinderHelper,
'nova': ServiceQuotaNovaHelper,
'neutron': ServiceQuotaNeutronHelper
'neutron': ServiceQuotaNeutronHelper,
'octavia': ServiceQuotaOctaviaHelper,
}
def __init__(self, project_id, size_difference_threshold=None):
@ -175,12 +221,18 @@ class QuotaManager(object):
match_percentages = []
for service_name, values in setting.items():
for name, value in values.items():
if value != 0:
if value > 0:
try:
current = current_quota[service_name][name]
match_percentages.append(float(current) / value)
except KeyError:
pass
elif value < 0:
# NOTE(amelia): Sub-zero quota means unlimited
if current_quota[service_name][name] < 0:
match_percentages.append(1.0)
else:
match_percentages.append(0.0)
elif current_quota[service_name][name] == 0:
match_percentages.append(1.0)
else:
@ -188,8 +240,7 @@ class QuotaManager(object):
# Calculate the average of how much it matches the setting
difference = abs(
(sum(match_percentages) / float(len(match_percentages))) - 1)
# TODO(amelia): Nicer form of this due to the new way of doing
# per action settings
if (difference <= self.size_diff_threshold):
quota_differences[size] = difference

View File

@ -23,6 +23,7 @@ identity_cache = {}
neutron_cache = {}
nova_cache = {}
cinder_cache = {}
octavia_cache = {}
class FakeProject(object):
@ -595,6 +596,75 @@ class FakeNeutronClient(object):
return neutron_cache[self.region][tenant_id]
class FakeOctaviaClient(object):
# {name in client call: name in response}
resource_dict = {'load_balancer': 'loadbalancers',
'listener': 'listeners',
'member': 'members',
'pool': 'pools',
'health_monitor': 'healthmonitors'}
# NOTE(amelia): Using the current octavia client we will get back
# dicts for everything, rather than the resources the
# other clients wrap.
# Additionally the openstacksdk octavia implemenation
# does not have quota commands
def __init__(self, region):
global octavia_cache
self.region = region
if region not in octavia_cache:
octavia_cache[region] = {}
self.cache = octavia_cache[region]
def quota_show(self, project_id):
self._ensure_project_exists(project_id)
quota = self.cache.get(project_id, {}).get('quota', [])
for item in self.resource_dict:
if item not in quota:
quota[item] = None
return {'quota': quota}
def quota_set(self, project_id, json):
self._ensure_project_exists(project_id)
self.cache[project_id]['quota'] = json['quota']
def quota_defaults_show(self):
return {
"quota": {
"load_balancer": 10,
"listener": -1,
"member": 50,
"pool": -1,
"health_monitor": -1
}
}
def lister(self, resource_type):
def action(project_id=None):
self._ensure_project_exists(project_id)
resource = self.cache.get(project_id, {}).get(resource_type, [])
links_name = resource_type + '_links'
resource_name = self.resource_dict[resource_type]
return {resource_name: resource, links_name: []}
return action
def _ensure_project_exists(self, project_id):
if project_id not in self.cache:
self.cache[project_id] = {
name: [] for name in self.resource_dict.keys()}
self.cache[project_id]['quota'] = dict(
settings.PROJECT_QUOTA_SIZES['small']['octavia'])
def __getattr__(self, name):
# NOTE(amelia): This is out of pure laziness
global octavia_cache
if name[-5:] == '_list' and name[:-5] in self.resource_dict:
return self.lister(name[:-5])
else:
raise AttributeError
class FakeNovaClient(FakeOpenstackClient):
def __init__(self, region):
@ -754,6 +824,10 @@ def setup_mock_caches(region, project_id):
setup_nova_cache(region, project_id)
setup_cinder_cache(region, project_id)
setup_neutron_cache(region, project_id)
client = FakeOctaviaClient(region)
if project_id in octavia_cache[region]:
del octavia_cache[region][project_id]
client._ensure_project_exists(project_id)
def get_fake_neutron(region):
@ -767,3 +841,7 @@ def get_fake_novaclient(region):
def get_fake_cinderclient(region):
global cinder_cache
return FakeCinderClient(region)
def get_fake_octaviaclient(region):
return FakeOctaviaClient(region)

View File

@ -313,6 +313,13 @@ PROJECT_QUOTA_SIZES = {
'security_group_rule': 100,
'subnet': 3,
},
"octavia": {
'health_monitor': 5,
"listener": 1,
"load_balancer": 1,
"member": 2,
"pool": 1,
},
},
"medium": {
"cinder": {
@ -341,7 +348,14 @@ PROJECT_QUOTA_SIZES = {
"security_group": 50,
"router": 5,
"port": 250
}
},
"octavia": {
'health_monitor': 50,
"listener": 5,
"load_balancer": 5,
"member": 5,
"pool": 5,
},
},
"large": {
"cinder": {
@ -370,7 +384,14 @@ PROJECT_QUOTA_SIZES = {
"security_group": 100,
"router": 10,
"port": 500
}
},
"octavia": {
'health_monitor': 100,
"listener": 10,
"load_balancer": 10,
"member": 10,
"pool": 10,
},
},
"large_cinder_only": {
"cinder": {

View File

@ -424,3 +424,5 @@ QUOTA_SERVICES:
- nova
- neutron
- cinder
# Additonal Quota Service
# - octavia

View File

@ -11,6 +11,7 @@ python-cinderclient>=2.0.1
python-neutronclient>=6.2.0
python-novaclient>=8.0.0
python-keystoneclient>=3.10.0
python-octaviaclient>=1.0.0
six>=1.10.0
jsonfield>=2.0.1
django-rest-swagger>=2.1.2