diff options
-rwxr-xr-x | bin/nova-conductor | 51 | ||||
-rw-r--r-- | doc/source/conf.py | 4 | ||||
-rw-r--r-- | doc/source/man/nova-conductor.rst | 45 | ||||
-rw-r--r-- | nova/conductor/__init__.py | 25 | ||||
-rw-r--r-- | nova/conductor/api.py | 61 | ||||
-rw-r--r-- | nova/conductor/manager.py | 51 | ||||
-rw-r--r-- | nova/conductor/rpcapi.py | 43 | ||||
-rw-r--r-- | nova/tests/conductor/__init__.py | 0 | ||||
-rw-r--r-- | nova/tests/conductor/test_conductor.py | 133 |
9 files changed, 412 insertions, 1 deletions
diff --git a/bin/nova-conductor b/bin/nova-conductor new file mode 100755 index 000000000..2dba8ee1b --- /dev/null +++ b/bin/nova-conductor @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Starter script for Nova Conductor.""" + +import eventlet +eventlet.monkey_patch() + +import os +import sys + +# If ../nova/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + + +from nova import config +from nova.openstack.common import log as logging +from nova import service +from nova import utils + +CONF = config.CONF +CONF.import_opt('topic', 'nova.conductor.api', group='conductor') + +if __name__ == '__main__': + config.parse_args(sys.argv) + logging.setup("nova") + utils.monkey_patch() + server = service.Service.create(binary='nova-conductor', + topic=CONF.conductor.topic, + manager=CONF.conductor.manager) + service.serve(server) + service.wait() diff --git a/doc/source/conf.py b/doc/source/conf.py index b52bcad0d..804080e79 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -154,7 +154,9 @@ man_pages = [ ('man/nova-scheduler', 'nova-scheduler', u'Cloud controller fabric', [u'OpenStack'], 1), ('man/nova-xvpvncproxy', 'nova-xvpvncproxy', u'Cloud controller fabric', - [u'OpenStack'], 1) + [u'OpenStack'], 1), + ('man/nova-conductor', 'nova-conductor', u'Cloud controller fabric', + [u'OpenStack'], 1), ] # -- Options for HTML output -------------------------------------------------- diff --git a/doc/source/man/nova-conductor.rst b/doc/source/man/nova-conductor.rst new file mode 100644 index 000000000..7a32730e1 --- /dev/null +++ b/doc/source/man/nova-conductor.rst @@ -0,0 +1,45 @@ +========== +nova-conductor +========== + +-------------------------------- +Server for the Nova Conductor +-------------------------------- + +:Author: openstack@lists.launchpad.net +:Date: 2012-11-16 +:Copyright: OpenStack LLC +:Version: 2012.1 +:Manual section: 1 +:Manual group: cloud computing + +SYNOPSIS +======== + + nova-conductor [options] + +DESCRIPTION +=========== + +nova-conductor is a server daemon that serves the Nova Conductor service, which provides coordination and database query support for Nova. + +OPTIONS +======= + + **General options** + +FILES +======== + +* /etc/nova/nova.conf + +SEE ALSO +======== + +* `OpenStack Nova <http://nova.openstack.org>`__ +* `OpenStack Nova <http://nova.openstack.org>`__ + +BUGS +==== + +* Nova is sourced in Launchpad so you can view current bugs at `OpenStack Nova <http://nova.openstack.org>`__ diff --git a/nova/conductor/__init__.py b/nova/conductor/__init__.py new file mode 100644 index 000000000..036860dbf --- /dev/null +++ b/nova/conductor/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2012 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.conductor import api as conductor_api +import nova.config +import nova.openstack.common.importutils + + +def API(*args, **kwargs): + if nova.config.CONF.conductor.use_local: + api = conductor_api.LocalAPI + else: + api = conductor_api.API + return api(*args, **kwargs) diff --git a/nova/conductor/api.py b/nova/conductor/api.py new file mode 100644 index 000000000..acb412625 --- /dev/null +++ b/nova/conductor/api.py @@ -0,0 +1,61 @@ +# Copyright 2012 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Handles all requests to the conductor service""" + +from nova.conductor import manager +from nova.conductor import rpcapi +from nova import config +from nova.openstack.common import cfg + +conductor_opts = [ + cfg.BoolOpt('use_local', + default=False, + help='Perform nova-conductor operations locally'), + cfg.StrOpt('topic', + default='conductor', + help='the topic conductor nodes listen on'), + cfg.StrOpt('manager', + default='nova.conductor.manager.ConductorManager', + help='full class name for the Manager for conductor'), +] +conductor_group = cfg.OptGroup(name='conductor', + title='Conductor Options') +CONF = config.CONF +CONF.register_group(conductor_group) +CONF.register_opts(conductor_opts, conductor_group) + + +class LocalAPI(object): + """A local version of the conductor API that does database updates + locally instead of via RPC""" + + def __init__(self): + self._manager = manager.ConductorManager() + + def instance_update(self, context, instance_uuid, **updates): + """Perform an instance update in the database""" + return self._manager.instance_update(context, instance_uuid, updates) + + +class API(object): + """Conductor API that does updates via RPC to the ConductorManager""" + + def __init__(self): + self.conductor_rpcapi = rpcapi.ConductorAPI() + + def instance_update(self, context, instance_uuid, **updates): + """Perform an instance update in the database""" + return self.conductor_rpcapi.instance_update(context, instance_uuid, + updates) diff --git a/nova/conductor/manager.py b/nova/conductor/manager.py new file mode 100644 index 000000000..3ffe82645 --- /dev/null +++ b/nova/conductor/manager.py @@ -0,0 +1,51 @@ +# Copyright 2012 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Handles database requests from other nova services""" + +from nova import manager +from nova import notifications +from nova.openstack.common import jsonutils +from nova.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + +allowed_updates = ['task_state', 'vm_state', 'expected_task_state', + 'power_state', 'access_ip_v4', 'access_ip_v6', + 'launched_at', 'terminated_at', 'host', + 'memory_mb', 'vcpus', 'root_gb', 'ephemeral_gb', + 'instance_type_id', + ] + + +class ConductorManager(manager.SchedulerDependentManager): + """Mission: TBD""" + + RPC_API_VERSION = '1.0' + + def __init__(self, *args, **kwargs): + super(ConductorManager, self).__init__(service_name='conductor', + *args, **kwargs) + + def instance_update(self, context, instance_uuid, updates): + for key in updates: + if key not in allowed_updates: + LOG.error(_("Instance update attempted for " + "'%(key)s' on %(instance_uuid)s") % locals()) + raise KeyError("unexpected update keyword '%s'" % key) + old_ref, instance_ref = self.db.instance_update_and_get_original( + context, instance_uuid, updates) + notifications.send_update(context, old_ref, instance_ref) + return jsonutils.to_primitive(instance_ref) diff --git a/nova/conductor/rpcapi.py b/nova/conductor/rpcapi.py new file mode 100644 index 000000000..7a6508f12 --- /dev/null +++ b/nova/conductor/rpcapi.py @@ -0,0 +1,43 @@ +# Copyright 2012 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Client side of the conductor RPC API""" + +from nova import config +import nova.openstack.common.rpc.proxy + +CONF = config.CONF + + +class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy): + """Client side of the conductor RPC API + + API version history: + + 1.0 - Initial version. + """ + + BASE_RPC_API_VERSION = '1.0' + + def __init__(self): + super(ConductorAPI, self).__init__( + topic=CONF.conductor.topic, + default_version=self.BASE_RPC_API_VERSION) + + def instance_update(self, context, instance_uuid, updates): + return self.call(context, + self.make_msg('instance_update', + instance_uuid=instance_uuid, + updates=updates), + topic=self.topic) diff --git a/nova/tests/conductor/__init__.py b/nova/tests/conductor/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/nova/tests/conductor/__init__.py diff --git a/nova/tests/conductor/test_conductor.py b/nova/tests/conductor/test_conductor.py new file mode 100644 index 000000000..fbf7d0325 --- /dev/null +++ b/nova/tests/conductor/test_conductor.py @@ -0,0 +1,133 @@ +# Copyright 2012 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Tests for the conductor service""" + +from nova.compute import instance_types +from nova.compute import vm_states +from nova import conductor +from nova.conductor import api as conductor_api +from nova.conductor import manager as conductor_manager +from nova.conductor import rpcapi as conductor_rpcapi +from nova import context +from nova import db +from nova import notifications +from nova import test + + +FAKE_IMAGE_REF = 'fake-image-ref' + + +class BaseTestCase(test.TestCase): + def setUp(self): + super(BaseTestCase, self).setUp() + self.user_id = 'fake' + self.project_id = 'fake' + self.context = context.RequestContext(self.user_id, + self.project_id) + + def _create_fake_instance(self, params=None, type_name='m1.tiny'): + if not params: + params = {} + + inst = {} + inst['vm_state'] = vm_states.ACTIVE + inst['image_ref'] = FAKE_IMAGE_REF + inst['reservation_id'] = 'r-fakeres' + inst['launch_time'] = '10' + inst['user_id'] = self.user_id + inst['project_id'] = self.project_id + inst['host'] = 'fake_host' + type_id = instance_types.get_instance_type_by_name(type_name)['id'] + inst['instance_type_id'] = type_id + inst['ami_launch_index'] = 0 + inst['memory_mb'] = 0 + inst['vcpus'] = 0 + inst['root_gb'] = 0 + inst['ephemeral_gb'] = 0 + inst['architecture'] = 'x86_64' + inst['os_type'] = 'Linux' + inst.update(params) + return db.instance_create(self.context, inst) + + +class ConductorTestCase(BaseTestCase): + """Conductor Manager Tests""" + def setUp(self): + super(ConductorTestCase, self).setUp() + self.conductor = conductor_manager.ConductorManager() + self.db = None + + def _do_update(self, instance_uuid, **updates): + return self.conductor.instance_update(self.context, instance_uuid, + updates) + + def test_instance_update(self): + instance = self._create_fake_instance() + new_inst = self._do_update(instance['uuid'], + vm_state=vm_states.STOPPED) + instance = db.instance_get_by_uuid(self.context, instance['uuid']) + self.assertEqual(instance['vm_state'], vm_states.STOPPED) + self.assertEqual(new_inst['vm_state'], instance['vm_state']) + + def test_instance_update_invalid_key(self): + # NOTE(danms): the real DB API call ignores invalid keys + if self.db == None: + self.assertRaises(KeyError, + self._do_update, 'any-uuid', foobar=1) + + +class ConductorRPCAPITestCase(ConductorTestCase): + """Conductor RPC API Tests""" + def setUp(self): + super(ConductorRPCAPITestCase, self).setUp() + self.conductor_service = self.start_service( + 'conductor', manager='nova.conductor.manager.ConductorManager') + self.conductor = conductor_rpcapi.ConductorAPI() + + +class ConductorLocalAPITestCase(ConductorTestCase): + """Conductor LocalAPI Tests""" + def setUp(self): + super(ConductorLocalAPITestCase, self).setUp() + self.conductor = conductor_api.LocalAPI() + self.db = db + + def _do_update(self, instance_uuid, **updates): + # NOTE(danms): the public API takes actual keyword arguments, + # so override the base class here to make the call correctly + return self.conductor.instance_update(self.context, instance_uuid, + **updates) + + +class ConductorAPITestCase(ConductorLocalAPITestCase): + """Conductor API Tests""" + def setUp(self): + super(ConductorAPITestCase, self).setUp() + self.conductor_service = self.start_service( + 'conductor', manager='nova.conductor.manager.ConductorManager') + self.conductor = conductor_api.API() + self.db = None + + +class ConductorImportTest(test.TestCase): + def test_import_conductor_local(self): + self.flags(use_local=True, group='conductor') + self.assertTrue(isinstance(conductor.API(), + conductor_api.LocalAPI)) + + def test_import_conductor_rpc(self): + self.flags(use_local=False, group='conductor') + self.assertTrue(isinstance(conductor.API(), + conductor_api.API)) |