Adding Quota Update Action

* This action can be run for new projects to
  ensure their size matches a set of size templates.
* Changes can be applied separately per region and service.

Change-Id: I3ef0fe0ba1f9d7df6a6f68e30cadbc19bbc0306f
This commit is contained in:
Dale Smith 2016-10-04 15:13:36 +01:00
parent 4ad164aff7
commit 47527734e2
9 changed files with 368 additions and 34 deletions

View File

@ -127,11 +127,12 @@ TASK_SETTINGS:
# default_actions:
# - NewProjectAction
#
# Additonal actions for views
# Additional actions for views
# These will run after the default actions, in the given order.
additional_actions:
- AddDefaultUsersToProjectAction
- NewProjectDefaultNetworkAction
- SetProjectQuotaAction
notifications:
standard:
EmailNotification:
@ -222,6 +223,10 @@ ACTION_SETTINGS:
- admin
default_roles:
- admin
SetProjectQuotaAction:
regions:
RegionOne:
quota_size: small
# mapping between roles and managable roles
ROLES_MAPPING:
@ -239,3 +244,30 @@ ROLES_MAPPING:
- project_mod
- heat_stack_owner
- _member_
PROJECT_QUOTA_SIZES:
small:
nova:
instances: 10
cores: 20
ram: 65536
floating_ips: 10
fixed_ips: 0
metadata_items: 128
injected_files: 5
injected_file_content_bytes: 10240
key_pairs: 50
security_groups: 20
security_group_rules: 100
cinder:
gigabytes: 5000
snapshots: 50
volumes: 20
neutron:
floatingip: 10
network: 3
port: 50
router: 3
security_group: 20
security_group_rule: 100
subnet: 3

View File

@ -3,8 +3,11 @@ decorator>=3.4.0
djangorestframework>=3.4.1
keystoneauth1>=2.11.0
keystonemiddleware>=4.7.0
python-keystoneclient>=3.3.0
python-neutronclient>=5.0.0
python-cinderclient>=1.9.0
python-neutronclient>=6.0.0
python-novaclient>=6.0.0
python-keystoneclient>=3.5.0
six>=1.9.0
jsonfield>=1.0.3
django-rest-swagger>=2.0.3
pyyaml>=3.11

View File

@ -484,7 +484,7 @@ class NewProjectAction(BaseAction, ProjectCreateBase):
self.add_note('Domain id does not match keystone user domain.')
return False
return super(NewProject, self)._validate_domain()
return super(NewProjectAction, self)._validate_domain()
def _validate_parent_project(self):
if self.parent_id:

View File

@ -19,33 +19,63 @@ from keystoneauth1.identity import v3
from keystoneauth1 import session
from keystoneclient import client as ks_client
from neutronclient.v2_0 import client as neutron_client
from cinderclient import client as cinderclient
from neutronclient.v2_0 import client as neutronclient
from novaclient import client as novaclient
# Defined for use locally
DEFAULT_COMPUTE_VERSION = "2"
DEFAULT_IDENTITY_VERSION = "3"
DEFAULT_IMAGE_VERSION = "2"
DEFAULT_METERING_VERSION = "2"
DEFAULT_OBJECT_STORAGE_VERSION = "1"
DEFAULT_ORCHESTRATION_VERSION = "1"
DEFAULT_VOLUME_VERSION = "2"
# Auth session shared by default with all clients
client_auth_session = None
def get_keystoneclient():
def get_auth_session():
""" Returns a global auth session to be shared by all clients """
global client_auth_session
if not client_auth_session:
auth = v3.Password(
username=settings.KEYSTONE['username'],
password=settings.KEYSTONE['password'],
project_name=settings.KEYSTONE['project_name'],
auth_url=settings.KEYSTONE['auth_url'],
user_domain_id=settings.KEYSTONE.get('domain_id', "default"),
project_domain_id=settings.KEYSTONE.get('domain_id', "default"),
)
sess = session.Session(auth=auth)
auth = ks_client.Client(session=sess)
return auth
auth = v3.Password(
username=settings.KEYSTONE['username'],
password=settings.KEYSTONE['password'],
project_name=settings.KEYSTONE['project_name'],
auth_url=settings.KEYSTONE['auth_url'],
user_domain_id=settings.KEYSTONE.get('domain_id', "default"),
project_domain_id=settings.KEYSTONE.get('domain_id', "default"),
)
client_auth_session = session.Session(auth=auth)
return client_auth_session
def get_keystoneclient(version=DEFAULT_IDENTITY_VERSION):
return ks_client.Client(
version,
session=get_auth_session())
def get_neutronclient(region):
auth = v3.Password(
username=settings.KEYSTONE['username'],
password=settings.KEYSTONE['password'],
project_name=settings.KEYSTONE['project_name'],
auth_url=settings.KEYSTONE['auth_url'],
user_domain_id=settings.KEYSTONE.get('domain_id', "default"),
project_domain_id=settings.KEYSTONE.get('domain_id', "default"),
)
sess = session.Session(auth=auth)
neutron = neutron_client.Client(session=sess, region_name=region)
return neutron
# always returns neutron client v2
return neutronclient.Client(
session=get_auth_session(),
region_name=region)
def get_novaclient(region, version=DEFAULT_COMPUTE_VERSION):
return novaclient.Client(
version,
session=get_auth_session(),
region_name=region)
def get_cinderclient(region, version=DEFAULT_VOLUME_VERSION):
return cinderclient.Client(
version,
session=get_auth_session(),
region_name=region)

