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:
parent
4ad164aff7
commit
47527734e2
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user