diff --git a/charms_openstack/adapters.py b/charms_openstack/adapters.py index fdf875a..2b2a9f2 100644 --- a/charms_openstack/adapters.py +++ b/charms_openstack/adapters.py @@ -24,6 +24,7 @@ import charms.reactive as reactive import charms.reactive.bus import charmhelpers.contrib.hahelpers.cluster as ch_cluster import charmhelpers.contrib.network.ip as ch_ip +import charmhelpers.contrib.openstack.context as ch_context import charmhelpers.contrib.openstack.utils as ch_utils import charmhelpers.core.hookenv as hookenv import charmhelpers.core.host as ch_host @@ -860,6 +861,63 @@ class APIConfigurationAdapter(ConfigurationAdapter): ctxt['memcache_port']) return ctxt + @property + @hookenv.cached + def workers(self): + """Return the a number of workers that depends on the + config('worker_muliplier') and the number of cpus. This function uses + the charmhelpers.contrib.openstack.context.WorkerConfigContext() to do + the heavy lifting so that any changes in charmhelpers propagate to this + function + + :returns: the number of workers to apply to a configuration file. + """ + return ch_context.WorkerConfigContext()()["workers"] + + @property + @hookenv.cached + def wsgi_worker_context(self): + """Return a WSGIWorkerConfigContext dictionary. + + This is used to configure a WSGI worker. The charm_instance class can + define some attributes (or properties - anything getattr(...) will work + against for: + + wsgi_script: a script/name to pass to the WSGIW... constructor + wsgi_admin_script: a script/name to pass to the WSGIW... + constructor + wsgi_public_script: a script/name to pass to the WSGIW... + constructor + wsgi_process_weight: an float between 0.0 and 1.0 to split the + share of all workers between main, admin and public workers. + wsgi_admin_process_weight: an float between 0.0 and 1.0 to split + the share of all workers between main, admin and public workers + wsgi_public_process_weight: an float between 0.0 and 1.0 to split + the share of all workers between main, admin and public workers + + The sum of the process weights should equal 1 to make sense. + + :returns: WSGIWorkerConfigContext dictionary. + """ + charm_instance = self.charm_instance or {} + kwargs = dict( + name=getattr(charm_instance, 'name', None), + script=getattr(charm_instance, 'wsgi_script', None), + admin_script=getattr(charm_instance, 'wsgi_admin_script', None), + public_script=getattr(charm_instance, 'wsgi_public_script', None), + process_weight=getattr( + charm_instance, 'wsgi_process_weight', None), + admin_process_weight=getattr( + charm_instance, 'wsgi_admin_process_weight', None), + public_process_weight=getattr( + charm_instance, 'wsgi_public_process_weight', None), + ) + # filtering the kwargs of Nones allows the default arguments on + # WSGIWorkerConfigContext.__init__(...) to be used. + filtered_kwargs = dict((k, v) for k, v in kwargs.items() + if v is not None) + return ch_context.WSGIWorkerConfigContext(**filtered_kwargs)() + def make_default_relation_adapter(base_cls, relation, properties): """Create a default relation adapter using a base class, and custom diff --git a/charms_openstack/charm/classes.py b/charms_openstack/charm/classes.py index d3b7a0d..9323a65 100644 --- a/charms_openstack/charm/classes.py +++ b/charms_openstack/charm/classes.py @@ -146,6 +146,18 @@ class OpenStackAPICharm(OpenStackCharm): # If None, then the default ConfigurationAdapter is used. configuration_class = os_adapters.APIConfigurationAdapter + # These can be overriden in the derived charm class to allow specialism of + # config files. These values are read in the APIConfigurationAdapter and + # used to furnish the dictionary provided from the property + # 'wsgi_worker_context'. e.g. config.wsgi_worker_context.processes would + # be the number of processes for the main API wsgi worker. + wsgi_script = None + wsgi_admin_script = None + wsgi_public_script = None + wsgi_process_weight = None # use the default from charm-helpers + wsgi_admin_process_weight = None # use the default from charm-helpers + wsgi_public_process_weight = None # use the default from charm-helpers + def upgrade_charm(self): """Setup token cache in case previous charm version did not.""" self.setup_token_cache() diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py index f7dba04..4a6d193 100644 --- a/unit_tests/__init__.py +++ b/unit_tests/__init__.py @@ -32,6 +32,8 @@ sys.modules['charmhelpers.contrib.openstack.utils'] = ( charmhelpers.contrib.openstack.utils) sys.modules['charmhelpers.contrib.openstack.templating'] = ( charmhelpers.contrib.openstack.templating) +sys.modules['charmhelpers.contrib.openstack.context'] = ( + charmhelpers.contrib.openstack.context) sys.modules['charmhelpers.contrib.network'] = charmhelpers.contrib.network sys.modules['charmhelpers.contrib.network.ip'] = ( charmhelpers.contrib.network.ip) diff --git a/unit_tests/test_charms_openstack_adapters.py b/unit_tests/test_charms_openstack_adapters.py index 17fc4c3..a1dbef8 100644 --- a/unit_tests/test_charms_openstack_adapters.py +++ b/unit_tests/test_charms_openstack_adapters.py @@ -790,6 +790,87 @@ class TestAPIConfigurationAdapter(unittest.TestCase): memcache.return_value = {'memcache_url': 'hello'} self.assertEquals(c.memcache_url, 'hello') + def test_workers(self): + class FakeWorkerConfigContext(object): + def __call__(self): + return {"workers": 8} + + with mock.patch.object(adapters.ch_context, 'WorkerConfigContext', + new=FakeWorkerConfigContext): + c = adapters.APIConfigurationAdapter() + self.assertEquals(c.workers, 8) + + def test_wsgi_worker_context(self): + class ChInstance1(object): + name = 'test-name' + wsgi_script = 'test-script' + api_ports = {} + + class ChInstance2(object): + name = 'test-name' + wsgi_script = 'test-script' + wsgi_admin_script = 'test-admin-script' + wsgi_public_script = 'test-public-script' + wsgi_process_weight = 0.5 + wsgi_admin_process_weight = 0.1 + wsgi_public_process_weight = 0.4 + api_ports = {} + + class ChInstance3(object): + name = 'test-name' + wsgi_script = None + wsgi_admin_script = 'test-admin-script' + wsgi_public_script = 'test-public-script' + wsgi_process_weight = None + wsgi_admin_process_weight = 0.1 + wsgi_public_process_weight = 0.4 + api_ports = {} + + class FakeWSGIWorkerConfigContext(): + copy_kwargs = None + + def __init__(self, **kwargs): + self.__class__.copy_kwargs = kwargs.copy() + + def __call__(self): + return "T" + + with mock.patch.object(adapters.ch_context, 'WSGIWorkerConfigContext', + new=FakeWSGIWorkerConfigContext): + # start with no charm instance to get default values + c = adapters.APIConfigurationAdapter() + self.assertEquals(c.wsgi_worker_context, "T") + self.assertEquals(FakeWSGIWorkerConfigContext.copy_kwargs, {}) + # start with a minimal charm_instance + instance = ChInstance1() + c = adapters.APIConfigurationAdapter(charm_instance=instance) + self.assertEquals(c.wsgi_worker_context, "T") + self.assertEquals(FakeWSGIWorkerConfigContext.copy_kwargs, + {'name': 'test-name', 'script': 'test-script'}) + # And then, all the options set: + instance = ChInstance2() + c = adapters.APIConfigurationAdapter(charm_instance=instance) + self.assertEquals(c.wsgi_worker_context, "T") + self.assertEquals(FakeWSGIWorkerConfigContext.copy_kwargs, + {'name': 'test-name', + 'script': 'test-script', + 'admin_script': 'test-admin-script', + 'public_script': 'test-public-script', + 'process_weight': 0.5, + 'admin_process_weight': 0.1, + 'public_process_weight': 0.4}) + # and finally, with some of the options set to None, to test + # filtering + instance = ChInstance3() + c = adapters.APIConfigurationAdapter(charm_instance=instance) + self.assertEquals(c.wsgi_worker_context, "T") + self.assertEquals(FakeWSGIWorkerConfigContext.copy_kwargs, + {'name': 'test-name', + 'admin_script': 'test-admin-script', + 'public_script': 'test-public-script', + 'admin_process_weight': 0.1, + 'public_process_weight': 0.4}) + class FakePeerHARelationAdapter(object):