View File

@ -17,6 +17,7 @@ from stacktask.actions.tenant_setup import serializers
from django.conf import settings
from stacktask.actions.user_store import IdentityManager
from stacktask.actions import openstack_clients
import six
class NewDefaultNetworkAction(BaseAction):
@ -356,6 +357,112 @@ class AddDefaultUsersToProjectAction(BaseAction):
pass
class SetProjectQuotaAction(BaseAction):
""" Updates quota for a given project to a configured quota level """
class ServiceQuotaFunctor(object):
def __call__(self, project_id, values):
self.client.quotas.update(
project_id,
**values)
class ServiceQuotaCinderFunctor(ServiceQuotaFunctor):
def __init__(self, region_name):
self.client = openstack_clients.get_cinderclient(
region=region_name)
class ServiceQuotaNovaFunctor(ServiceQuotaFunctor):
def __init__(self, region_name):
self.client = openstack_clients.get_novaclient(
region=region_name)
class ServiceQuotaNeutronFunctor(ServiceQuotaFunctor):
def __init__(self, region_name):
self.client = openstack_clients.get_neutronclient(
region=region_name)
def __call__(self, project_id, values):
body = {
'quota': values
}
self.client.update_quota(
project_id,
body)
_quota_updaters = {
'cinder': ServiceQuotaCinderFunctor,
'nova': ServiceQuotaNovaFunctor,
'neutron': ServiceQuotaNeutronFunctor
}
def _validate_project_exists(self):
if not self.project_id:
self.add_note('No project_id set, previous action should have '
'set it.')
return False
id_manager = IdentityManager()
project = id_manager.get_project(self.project_id)
if not project:
self.add_note('Project with id %s does not exist.' %
self.project_id)
return False
self.add_note('Project with id %s exists.' % self.project_id)
return True
def _pre_validate(self):
# Nothing to validate yet.
self.action.valid = True
self.action.save()
def _validate(self):
# Make sure the project id is valid and can be used
self.action.valid = (
self._validate_project_exists()
)
self.action.save()
def _pre_approve(self):
self._pre_validate()
def _post_approve(self):
# Assumption: another action has placed the project_id into the cache.
self.project_id = self.action.task.cache.get('project_id', None)
self._validate()
if not self.valid or self.action.state == "completed":
return
# update quota for each openstack service
regions_dict = settings.ACTION_SETTINGS.get(
'SetProjectQuotaAction', {}).get('regions', {})
for region_name, region_settings in six.iteritems(regions_dict):
quota_size = region_settings.get('quota_size')
quota_settings = settings.PROJECT_QUOTA_SIZES.get(quota_size, {})
if not quota_settings:
self.add_note(
"Project quota not defined for size '%s' in region %s." % (
quota_size, region_name))
continue
for service_name, values in six.iteritems(quota_settings):
updater_class = self._quota_updaters.get(service_name)
if not updater_class:
self.add_note("No quota updater found for %s. Ignoring" %
service_name)
continue
# functor for the service+region
service_functor = updater_class(region_name)
service_functor(self.project_id, values)
self.add_note("Project quota for region %s set to %s" % (
region_name, quota_size))
self.action.state = "completed"
self.action.save()
def _submit(self, token_data):
pass
action_classes = {
'NewDefaultNetworkAction':
(NewDefaultNetworkAction,
@ -365,7 +472,10 @@ action_classes = {
serializers.NewProjectDefaultNetworkSerializer),
'AddDefaultUsersToProjectAction':
(AddDefaultUsersToProjectAction,
serializers.AddDefaultUsersToProjectSerializer)
serializers.AddDefaultUsersToProjectSerializer),
'SetProjectQuotaAction':
(SetProjectQuotaAction,
serializers.SetProjectQuotaSerializer)
}
settings.ACTION_CLASSES.update(action_classes)

View File

@ -28,3 +28,7 @@ class NewProjectDefaultNetworkSerializer(serializers.Serializer):
class AddDefaultUsersToProjectSerializer(serializers.Serializer):
domain_id = serializers.CharField(max_length=64, default='default')
class SetProjectQuotaSerializer(serializers.Serializer):
pass

View File

@ -18,13 +18,40 @@ import mock
from stacktask.actions.tenant_setup.models import (
NewDefaultNetworkAction, NewProjectDefaultNetworkAction,
AddDefaultUsersToProjectAction)
AddDefaultUsersToProjectAction, SetProjectQuotaAction)
from stacktask.api.models import Task
from stacktask.api.v1 import tests
from stacktask.api.v1.tests import FakeManager, setup_temp_cache
neutron_cache = {}
nova_cache = {}
cinder_cache = {}
class FakeOpenstackClient(object):
class Quotas(object):
""" Stub class for testing quotas """
def __init__(self, service):
self.service = service
def update(self, project_id, **kwargs):
self.service.update_quota(project_id, **kwargs)
def __init__(self, region, cache):
self.region = region
self._cache = cache
self.quotas = FakeOpenstackClient.Quotas(self)
def update_quota(self, project_id, **kwargs):
if self.region not in self._cache:
self._cache[self.region] = {}
if project_id not in self._cache[self.region]:
self._cache[self.region][project_id] = {
'quota': {}
}
quota = self._cache[self.region][project_id]['quota']
quota.update(kwargs)
class FakeNeutronClient(object):
@ -59,6 +86,16 @@ class FakeNeutronClient(object):
router['router']['interface'] = body
return router
def update_quota(self, project_id, body):
global neutron_cache
if project_id not in neutron_cache:
neutron_cache[project_id] = {}
if 'quota' not in neutron_cache[project_id]:
neutron_cache[project_id]['quota'] = {}
quota = neutron_cache[project_id]['quota']
quota.update(body['quota'])
def setup_neutron_cache():
global neutron_cache
@ -66,7 +103,7 @@ def setup_neutron_cache():
'i': 0,
'networks': {},
'subnets': {},
'routers': {}
'routers': {},
}
@ -74,6 +111,16 @@ def get_fake_neutron(region):
return FakeNeutronClient()
def get_fake_novaclient(region):
global nova_cache
return FakeOpenstackClient(region, nova_cache)
def get_fake_cinderclient(region):
global cinder_cache
return FakeOpenstackClient(region, cinder_cache)
class ProjectSetupActionTests(TestCase):
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
@ -523,3 +570,59 @@ class ProjectSetupActionTests(TestCase):
project = tests.temp_cache['projects']['test_project']
self.assertEquals(project.roles['user_id_0'], ['admin'])
@mock.patch('stacktask.actions.tenant_setup.models.IdentityManager',
FakeManager)
@mock.patch(
'stacktask.actions.tenant_setup.models.' +
'openstack_clients.get_neutronclient',
get_fake_neutron)
@mock.patch(
'stacktask.actions.tenant_setup.models.' +
'openstack_clients.get_novaclient',
get_fake_novaclient)
@mock.patch(
'stacktask.actions.tenant_setup.models.' +
'openstack_clients.get_cinderclient',
get_fake_cinderclient)
def test_set_quota(self):
"""
Base case, sets quota on all services of the cached project id.
"""
project = mock.Mock()
project.id = 'test_project_id'
project.name = 'test_project'
project.domain = 'default'
project.roles = {}
setup_temp_cache({'test_project': project}, {})
setup_neutron_cache()
task = Task.objects.create(
ip_address="0.0.0.0", keystone_user={'roles': ['admin']})
task.cache = {'project_id': "test_project_id"}
action = SetProjectQuotaAction({}, task=task, order=1)
action.pre_approve()
self.assertEquals(action.valid, True)
action.post_approve()
self.assertEquals(action.valid, True)
# check the quotas were updated
# This relies on test_settings heavily.
cinderquota = cinder_cache['RegionOne']['test_project_id']['quota']
self.assertEquals(cinderquota['gigabytes'], 5000)
novaquota = nova_cache['RegionOne']['test_project_id']['quota']
self.assertEquals(novaquota['ram'], 65536)
neutronquota = neutron_cache['test_project_id']['quota']
self.assertEquals(neutronquota['network'], 3)
# RegionTwo, cinder only
self.assertFalse('RegionTwo' in nova_cache)
r2_cinderquota = cinder_cache['RegionTwo']['test_project_id']['quota']
self.assertEquals(r2_cinderquota['gigabytes'], 73571)
self.assertEquals(r2_cinderquota['snapshots'], 73572)
self.assertEquals(r2_cinderquota['volumes'], 73573)

View File

@ -164,6 +164,8 @@ ACTION_SETTINGS = CONFIG['ACTION_SETTINGS']
ROLES_MAPPING = CONFIG['ROLES_MAPPING']
PROJECT_QUOTA_SIZES = CONFIG['PROJECT_QUOTA_SIZES']
# Defaults for backwards compatibility.
ACTIVE_TASKVIEWS = CONFIG.get(
'ACTIVE_TASKVIEWS',

View File

@ -173,7 +173,7 @@ ACTION_SETTINGS = {
'public_network': '3cb50f61-5bce-4c03-96e6-8e262e12bb35',
'router_name': 'somerouter',
'subnet_name': 'somesubnet'
}
},
},
'AddDefaultUsersToProjectAction': {
'default_users': [
@ -182,7 +182,17 @@ ACTION_SETTINGS = {
'default_roles': [
'admin',
],
}
},
'SetProjectQuotaAction': {
'regions': {
'RegionOne': {
'quota_size': 'small'
},
'RegionTwo': {
'quota_size': 'large'
}
},
},
}
@ -198,6 +208,45 @@ ROLES_MAPPING = {
],
}
PROJECT_QUOTA_SIZES = {
'small': {
'nova': {
'instances': 10,
'cores': 20,
'ram': 65536,
'floating_ips': 10,
'fixed_ips': 0,
'metadata_items': 128,
'injected_files': 5,
'injected_file_content_bytes': 10240,
'key_pairs': 50,
'security_groups': 20,
'security_group_rules': 100,
},
'cinder': {
'gigabytes': 5000,
'snapshots': 50,
'volumes': 20,
},
'neutron': {
'floatingip': 10,
'network': 3,
'port': 50,
'router': 3,
'security_group': 20,
'security_group_rule': 100,
'subnet': 3,
},
},
'large': {
'cinder': {
'gigabytes': 73571,
'snapshots': 73572,
'volumes': 73573,
},
},
}
SHOW_ACTION_ENDPOINTS = True
conf_dict = {
@ -216,5 +265,6 @@ conf_dict = {
"TOKEN_SUBMISSION_URL": TOKEN_SUBMISSION_URL,
"TOKEN_EXPIRE_TIME": TOKEN_EXPIRE_TIME,
"ROLES_MAPPING": ROLES_MAPPING,
"SHOW_ACTION_ENDPOINTS": SHOW_ACTION_ENDPOINTS
"PROJECT_QUOTA_SIZES": PROJECT_QUOTA_SIZES,
"SHOW_ACTION_ENDPOINTS": SHOW_ACTION_ENDPOINTS,
}