From f39d6549d4e57941b14f328fa5a52a3a5f925d42 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sun, 18 Jul 2010 18:15:12 +0100 Subject: [PATCH 01/11] In preparation for XenAPI support, refactor the interface between nova.compute and the hypervisor (i.e. libvirt). compute.node is no longer coupled tightly with libvirt. Instead, hypervisor connections are handled through a simple abstract interface. This has the additional advantage that there is no need to riddle the code with FLAGS.fake_libvirt checks, as we now have an interface behind which we can mock. The libvirt-specific code, and the fakevirt code used for unit tests, have moved into nova.virt. The fake_libvirt flag has been replaced with a connection_type flag, that will allow us to specify different connection types. The disk image handling (S3 or local disk image fetch) has moved into nova.virt.images, where it will be easier to share between connection types. The power_state values (Instance.RUNNING etc) and the INSTANCE_TYPES dictionary have moved into their own files (nova.compute.instance_types and nova.compute.power_state) so that we can share them without mutual dependencies between nova.compute.node and nova.virt.libvirt_conn. --- bin/dhcpleasor.py | 2 +- docs/fakes.rst | 4 +- nova/compute/instance_types.py | 30 +++ nova/compute/node.py | 276 +++------------------- nova/compute/power_state.py | 41 ++++ nova/endpoint/cloud.py | 3 +- nova/fakevirt.py | 112 --------- nova/flags.py | 3 +- nova/tests/access_unittest.py | 2 +- nova/tests/cloud_unittest.py | 8 +- nova/tests/fake_flags.py | 2 +- nova/tests/future_unittest.py | 2 +- nova/tests/model_unittest.py | 2 +- nova/tests/network_unittest.py | 2 +- nova/tests/node_unittest.py | 2 +- nova/tests/objectstore_unittest.py | 2 + nova/tests/real_flags.py | 2 +- nova/tests/storage_unittest.py | 2 +- nova/tests/users_unittest.py | 2 +- nova/virt/__init__.py | 15 ++ nova/virt/connection.py | 42 ++++ nova/virt/fake.py | 81 +++++++ nova/virt/images.py | 55 +++++ nova/virt/libvirt_conn.py | 353 +++++++++++++++++++++++++++++ 24 files changed, 667 insertions(+), 378 deletions(-) create mode 100644 nova/compute/instance_types.py create mode 100644 nova/compute/power_state.py delete mode 100644 nova/fakevirt.py create mode 100644 nova/virt/__init__.py create mode 100644 nova/virt/connection.py create mode 100644 nova/virt/fake.py create mode 100644 nova/virt/images.py create mode 100644 nova/virt/libvirt_conn.py diff --git a/bin/dhcpleasor.py b/bin/dhcpleasor.py index 07ff325f16..4a3f374d53 100755 --- a/bin/dhcpleasor.py +++ b/bin/dhcpleasor.py @@ -71,7 +71,7 @@ def main(argv=None): FLAGS.fake_rabbit = True FLAGS.redis_db = 8 FLAGS.network_size = 32 - FLAGS.fake_libvirt=True + FLAGS.connection_type = 'fake' FLAGS.fake_network=True FLAGS.fake_users = True action = argv[1] diff --git a/docs/fakes.rst b/docs/fakes.rst index bea8bc4e96..a993fb4c89 100644 --- a/docs/fakes.rst +++ b/docs/fakes.rst @@ -18,10 +18,10 @@ Nova Fakes ========== -The :mod:`fakevirt` Module +The :mod:`virt.fake` Module -------------------------- -.. automodule:: nova.fakevirt +.. automodule:: nova.virt.fake :members: :undoc-members: :show-inheritance: diff --git a/nova/compute/instance_types.py b/nova/compute/instance_types.py new file mode 100644 index 0000000000..439be3c7d5 --- /dev/null +++ b/nova/compute/instance_types.py @@ -0,0 +1,30 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# Copyright (c) 2010 Citrix Systems, Inc. +# +# 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. + +""" +The built-in instance properties. +""" + +INSTANCE_TYPES = {} +INSTANCE_TYPES['m1.tiny'] = {'memory_mb': 512, 'vcpus': 1, 'local_gb': 0} +INSTANCE_TYPES['m1.small'] = {'memory_mb': 1024, 'vcpus': 1, 'local_gb': 10} +INSTANCE_TYPES['m1.medium'] = {'memory_mb': 2048, 'vcpus': 2, 'local_gb': 10} +INSTANCE_TYPES['m1.large'] = {'memory_mb': 4096, 'vcpus': 4, 'local_gb': 10} +INSTANCE_TYPES['m1.xlarge'] = {'memory_mb': 8192, 'vcpus': 4, 'local_gb': 10} +INSTANCE_TYPES['c1.medium'] = {'memory_mb': 2048, 'vcpus': 4, 'local_gb': 10} diff --git a/nova/compute/node.py b/nova/compute/node.py index d681ec6611..7146d12794 100644 --- a/nova/compute/node.py +++ b/nova/compute/node.py @@ -20,93 +20,48 @@ Compute Node: Runs on each compute node, managing the - hypervisor using libvirt. + hypervisor using the virt module. """ import base64 -import json import logging import os -import shutil import sys from twisted.internet import defer from twisted.internet import task from twisted.application import service - -try: - import libvirt -except Exception, err: - logging.warning('no libvirt found') - from nova import exception -from nova import fakevirt from nova import flags from nova import process from nova import utils -from nova.compute import disk from nova.compute import model from nova.compute import network -from nova.objectstore import image # for image_path flag +from nova.compute import power_state +from nova.compute.instance_types import INSTANCE_TYPES +from nova.virt import connection as virt_connection from nova.volume import storage FLAGS = flags.FLAGS -flags.DEFINE_string('libvirt_xml_template', - utils.abspath('compute/libvirt.xml.template'), - 'Libvirt XML Template') -flags.DEFINE_bool('use_s3', True, - 'whether to get images from s3 or use local copy') flags.DEFINE_string('instances_path', utils.abspath('../instances'), 'where instances are stored on disk') -INSTANCE_TYPES = {} -INSTANCE_TYPES['m1.tiny'] = {'memory_mb': 512, 'vcpus': 1, 'local_gb': 0} -INSTANCE_TYPES['m1.small'] = {'memory_mb': 1024, 'vcpus': 1, 'local_gb': 10} -INSTANCE_TYPES['m1.medium'] = {'memory_mb': 2048, 'vcpus': 2, 'local_gb': 10} -INSTANCE_TYPES['m1.large'] = {'memory_mb': 4096, 'vcpus': 4, 'local_gb': 10} -INSTANCE_TYPES['m1.xlarge'] = {'memory_mb': 8192, 'vcpus': 4, 'local_gb': 10} -INSTANCE_TYPES['c1.medium'] = {'memory_mb': 2048, 'vcpus': 4, 'local_gb': 10} - - -def _image_path(path=''): - return os.path.join(FLAGS.images_path, path) - - -def _image_url(path): - return "%s:%s/_images/%s" % (FLAGS.s3_host, FLAGS.s3_port, path) - class Node(object, service.Service): """ Manages the running instances. """ def __init__(self): - """ load configuration options for this node and connect to libvirt """ + """ load configuration options for this node and connect to the hypervisor""" super(Node, self).__init__() self._instances = {} - self._conn = self._get_connection() + self._conn = virt_connection.get_connection() self._pool = process.ProcessPool() self.instdir = model.InstanceDirectory() # TODO(joshua): This needs to ensure system state, specifically: modprobe aoe - def _get_connection(self): - """ returns a libvirt connection object """ - # TODO(termie): maybe lazy load after initial check for permissions - # TODO(termie): check whether we can be disconnected - if FLAGS.fake_libvirt: - conn = fakevirt.FakeVirtConnection.instance() - else: - auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], - 'root', - None] - conn = libvirt.openAuth('qemu:///system', auth, 0) - if conn == None: - logging.error('Failed to open connection to the hypervisor') - sys.exit(1) - return conn - def noop(self): """ simple test of an AMQP message call """ return defer.succeed('PONG') @@ -122,8 +77,7 @@ class Node(object, service.Service): def adopt_instances(self): """ if there are instances already running, adopt them """ return defer.succeed(0) - instance_names = [self._conn.lookupByID(x).name() - for x in self._conn.listDomainsID()] + instance_names = self._conn.list_instances() for name in instance_names: try: new_inst = Instance.fromName(self._conn, self._pool, name) @@ -155,7 +109,7 @@ class Node(object, service.Service): logging.exception("model server went away") yield - # @exception.wrap_exception + @exception.wrap_exception def run_instance(self, instance_id, **_kwargs): """ launch a new instance with specified options """ logging.debug("Starting instance %s..." % (instance_id)) @@ -174,8 +128,7 @@ class Node(object, service.Service): logging.info("Instances current state is %s", new_inst.state) if new_inst.is_running(): raise exception.Error("Instance is already running") - d = new_inst.spawn() - return d + new_inst.spawn() @exception.wrap_exception def terminate_instance(self, instance_id): @@ -309,20 +262,6 @@ class Instance(object): self.datamodel.save() logging.debug("Finished init of Instance with id of %s" % name) - def toXml(self): - # TODO(termie): cache? - logging.debug("Starting the toXML method") - libvirt_xml = open(FLAGS.libvirt_xml_template).read() - xml_info = self.datamodel.copy() - # TODO(joshua): Make this xml express the attached disks as well - - # TODO(termie): lazy lazy hack because xml is annoying - xml_info['nova'] = json.dumps(self.datamodel.copy()) - libvirt_xml = libvirt_xml % xml_info - logging.debug("Finished the toXML method") - - return libvirt_xml - @classmethod def fromName(cls, conn, pool, name): """ use the saved data for reloading the instance """ @@ -333,7 +272,7 @@ class Instance(object): def set_state(self, state_code, state_description=None): self.datamodel['state'] = state_code if not state_description: - state_description = STATE_NAMES[state_code] + state_description = power_state.name(state_code) self.datamodel['state_description'] = state_description self.datamodel.save() @@ -347,37 +286,29 @@ class Instance(object): return self.datamodel['name'] def is_pending(self): - return (self.state == Instance.NOSTATE or self.state == 'pending') + return (self.state == power_state.NOSTATE or self.state == 'pending') def is_destroyed(self): - return self.state == Instance.SHUTOFF + return self.state == power_state.SHUTOFF def is_running(self): logging.debug("Instance state is: %s" % self.state) - return (self.state == Instance.RUNNING or self.state == 'running') + return (self.state == power_state.RUNNING or self.state == 'running') def describe(self): return self.datamodel def info(self): - logging.debug("Getting info for dom %s" % self.name) - virt_dom = self._conn.lookupByName(self.name) - (state, max_mem, mem, num_cpu, cpu_time) = virt_dom.info() - return {'state': state, - 'max_mem': max_mem, - 'mem': mem, - 'num_cpu': num_cpu, - 'cpu_time': cpu_time, - 'node_name': FLAGS.node_name} - - def basepath(self, path=''): - return os.path.abspath(os.path.join(self.datamodel['basepath'], path)) + result = self._conn.get_info(self.name) + result['node_name'] = FLAGS.node_name + return result def update_state(self): self.datamodel.update(self.info()) self.set_state(self.state) self.datamodel.save() # Extra, but harmless + @defer.inlineCallbacks @exception.wrap_exception def destroy(self): if self.is_destroyed(): @@ -385,38 +316,9 @@ class Instance(object): raise exception.Error('trying to destroy already destroyed' ' instance: %s' % self.name) - self.set_state(Instance.NOSTATE, 'shutting_down') - try: - virt_dom = self._conn.lookupByName(self.name) - virt_dom.destroy() - except Exception, _err: - pass - # If the instance is already terminated, we're still happy - d = defer.Deferred() - d.addCallback(lambda x: self._cleanup()) - d.addCallback(lambda x: self.datamodel.destroy()) - # TODO(termie): short-circuit me for tests - # WE'LL save this for when we do shutdown, - # instead of destroy - but destroy returns immediately - timer = task.LoopingCall(f=None) - def _wait_for_shutdown(): - try: - self.update_state() - if self.state == Instance.SHUTDOWN: - timer.stop() - d.callback(None) - except Exception: - self.set_state(Instance.SHUTDOWN) - timer.stop() - d.callback(None) - timer.f = _wait_for_shutdown - timer.start(interval=0.5, now=True) - return d - - def _cleanup(self): - target = os.path.abspath(self.datamodel['basepath']) - logging.info("Deleting instance files at %s", target) - shutil.rmtree(target) + self.set_state(power_state.NOSTATE, 'shutting_down') + yield self._conn.destroy(self) + self.datamodel.destroy() @defer.inlineCallbacks @exception.wrap_exception @@ -427,136 +329,26 @@ class Instance(object): 'instance: %s (state: %s)' % (self.name, self.state)) logging.debug('rebooting instance %s' % self.name) - self.set_state(Instance.NOSTATE, 'rebooting') - yield self._conn.lookupByName(self.name).destroy() - self._conn.createXML(self.toXml(), 0) - - d = defer.Deferred() - timer = task.LoopingCall(f=None) - def _wait_for_reboot(): - try: - self.update_state() - if self.is_running(): - logging.debug('rebooted instance %s' % self.name) - timer.stop() - d.callback(None) - except Exception: - self.set_state(Instance.SHUTDOWN) - timer.stop() - d.callback(None) - timer.f = _wait_for_reboot - timer.start(interval=0.5, now=True) - yield d - - def _fetch_s3_image(self, image, path): - url = _image_url('%s/image' % image) - d = self._pool.simpleExecute('curl --silent %s -o %s' % (url, path)) - return d - - def _fetch_local_image(self, image, path): - source = _image_path('%s/image' % image) - d = self._pool.simpleExecute('cp %s %s' % (source, path)) - return d - - @defer.inlineCallbacks - def _create_image(self, libvirt_xml): - # syntactic nicety - data = self.datamodel - basepath = self.basepath - - # ensure directories exist and are writable - yield self._pool.simpleExecute('mkdir -p %s' % basepath()) - yield self._pool.simpleExecute('chmod 0777 %s' % basepath()) - - - # TODO(termie): these are blocking calls, it would be great - # if they weren't. - logging.info('Creating image for: %s', data['instance_id']) - f = open(basepath('libvirt.xml'), 'w') - f.write(libvirt_xml) - f.close() - - if FLAGS.fake_libvirt: - logging.info('fake_libvirt, nothing to do for create_image') - raise defer.returnValue(None); - - if FLAGS.use_s3: - _fetch_file = self._fetch_s3_image - else: - _fetch_file = self._fetch_local_image - - if not os.path.exists(basepath('disk')): - yield _fetch_file(data['image_id'], basepath('disk-raw')) - if not os.path.exists(basepath('kernel')): - yield _fetch_file(data['kernel_id'], basepath('kernel')) - if not os.path.exists(basepath('ramdisk')): - yield _fetch_file(data['ramdisk_id'], basepath('ramdisk')) - - execute = lambda cmd, input=None: self._pool.simpleExecute(cmd=cmd, - input=input, - error_ok=1) - - key = data['key_data'] - net = None - if FLAGS.simple_network: - with open(FLAGS.simple_network_template) as f: - net = f.read() % {'address': data['private_dns_name'], - 'network': FLAGS.simple_network_network, - 'netmask': FLAGS.simple_network_netmask, - 'gateway': FLAGS.simple_network_gateway, - 'broadcast': FLAGS.simple_network_broadcast, - 'dns': FLAGS.simple_network_dns} - if key or net: - logging.info('Injecting data into image %s', data['image_id']) - yield disk.inject_data(basepath('disk-raw'), key, net, execute=execute) - - if os.path.exists(basepath('disk')): - yield self._pool.simpleExecute('rm -f %s' % basepath('disk')) - - bytes = (INSTANCE_TYPES[data['instance_type']]['local_gb'] - * 1024 * 1024 * 1024) - yield disk.partition( - basepath('disk-raw'), basepath('disk'), bytes, execute=execute) + self.set_state(power_state.NOSTATE, 'rebooting') + yield self._conn.reboot(self) + self.update_state() @defer.inlineCallbacks @exception.wrap_exception def spawn(self): - self.set_state(Instance.NOSTATE, 'spawning') + self.set_state(power_state.NOSTATE, 'spawning') logging.debug("Starting spawn in Instance") - - xml = self.toXml() - self.set_state(Instance.NOSTATE, 'launching') - logging.info('self %s', self) try: - yield self._create_image(xml) - self._conn.createXML(xml, 0) - # TODO(termie): this should actually register - # a callback to check for successful boot - logging.debug("Instance is running") - - local_d = defer.Deferred() - timer = task.LoopingCall(f=None) - def _wait_for_boot(): - try: - self.update_state() - if self.is_running(): - logging.debug('booted instance %s' % self.name) - timer.stop() - local_d.callback(None) - except Exception: - self.set_state(Instance.SHUTDOWN) - logging.error('Failed to boot instance %s' % self.name) - timer.stop() - local_d.callback(None) - timer.f = _wait_for_boot - timer.start(interval=0.5, now=True) + yield self._conn.spawn(self) except Exception, ex: logging.debug(ex) - self.set_state(Instance.SHUTDOWN) + self.set_state(power_state.SHUTDOWN) + self.update_state() @exception.wrap_exception def console_output(self): - if not FLAGS.fake_libvirt: + # FIXME: Abstract this for Xen + if FLAGS.connection_type == 'libvirt': fname = os.path.abspath( os.path.join(self.datamodel['basepath'], 'console.log')) with open(fname, 'r') as f: @@ -564,13 +356,3 @@ class Instance(object): else: console = 'FAKE CONSOLE OUTPUT' return defer.succeed(console) - -STATE_NAMES = { - Instance.NOSTATE : 'pending', - Instance.RUNNING : 'running', - Instance.BLOCKED : 'blocked', - Instance.PAUSED : 'paused', - Instance.SHUTDOWN : 'shutdown', - Instance.SHUTOFF : 'shutdown', - Instance.CRASHED : 'crashed', -} diff --git a/nova/compute/power_state.py b/nova/compute/power_state.py new file mode 100644 index 0000000000..b27aa4677a --- /dev/null +++ b/nova/compute/power_state.py @@ -0,0 +1,41 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# Copyright (c) 2010 Citrix Systems, Inc. +# +# 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. + +"""The various power states that a VM can be in.""" + +NOSTATE = 0x00 +RUNNING = 0x01 +BLOCKED = 0x02 +PAUSED = 0x03 +SHUTDOWN = 0x04 +SHUTOFF = 0x05 +CRASHED = 0x06 + + +def name(code): + d = { + NOSTATE : 'pending', + RUNNING : 'running', + BLOCKED : 'blocked', + PAUSED : 'paused', + SHUTDOWN: 'shutdown', + SHUTOFF : 'shutdown', + CRASHED : 'crashed', + } + return d[code] diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 3b7b4804b2..51f5c859b8 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -39,6 +39,7 @@ from nova.auth import users from nova.compute import model from nova.compute import network from nova.compute import node +from nova.compute.instance_types import INSTANCE_TYPES from nova.endpoint import images from nova.volume import storage @@ -103,7 +104,7 @@ class CloudController(object): result = {} for instance in self.instdir.all: if instance['project_id'] == project_id: - line = '%s slots=%d' % (instance['private_dns_name'], node.INSTANCE_TYPES[instance['instance_type']]['vcpus']) + line = '%s slots=%d' % (instance['private_dns_name'], INSTANCE_TYPES[instance['instance_type']]['vcpus']) if instance['key_name'] in result: result[instance['key_name']].append(line) else: diff --git a/nova/fakevirt.py b/nova/fakevirt.py deleted file mode 100644 index bcbeae548f..0000000000 --- a/nova/fakevirt.py +++ /dev/null @@ -1,112 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -""" -A fake (in-memory) hypervisor+api. Allows nova testing w/o KVM and libvirt. -""" - -import StringIO -from xml.etree import ElementTree - - -class FakeVirtConnection(object): - # FIXME: networkCreateXML, listNetworks don't do anything since - # they aren't exercised in tests yet - - def __init__(self): - self.next_index = 0 - self.instances = {} - - @classmethod - def instance(cls): - if not hasattr(cls, '_instance'): - cls._instance = cls() - return cls._instance - - def lookupByID(self, i): - return self.instances[str(i)] - - def listDomainsID(self): - return self.instances.keys() - - def listNetworks(self): - return [] - - def lookupByName(self, instance_id): - for x in self.instances.values(): - if x.name() == instance_id: - return x - raise Exception('no instance found for instance_id: %s' % instance_id) - - def networkCreateXML(self, xml): - pass - - def createXML(self, xml, flags): - # parse the xml :( - xml_stringio = StringIO.StringIO(xml) - - my_xml = ElementTree.parse(xml_stringio) - name = my_xml.find('name').text - - fake_instance = FakeVirtInstance(conn=self, - index=str(self.next_index), - name=name, - xml=my_xml) - self.instances[str(self.next_index)] = fake_instance - self.next_index += 1 - - def _removeInstance(self, i): - self.instances.pop(str(i)) - - -class FakeVirtInstance(object): - NOSTATE = 0x00 - RUNNING = 0x01 - BLOCKED = 0x02 - PAUSED = 0x03 - SHUTDOWN = 0x04 - SHUTOFF = 0x05 - CRASHED = 0x06 - - def __init__(self, conn, index, name, xml): - self._conn = conn - self._destroyed = False - self._name = name - self._index = index - self._state = self.RUNNING - - def name(self): - return self._name - - def destroy(self): - if self._state == self.SHUTOFF: - raise Exception('instance already destroyed: %s' % self.name()) - self._state = self.SHUTDOWN - self._conn._removeInstance(self._index) - - def info(self): - return [self._state, 0, 2, 0, 0] - - def XMLDesc(self, flags): - return open('fakevirtinstance.xml', 'r').read() - - def blockStats(self, disk): - return [0L, 0L, 0L, 0L, null] - - def interfaceStats(self, iface): - return [0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L] diff --git a/nova/flags.py b/nova/flags.py index 60245a3491..f9ebb28f7b 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -36,14 +36,13 @@ DEFINE_bool = DEFINE_bool # Define any app-specific flags in their own files, docs at: # http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#39 +DEFINE_string('connection_type', 'libvirt', 'libvirt or fake') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_integer('s3_internal_port', 3334, 's3 port') DEFINE_string('s3_host', '127.0.0.1', 's3 host') #DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') DEFINE_string('compute_topic', 'compute', 'the topic compute nodes listen on') DEFINE_string('storage_topic', 'storage', 'the topic storage nodes listen on') -DEFINE_bool('fake_libvirt', False, - 'whether to use a fake libvirt or not') DEFINE_bool('verbose', False, 'show debug output') DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit') DEFINE_bool('fake_network', False, 'should we use fake network devices and addresses') diff --git a/nova/tests/access_unittest.py b/nova/tests/access_unittest.py index 8500dd0cbd..6cf7e893d7 100644 --- a/nova/tests/access_unittest.py +++ b/nova/tests/access_unittest.py @@ -33,7 +33,7 @@ class Context(object): class AccessTestCase(test.BaseTestCase): def setUp(self): super(AccessTestCase, self).setUp() - FLAGS.fake_libvirt = True + FLAGS.connection_type = 'fake' FLAGS.fake_storage = True um = UserManager.instance() # Make test users diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index b8614fdc85..8040f63312 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -39,7 +39,7 @@ FLAGS = flags.FLAGS class CloudTestCase(test.BaseTestCase): def setUp(self): super(CloudTestCase, self).setUp() - self.flags(fake_libvirt=True, + self.flags(connection_type='fake', fake_storage=True, fake_users=True) @@ -72,7 +72,7 @@ class CloudTestCase(test.BaseTestCase): users.UserManager.instance().delete_user('admin') def test_console_output(self): - if FLAGS.fake_libvirt: + if FLAGS.connection_type == 'fake': logging.debug("Can't test instances without a real virtual env.") return instance_id = 'foo' @@ -83,7 +83,7 @@ class CloudTestCase(test.BaseTestCase): rv = yield self.node.terminate_instance(instance_id) def test_run_instances(self): - if FLAGS.fake_libvirt: + if FLAGS.connection_type == 'fake': logging.debug("Can't test instances without a real virtual env.") return image_id = FLAGS.default_image @@ -104,7 +104,7 @@ class CloudTestCase(test.BaseTestCase): break self.assert_(rv) - if not FLAGS.fake_libvirt: + if connection_type != 'fake': time.sleep(45) # Should use boto for polling here for reservations in rv['reservationSet']: # for res_id in reservations.keys(): diff --git a/nova/tests/fake_flags.py b/nova/tests/fake_flags.py index d32f40d8f9..5fcd2bcac9 100644 --- a/nova/tests/fake_flags.py +++ b/nova/tests/fake_flags.py @@ -20,7 +20,7 @@ from nova import flags FLAGS = flags.FLAGS -FLAGS.fake_libvirt = True +FLAGS.connection_type = 'fake' FLAGS.fake_storage = True FLAGS.fake_rabbit = True FLAGS.fake_network = True diff --git a/nova/tests/future_unittest.py b/nova/tests/future_unittest.py index da5470ffe5..31ec83065c 100644 --- a/nova/tests/future_unittest.py +++ b/nova/tests/future_unittest.py @@ -39,7 +39,7 @@ FLAGS = flags.FLAGS class AdminTestCase(test.BaseTestCase): def setUp(self): super(AdminTestCase, self).setUp() - self.flags(fake_libvirt=True, + self.flags(connection_type='fake', fake_rabbit=True) self.conn = rpc.Connection.instance() diff --git a/nova/tests/model_unittest.py b/nova/tests/model_unittest.py index 1bd7e527f0..b9eb2ac964 100644 --- a/nova/tests/model_unittest.py +++ b/nova/tests/model_unittest.py @@ -34,7 +34,7 @@ FLAGS = flags.FLAGS class ModelTestCase(test.TrialTestCase): def setUp(self): super(ModelTestCase, self).setUp() - self.flags(fake_libvirt=True, + self.flags(connection_type='fake', fake_storage=True, fake_users=True) diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index a822cc1d98..45ee6dbc7b 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -33,7 +33,7 @@ from nova import utils class NetworkTestCase(test.TrialTestCase): def setUp(self): super(NetworkTestCase, self).setUp() - self.flags(fake_libvirt=True, + self.flags(connection_type='fake', fake_storage=True, fake_network=True, network_size=32) diff --git a/nova/tests/node_unittest.py b/nova/tests/node_unittest.py index 93942d79e7..86d9775fd5 100644 --- a/nova/tests/node_unittest.py +++ b/nova/tests/node_unittest.py @@ -57,7 +57,7 @@ class NodeConnectionTestCase(test.TrialTestCase): def setUp(self): logging.getLogger().setLevel(logging.DEBUG) super(NodeConnectionTestCase, self).setUp() - self.flags(fake_libvirt=True, + self.flags(connection_type='fake', fake_storage=True, fake_users=True) self.node = node.Node() diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index f47ca7f00c..f22256aaff 100644 --- a/nova/tests/objectstore_unittest.py +++ b/nova/tests/objectstore_unittest.py @@ -25,6 +25,8 @@ import tempfile from nova import flags from nova import objectstore +from nova.objectstore import bucket # for buckets_path flag +from nova.objectstore import image # for images_path flag from nova import test from nova.auth import users diff --git a/nova/tests/real_flags.py b/nova/tests/real_flags.py index 9e106f2276..690fb640af 100644 --- a/nova/tests/real_flags.py +++ b/nova/tests/real_flags.py @@ -20,7 +20,7 @@ from nova import flags FLAGS = flags.FLAGS -FLAGS.fake_libvirt = False +FLAGS.connection_type = 'libvirt' FLAGS.fake_storage = False FLAGS.fake_rabbit = False FLAGS.fake_network = False diff --git a/nova/tests/storage_unittest.py b/nova/tests/storage_unittest.py index 60576d74f1..f400cd2fdb 100644 --- a/nova/tests/storage_unittest.py +++ b/nova/tests/storage_unittest.py @@ -34,7 +34,7 @@ class StorageTestCase(test.TrialTestCase): super(StorageTestCase, self).setUp() self.mynode = node.Node() self.mystorage = None - self.flags(fake_libvirt=True, + self.flags(connection_type='fake', fake_storage=True) self.mystorage = storage.BlockStore() diff --git a/nova/tests/users_unittest.py b/nova/tests/users_unittest.py index 3017210750..824d5cff6a 100644 --- a/nova/tests/users_unittest.py +++ b/nova/tests/users_unittest.py @@ -35,7 +35,7 @@ class UserTestCase(test.BaseTestCase): flush_db = False def setUp(self): super(UserTestCase, self).setUp() - self.flags(fake_libvirt=True, + self.flags(connection_type='fake', fake_storage=True) self.users = users.UserManager.instance() diff --git a/nova/virt/__init__.py b/nova/virt/__init__.py new file mode 100644 index 0000000000..3d598c463c --- /dev/null +++ b/nova/virt/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Citrix Systems, Inc. +# +# 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. diff --git a/nova/virt/connection.py b/nova/virt/connection.py new file mode 100644 index 0000000000..25c8174159 --- /dev/null +++ b/nova/virt/connection.py @@ -0,0 +1,42 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# Copyright (c) 2010 Citrix Systems, Inc. +# +# 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 import flags +from nova.virt import fake +from nova.virt import libvirt_conn + + +FLAGS = flags.FLAGS + + +def get_connection(read_only=False): + # TODO(termie): maybe lazy load after initial check for permissions + # TODO(termie): check whether we can be disconnected + t = FLAGS.connection_type + if t == 'fake': + conn = fake.get_connection(read_only) + elif t == 'libvirt': + conn = libvirt_conn.get_connection(read_only) + else: + raise Exception('Unknown connection type "%s"' % t) + + if conn is None: + logging.error('Failed to open connection to the hypervisor') + sys.exit(1) + return conn diff --git a/nova/virt/fake.py b/nova/virt/fake.py new file mode 100644 index 0000000000..d9ae5ac961 --- /dev/null +++ b/nova/virt/fake.py @@ -0,0 +1,81 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# Copyright (c) 2010 Citrix Systems, Inc. +# +# 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. + +""" +A fake (in-memory) hypervisor+api. Allows nova testing w/o a hypervisor. +""" + +import logging + +from nova.compute import power_state + + +def get_connection(_): + # The read_only parameter is ignored. + return FakeConnection.instance() + + +class FakeConnection(object): + def __init__(self): + self.instances = {} + + @classmethod + def instance(cls): + if not hasattr(cls, '_instance'): + cls._instance = cls() + return cls._instance + + def list_instances(self): + return self.instances.keys() + + def spawn(self, instance): + fake_instance = FakeInstance() + self.instances[instance.name] = fake_instance + fake_instance._state = power_state.RUNNING + + def reboot(self, instance): + pass + + def destroy(self, instance): + del self.instances[instance.name] + + def get_info(self, instance_id): + i = self.instances[instance_id] + return {'state': i._state, + 'max_mem': 0, + 'mem': 0, + 'num_cpu': 2, + 'cpu_time': 0} + + def list_disks(self, instance_id): + return ['A_DISK'] + + def list_interfaces(self, instance_id): + return ['A_VIF'] + + def block_stats(self, instance_id, disk_id): + return [0L, 0L, 0L, 0L, null] + + def interface_stats(self, instance_id, iface_id): + return [0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L] + + +class FakeInstance(object): + def __init__(self): + self._state = power_state.NOSTATE diff --git a/nova/virt/images.py b/nova/virt/images.py new file mode 100644 index 0000000000..0b11c134e3 --- /dev/null +++ b/nova/virt/images.py @@ -0,0 +1,55 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# Copyright (c) 2010 Citrix Systems, Inc. +# +# 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. + +""" +Handling of VM disk images. +""" + +import os.path + +from nova import flags + +FLAGS = flags.FLAGS + +flags.DEFINE_bool('use_s3', True, + 'whether to get images from s3 or use local copy') + + +def fetch(pool, image, path): + if FLAGS.use_s3: + f = _fetch_s3_image + else: + f = _fetch_local_image + return f(pool, image, path) + +def _fetch_s3_image(pool, image, path): + url = _image_url('%s/image' % image) + d = pool.simpleExecute('curl --silent %s -o %s' % (url, path)) + return d + +def _fetch_local_image(pool, image, path): + source = _image_path('%s/image' % image) + d = pool.simpleExecute('cp %s %s' % (source, path)) + return d + +def _image_path(path): + return os.path.join(FLAGS.images_path, path) + +def _image_url(path): + return "%s:%s/_images/%s" % (FLAGS.s3_host, FLAGS.s3_port, path) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py new file mode 100644 index 0000000000..74fec650e6 --- /dev/null +++ b/nova/virt/libvirt_conn.py @@ -0,0 +1,353 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# Copyright (c) 2010 Citrix Systems, Inc. +# +# 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. + +""" +A connection to a hypervisor (e.g. KVM) through libvirt. +""" + +import json +import logging +import os.path +import shutil +import sys + +from twisted.internet import defer +from twisted.internet import task + +from nova import exception +from nova import flags +from nova import process +from nova import utils +from nova.compute import disk +from nova.compute import instance_types +from nova.compute import power_state +from nova.virt import images + +libvirt = None +libxml2 = None + +FLAGS = flags.FLAGS +flags.DEFINE_string('libvirt_xml_template', + utils.abspath('compute/libvirt.xml.template'), + 'Libvirt XML Template') + +def get_connection(read_only): + # These are loaded late so that there's no need to install these + # libraries when not using libvirt. + global libvirt + global libxml2 + if libvirt is None: + libvirt = __import__('libvirt') + if libxml2 is None: + libxml2 = __import__('libxml2') + return LibvirtConnection(read_only) + + +class LibvirtConnection(object): + def __init__(self, read_only): + auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], + 'root', + None] + if read_only: + self._conn = libvirt.openReadOnly('qemu:///system') + else: + self._conn = libvirt.openAuth('qemu:///system', auth, 0) + self._pool = process.ProcessPool() + + + def list_instances(self): + return [self._conn.lookupByID(x).name() + for x in self._conn.listDomainsID()] + + + def destroy(self, instance): + try: + virt_dom = self._conn.lookupByName(instance.name) + virt_dom.destroy() + except Exception, _err: + pass + # If the instance is already terminated, we're still happy + d = defer.Deferred() + d.addCallback(lambda x: self._cleanup()) + # FIXME: What does this comment mean? + # TODO(termie): short-circuit me for tests + # WE'LL save this for when we do shutdown, + # instead of destroy - but destroy returns immediately + timer = task.LoopingCall(f=None) + def _wait_for_shutdown(): + try: + instance.update_state() + if instance.state == power_state.SHUTDOWN: + timer.stop() + d.callback(None) + except Exception: + instance.set_state(power_state.SHUTDOWN) + timer.stop() + d.callback(None) + timer.f = _wait_for_shutdown + timer.start(interval=0.5, now=True) + return d + + + def _cleanup(self, instance): + target = os.path.abspath(instance.datamodel['basepath']) + logging.info("Deleting instance files at %s", target) + shutil.rmtree(target) + + + @defer.inlineCallbacks + @exception.wrap_exception + def reboot(self, instance): + xml = self.toXml(instance) + yield self._conn.lookupByName(instance.name).destroy() + yield self._conn.createXML(xml, 0) + + d = defer.Deferred() + timer = task.LoopingCall(f=None) + def _wait_for_reboot(): + try: + instance.update_state() + if instance.is_running(): + logging.debug('rebooted instance %s' % instance.name) + timer.stop() + d.callback(None) + except Exception, exn: + logging.error('_wait_for_reboot failed: %s' % exn) + instance.set_state(power_state.SHUTDOWN) + timer.stop() + d.callback(None) + timer.f = _wait_for_reboot + timer.start(interval=0.5, now=True) + yield d + + + @defer.inlineCallbacks + @exception.wrap_exception + def spawn(self, instance): + xml = self.toXml(instance) + instance.set_state(power_state.NOSTATE, 'launching') + yield self._create_image(instance, xml) + yield self._conn.createXML(xml, 0) + # TODO(termie): this should actually register + # a callback to check for successful boot + logging.debug("Instance is running") + + local_d = defer.Deferred() + timer = task.LoopingCall(f=None) + def _wait_for_boot(): + try: + instance.update_state() + if instance.is_running(): + logging.debug('booted instance %s' % instance.name) + timer.stop() + local_d.callback(None) + except Exception, exn: + logging.error("_wait_for_boot exception %s" % exn) + self.set_state(power_state.SHUTDOWN) + logging.error('Failed to boot instance %s' % instance.name) + timer.stop() + local_d.callback(None) + timer.f = _wait_for_boot + timer.start(interval=0.5, now=True) + yield local_d + + + @defer.inlineCallbacks + def _create_image(self, instance, libvirt_xml): + # syntactic nicety + data = instance.datamodel + basepath = lambda x='': self.basepath(instance, x) + + # ensure directories exist and are writable + yield self._pool.simpleExecute('mkdir -p %s' % basepath()) + yield self._pool.simpleExecute('chmod 0777 %s' % basepath()) + + + # TODO(termie): these are blocking calls, it would be great + # if they weren't. + logging.info('Creating image for: %s', data['instance_id']) + f = open(basepath('libvirt.xml'), 'w') + f.write(libvirt_xml) + f.close() + + if not os.path.exists(basepath('disk')): + yield images.fetch(self._pool, data['image_id'], basepath('disk-raw')) + if not os.path.exists(basepath('kernel')): + yield images.fetch(self._pool, data['kernel_id'], basepath('kernel')) + if not os.path.exists(basepath('ramdisk')): + yield images.fetch(self._pool, data['ramdisk_id'], basepath('ramdisk')) + + execute = lambda cmd, input=None: self._pool.simpleExecute(cmd=cmd, + input=input, + error_ok=1) + + key = data['key_data'] + net = None + if FLAGS.simple_network: + with open(FLAGS.simple_network_template) as f: + net = f.read() % {'address': data['private_dns_name'], + 'network': FLAGS.simple_network_network, + 'netmask': FLAGS.simple_network_netmask, + 'gateway': FLAGS.simple_network_gateway, + 'broadcast': FLAGS.simple_network_broadcast, + 'dns': FLAGS.simple_network_dns} + if key or net: + logging.info('Injecting data into image %s', data['image_id']) + yield disk.inject_data(basepath('disk-raw'), key, net, execute=execute) + + if os.path.exists(basepath('disk')): + yield self._pool.simpleExecute('rm -f %s' % basepath('disk')) + + bytes = (instance_types.INSTANCE_TYPES[data['instance_type']]['local_gb'] + * 1024 * 1024 * 1024) + yield disk.partition( + basepath('disk-raw'), basepath('disk'), bytes, execute=execute) + + + def basepath(self, instance, path=''): + return os.path.abspath(os.path.join(instance.datamodel['basepath'], path)) + + + def toXml(self, instance): + # TODO(termie): cache? + logging.debug("Starting the toXML method") + libvirt_xml = open(FLAGS.libvirt_xml_template).read() + xml_info = instance.datamodel.copy() + # TODO(joshua): Make this xml express the attached disks as well + + # TODO(termie): lazy lazy hack because xml is annoying + xml_info['nova'] = json.dumps(instance.datamodel.copy()) + libvirt_xml = libvirt_xml % xml_info + logging.debug("Finished the toXML method") + + return libvirt_xml + + + def get_info(self, instance_id): + virt_dom = self._conn.lookupByName(instance_id) + (state, max_mem, mem, num_cpu, cpu_time) = virt_dom.info() + return {'state': state, + 'max_mem': max_mem, + 'mem': mem, + 'num_cpu': num_cpu, + 'cpu_time': cpu_time} + + + def get_disks(self, instance_id): + """ + Note that this function takes an instance ID, not an Instance, so + that it can be called by monitor. + + Returns a list of all block devices for this domain. + """ + domain = self._conn.lookupByName(instance_id) + # TODO(devcamcar): Replace libxml2 with etree. + xml = domain.XMLDesc(0) + doc = None + + try: + doc = libxml2.parseDoc(xml) + except: + return [] + + ctx = doc.xpathNewContext() + disks = [] + + try: + ret = ctx.xpathEval('/domain/devices/disk') + + for node in ret: + devdst = None + + for child in node.children: + if child.name == 'target': + devdst = child.prop('dev') + + if devdst == None: + continue + + disks.append(devdst) + finally: + if ctx != None: + ctx.xpathFreeContext() + if doc != None: + doc.freeDoc() + + return disks + + + def get_interfaces(self, instance_id): + """ + Note that this function takes an instance ID, not an Instance, so + that it can be called by monitor. + + Returns a list of all network interfaces for this instance. + """ + domain = self._conn.lookupByName(instance_id) + # TODO(devcamcar): Replace libxml2 with etree. + xml = domain.XMLDesc(0) + doc = None + + try: + doc = libxml2.parseDoc(xml) + except: + return [] + + ctx = doc.xpathNewContext() + interfaces = [] + + try: + ret = ctx.xpathEval('/domain/devices/interface') + + for node in ret: + devdst = None + + for child in node.children: + if child.name == 'target': + devdst = child.prop('dev') + + if devdst == None: + continue + + interfaces.append(devdst) + finally: + if ctx != None: + ctx.xpathFreeContext() + if doc != None: + doc.freeDoc() + + return interfaces + + + def block_stats(self, instance_id, disk): + """ + Note that this function takes an instance ID, not an Instance, so + that it can be called by monitor. + """ + domain = self._conn.lookupByName(instance_id) + return domain.blockStats(disk) + + + def interface_stats(self, instance_id, interface): + """ + Note that this function takes an instance ID, not an Instance, so + that it can be called by monitor. + """ + domain = self._conn.lookupByName(instance_id) + return domain.interfaceStats(interface) From 2dd9438e192b5d760db0c5cee5bb5ded1ec5a9cc Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sun, 18 Jul 2010 18:24:17 +0100 Subject: [PATCH 02/11] Remove the tight coupling between nova.compute.monitor and libvirt. The libvirt-specific code was placed in nova.virt.libvirt_conn by the last changeset. This greatly simplifies the monitor code, and puts the libvirt-specific XML record parsing in a libvirt-specific place. --- nova/compute/monitor.py | 127 ++++++++-------------------------------- 1 file changed, 23 insertions(+), 104 deletions(-) diff --git a/nova/compute/monitor.py b/nova/compute/monitor.py index fdc86b031c..19e1a483df 100644 --- a/nova/compute/monitor.py +++ b/nova/compute/monitor.py @@ -27,7 +27,6 @@ Instance Monitoring: import boto import boto.s3 import datetime -import libxml2 import logging import os import rrdtool @@ -37,12 +36,8 @@ from twisted.internet import defer from twisted.internet import task from twisted.application import service -try: - import libvirt -except Exception, err: - logging.warning('no libvirt found') - from nova import flags +from nova.virt import connection as virt_connection FLAGS = flags.FLAGS @@ -130,83 +125,6 @@ def init_rrd(instance, name): *RRD_VALUES[name] ) -def get_disks(domain): - """ - Returns a list of all block devices for this domain. - """ - # TODO(devcamcar): Replace libxml2 with etree. - xml = domain.XMLDesc(0) - doc = None - - try: - doc = libxml2.parseDoc(xml) - except: - return [] - - ctx = doc.xpathNewContext() - disks = [] - - try: - ret = ctx.xpathEval('/domain/devices/disk') - - for node in ret: - devdst = None - - for child in node.children: - if child.name == 'target': - devdst = child.prop('dev') - - if devdst == None: - continue - - disks.append(devdst) - finally: - if ctx != None: - ctx.xpathFreeContext() - if doc != None: - doc.freeDoc() - - return disks - -def get_interfaces(domain): - """ - Returns a list of all network interfaces for this instance. - """ - # TODO(devcamcar): Replace libxml2 with etree. - xml = domain.XMLDesc(0) - doc = None - - try: - doc = libxml2.parseDoc(xml) - except: - return [] - - ctx = doc.xpathNewContext() - interfaces = [] - - try: - ret = ctx.xpathEval('/domain/devices/interface') - - for node in ret: - devdst = None - - for child in node.children: - if child.name == 'target': - devdst = child.prop('dev') - - if devdst == None: - continue - - interfaces.append(devdst) - finally: - if ctx != None: - ctx.xpathFreeContext() - if doc != None: - doc.freeDoc() - - return interfaces - - def graph_cpu(instance, duration): """ Creates a graph of cpu usage for the specified instance and duration. @@ -317,10 +235,9 @@ def store_graph(instance_id, filename): class Instance(object): - def __init__(self, conn, domain): + def __init__(self, conn, instance_id): self.conn = conn - self.domain = domain - self.instance_id = domain.name() + self.instance_id = instance_id self.last_updated = datetime.datetime.min self.cputime = 0 self.cputime_last_updated = None @@ -385,14 +302,14 @@ class Instance(object): """ Returns cpu usage statistics for this instance. """ - info = self.domain.info() + info = self.conn.get_info(self.instance_id) # Get the previous values. cputime_last = self.cputime cputime_last_updated = self.cputime_last_updated # Get the raw CPU time used in nanoseconds. - self.cputime = float(info[4]) + self.cputime = float(info['cpu_time']) self.cputime_last_updated = utcnow() logging.debug('CPU: %d', self.cputime) @@ -413,8 +330,8 @@ class Instance(object): logging.debug('cputime_delta = %s', cputime_delta) # Get the number of virtual cpus in this domain. - vcpus = int(info[3]) - + vcpus = int(info['num_cpu']) + logging.debug('vcpus = %d', vcpus) # Calculate CPU % used and cap at 100. @@ -427,14 +344,13 @@ class Instance(object): rd = 0 wr = 0 - # Get a list of block devices for this instance. - disks = get_disks(self.domain) + disks = self.conn.get_disks(self.instance_id) # Aggregate the read and write totals. for disk in disks: try: rd_req, rd_bytes, wr_req, wr_bytes, errs = \ - self.domain.blockStats(disk) + self.conn.block_stats(self.instance_id, disk) rd += rd_bytes wr += wr_bytes except TypeError: @@ -451,13 +367,12 @@ class Instance(object): rx = 0 tx = 0 - # Get a list of all network interfaces for this instance. - interfaces = get_interfaces(self.domain) + interfaces = self.conn.get_interfaces(self.instance_id) # Aggregate the in and out totals. for interface in interfaces: try: - stats = self.domain.interfaceStats(interface) + stats = self.conn.interface_stats(self.instance_id, interface) rx += stats[0] tx += stats[4] except TypeError: @@ -493,20 +408,24 @@ class InstanceMonitor(object, service.Service): Update resource usage for all running instances. """ try: - conn = libvirt.openReadOnly(None) - except libvirt.libvirtError: - logging.exception('unexpected libvirt error') + conn = virt_connection.get_connection(read_only=True) + except Exception, exn: + logging.exception('unexpected exception getting connection') time.sleep(FLAGS.monitoring_instances_delay) return - domain_ids = conn.listDomainsID() - + domain_ids = conn.list_instances() + try: + self.updateInstances_(conn, domain_ids) + except Exception, exn: + logging.exception('updateInstances_') + + def updateInstances_(self, conn, domain_ids): for domain_id in domain_ids: if not domain_id in self._instances: - domain = conn.lookupByID(domain_id) - instance = Instance(conn, domain) + instance = Instance(conn, domain_id) self._instances[domain_id] = instance - logging.debug('Found instance: %s', instance.instance_id) + logging.debug('Found instance: %s', domain_id) for key in self._instances.keys(): instance = self._instances[key] From 1046fd21fad35fdb9922f667017937ec94774498 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sun, 18 Jul 2010 18:28:21 +0100 Subject: [PATCH 03/11] First commit of XenAPI-specific code (i.e. connections to the open-source community project Xen Cloud Platform, or the open-source commercial product Citrix XenServer). A new connection type has been added (xenapi) which means that libvirt calls will be replaced with calls to XenAPI. This support depends upon the XenAPI library (available from xen.org). However, the library is loaded on-demand, so there is no need for the library to be present unless you actually want to use XenAPI. The same is true of libvirt, so there is no need to have libvirt present if you are only using XenAPI. This work is incomplete. The VMs don't actually start yet, and won't until we settle on the proposed refactoring for bootable volumes. Also, VM console support is not yet refactored. Finally, xenapi.py does not support the metrics monitoring calls used by monitor.py (block_stats, interface_stats). XenAPI already includes HTTP access to RRDs for retrieving aggregated stats, so there is no need for monitor.py at all (xapi does it for you). The plan is to arrange for those RRDs to be passed straight to the reporting layer without the need for the aggregation code in nova-monitorinstance. --- nova/flags.py | 2 +- nova/virt/connection.py | 3 + nova/virt/xenapi.py | 138 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 nova/virt/xenapi.py diff --git a/nova/flags.py b/nova/flags.py index f9ebb28f7b..caf2d2e933 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -36,7 +36,7 @@ DEFINE_bool = DEFINE_bool # Define any app-specific flags in their own files, docs at: # http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#39 -DEFINE_string('connection_type', 'libvirt', 'libvirt or fake') +DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_integer('s3_internal_port', 3334, 's3 port') DEFINE_string('s3_host', '127.0.0.1', 's3 host') diff --git a/nova/virt/connection.py b/nova/virt/connection.py index 25c8174159..004adb19db 100644 --- a/nova/virt/connection.py +++ b/nova/virt/connection.py @@ -20,6 +20,7 @@ from nova import flags from nova.virt import fake from nova.virt import libvirt_conn +from nova.virt import xenapi FLAGS = flags.FLAGS @@ -33,6 +34,8 @@ def get_connection(read_only=False): conn = fake.get_connection(read_only) elif t == 'libvirt': conn = libvirt_conn.get_connection(read_only) + elif t == 'xenapi': + conn = xenapi.get_connection(read_only) else: raise Exception('Unknown connection type "%s"' % t) diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py new file mode 100644 index 0000000000..46ff9c5e42 --- /dev/null +++ b/nova/virt/xenapi.py @@ -0,0 +1,138 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Citrix Systems, Inc. +# +# 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. + +""" +A connection to XenServer or Xen Cloud Platform. +""" + +import logging + +from twisted.internet import defer +from twisted.internet import task + +from nova import exception +from nova import flags +from nova import process +from nova.compute import power_state + +XenAPI = None + + +def get_connection(_): + """Note that XenAPI doesn't have a read-only connection mode, so + the read_only parameter is ignored.""" + # This is loaded late so that there's no need to install this + # library when not using XenAPI. + global XenAPI + if XenAPI is not None: + XenAPI = __import__('XenAPI') + return XenAPIConnection('http://eli.testdev.hq.xensource.com', + 'root', 'xensource') + + +class XenAPIConnection(object): + + def __init__(self, url, user, pw): + self._conn = XenAPI.Session(url) + self._conn.login_with_password(user, pw) + self._pool = process.Pool() + + def list_instances(self): + result = [self._conn.xenapi.VM.get_name_label(vm) \ + for vm in self._conn.xenapi.VM.get_all()] + + @defer.inlineCallbacks + @exception.wrap_exception + def spawn(self, instance): + vm = self.lookup(instance.name) + if vm is not None: + raise Exception('Attempted to create non-unique name %s' % + instance.name) + mem = str(long(instance.datamodel['memory_kb']) * 1024) + vcpus = str(instance.datamodel['vcpus']) + rec = { + 'name_label': instance.name, + 'name_description': '', + 'is_a_template': False, + 'memory_static_min': '0', + 'memory_static_max': mem, + 'memory_dynamic_min': mem, + 'memory_dynamic_max': mem, + 'VCPUs_at_startup': vcpus, + 'VCPUs_max': vcpus, + 'VCPUs_params': {}, + 'actions_after_shutdown': 'destroy', + 'actions_after_reboot': 'restart', + 'actions_after_crash': 'destroy', + 'PV_bootloader': '', + 'PV_kernel': instance.datamodel['kernel_id'], + 'PV_ramdisk': instance.datamodel['ramdisk_id'], + 'PV_args': '', + 'PV_bootloader_args': '', + 'PV_legacy_args': '', + 'HVM_boot_policy': '', + 'HVM_boot_params': {}, + 'platform': {}, + 'PCI_bus': '', + 'recommendations': '', + 'affinity': '', + 'user_version': '0', + 'other_config': {}, + } + vm = yield self._conn.xenapi.VM.create(rec) + #yield self._conn.xenapi.VM.start(vm, False, False) + + + def reboot(self, instance): + vm = self.lookup(instance.name) + if vm is None: + raise Exception('instance not present %s' % instance.name) + yield self._conn.xenapi.VM.clean_reboot(vm) + + def destroy(self, instance): + vm = self.lookup(instance.name) + if vm is None: + raise Exception('instance not present %s' % instance.name) + yield self._conn.xenapi.VM.destroy(vm) + + def get_info(self, instance_id): + vm = self.lookup(instance_id) + if vm is None: + raise Exception('instance not present %s' % instance.name) + rec = self._conn.xenapi.VM.get_record(vm) + return {'state': power_state_from_xenapi[rec['power_state']], + 'max_mem': long(rec['memory_static_max']) >> 10, + 'mem': long(rec['memory_dynamic_max']) >> 10, + 'num_cpu': rec['VCPUs_max'], + 'cpu_time': 0} + + def lookup(self, i): + vms = self._conn.xenapi.VM.get_by_name_label(i) + n = len(vms) + if n == 0: + return None + elif n > 1: + raise Exception('duplicate name found: %s' % i) + else: + return vms[0] + + power_state_from_xenapi = { + 'Halted' : power_state.RUNNING, #FIXME + 'Running' : power_state.RUNNING, + 'Paused' : power_state.PAUSED, + 'Suspended': power_state.SHUTDOWN, # FIXME + 'Crashed' : power_state.CRASHED + } From 8b0c70cce2dd914f1ab4caca8883d616c7c669d6 Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Mon, 19 Jul 2010 21:39:33 -0700 Subject: [PATCH 04/11] Map exceptions to 404 / 403 codes, as was done before the move to twisted. However, I don't think this is the right way to do this in Twisted. For example, exceptions thrown after the render method returns will not be mapped --- nova/objectstore/handler.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index c670ee02f4..c3c4486bfd 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -116,7 +116,21 @@ def get_context(request): logging.debug("Authentication Failure: %s" % ex) raise exception.NotAuthorized -class S3(Resource): +class ErrorHandlingResource(Resource): + """Maps exceptions to 404 / 401 codes. Won't work for exceptions thrown after NOT_DONE_YET is returned.""" + # TODO: This needs to be plugged in to the right place in twisted... + # This doesn't look like it's the right place (consider exceptions in getChild; or after NOT_DONE_YET is returned + def render(self, request): + try: + return Resource.render(self, request) + except exception.NotFound: + request.setResponseCode(404) + return '' + except exception.NotAuthorized: + request.setResponseCode(403) + return '' + +class S3(ErrorHandlingResource): """Implementation of an S3-like storage server based on local files.""" def getChild(self, name, request): request.context = get_context(request) @@ -136,7 +150,7 @@ class S3(Resource): }}) return server.NOT_DONE_YET -class BucketResource(Resource): +class BucketResource(ErrorHandlingResource): def __init__(self, name): Resource.__init__(self) self.name = name @@ -186,7 +200,7 @@ class BucketResource(Resource): return '' -class ObjectResource(Resource): +class ObjectResource(ErrorHandlingResource): def __init__(self, bucket, name): Resource.__init__(self) self.bucket = bucket @@ -227,7 +241,7 @@ class ObjectResource(Resource): request.setResponseCode(204) return '' -class ImageResource(Resource): +class ImageResource(ErrorHandlingResource): isLeaf = True def getChild(self, name, request): From f6ae05f993016f45af2c19718a6e84e50e4a775e Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Thu, 22 Jul 2010 11:49:13 -0700 Subject: [PATCH 05/11] Nobody wants to take on this twisted cleanup. It works for now, but could be much nicer if twisted has a nice hook-point for exception mapping --- nova/objectstore/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index c3c4486bfd..098e7a1678 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -118,7 +118,7 @@ def get_context(request): class ErrorHandlingResource(Resource): """Maps exceptions to 404 / 401 codes. Won't work for exceptions thrown after NOT_DONE_YET is returned.""" - # TODO: This needs to be plugged in to the right place in twisted... + # TODO(unassigned) (calling-all-twisted-experts): This needs to be plugged in to the right place in twisted... # This doesn't look like it's the right place (consider exceptions in getChild; or after NOT_DONE_YET is returned def render(self, request): try: From 4c536de1732c531bfb87018826a92de2744e8d1a Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sat, 24 Jul 2010 14:45:35 +0100 Subject: [PATCH 06/11] Add missing import following merge from trunk (cset 150). --- nova/virt/images.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/virt/images.py b/nova/virt/images.py index fd74349b11..12338fd804 100644 --- a/nova/virt/images.py +++ b/nova/virt/images.py @@ -24,6 +24,7 @@ Handling of VM disk images. import os.path from nova import flags +from nova import process FLAGS = flags.FLAGS From 1a53eaeed901f3c789ebdb867b73996ccac608c3 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sun, 25 Jul 2010 15:00:37 +0100 Subject: [PATCH 07/11] Fix assertion "Someone released me too many times: too many tokens!" when more than one process was running at the same time. This was caused by the override of SharedPool.__new__ not stopping ProcessPool.__init__ from being run whenever process.simple_execute is called. When __init__ ran for the second time, the DeferredSemaphore was replaced, and this meant that we ended up releasing a different semaphore to the one that was acquired. --- nova/process.py | 13 ++++++------- nova/tests/process_unittest.py | 7 +++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/nova/process.py b/nova/process.py index d3558ed2e7..8ecef1584b 100644 --- a/nova/process.py +++ b/nova/process.py @@ -205,13 +205,12 @@ class ProcessPool(object): self._pool.release() return rv -class SharedPool(ProcessPool): - _instance = None - def __new__(cls, *args, **kwargs): - if not cls._instance: - cls._instance = super(SharedPool, cls).__new__( - cls, *args, **kwargs) - return cls._instance +_instance = None +def SharedPool(): + global _instance + if _instance is None: + _instance = ProcessPool() + return _instance def simple_execute(cmd, **kwargs): return SharedPool().simple_execute(cmd, **kwargs) diff --git a/nova/tests/process_unittest.py b/nova/tests/process_unittest.py index 1c15b69a0f..c96bb59136 100644 --- a/nova/tests/process_unittest.py +++ b/nova/tests/process_unittest.py @@ -120,3 +120,10 @@ class ProcessTestCase(test.TrialTestCase): pool2 = process.SharedPool() self.assert_(id(pool1) == id(pool2)) + def test_shared_pool_works_as_singleton(self): + d1 = process.simple_execute('sleep 1') + d2 = process.simple_execute('sleep 0.005') + # lp609749: would have failed with + # exceptions.AssertionError: Someone released me too many times: + # too many tokens! + return d1 From b2d769cb92dce5be26288c8e389491cf554b5703 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sun, 25 Jul 2010 15:08:48 +0100 Subject: [PATCH 08/11] Removed creation of process pools. We don't use these any more now that we're using process.simple_execute. --- nova/virt/libvirt_conn.py | 1 - nova/virt/xenapi.py | 1 - 2 files changed, 2 deletions(-) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 39ed9bd780..30a1820574 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -68,7 +68,6 @@ class LibvirtConnection(object): self._conn = libvirt.openReadOnly('qemu:///system') else: self._conn = libvirt.openAuth('qemu:///system', auth, 0) - self._pool = process.ProcessPool() def list_instances(self): diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py index 46ff9c5e42..58fcd79c5e 100644 --- a/nova/virt/xenapi.py +++ b/nova/virt/xenapi.py @@ -48,7 +48,6 @@ class XenAPIConnection(object): def __init__(self, url, user, pw): self._conn = XenAPI.Session(url) self._conn.login_with_password(user, pw) - self._pool = process.Pool() def list_instances(self): result = [self._conn.xenapi.VM.get_name_label(vm) \ From 6d636cd416d4a0f8a778ea9cb04c41de6299714e Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sun, 25 Jul 2010 20:31:53 +0100 Subject: [PATCH 09/11] Fix instance cleanup. --- nova/virt/libvirt_conn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 30a1820574..2c34711bc7 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -83,7 +83,7 @@ class LibvirtConnection(object): pass # If the instance is already terminated, we're still happy d = defer.Deferred() - d.addCallback(lambda x: self._cleanup()) + d.addCallback(lambda _: self._cleanup(instance)) # FIXME: What does this comment mean? # TODO(termie): short-circuit me for tests # WE'LL save this for when we do shutdown, From c5edaa2186add12947185cb1fd47e0a48eccafa9 Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Sun, 25 Jul 2010 20:32:33 +0100 Subject: [PATCH 10/11] Replace hardcoded example URL, username, and password with flags called xenapi_connection_url, xenapi_connection_username, xenapi_connection_password. --- nova/virt/xenapi.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py index 58fcd79c5e..dc372e3e33 100644 --- a/nova/virt/xenapi.py +++ b/nova/virt/xenapi.py @@ -30,6 +30,17 @@ from nova.compute import power_state XenAPI = None +FLAGS = flags.FLAGS +flags.DEFINE_string('xenapi_connection_url', + None, + 'URL for connection to XenServer/Xen Cloud Platform. Required if connection_type=xenapi.') +flags.DEFINE_string('xenapi_connection_username', + 'root', + 'Username for connection to XenServer/Xen Cloud Platform. Used only if connection_type=xenapi.') +flags.DEFINE_string('xenapi_connection_password', + None, + 'Password for connection to XenServer/Xen Cloud Platform. Used only if connection_type=xenapi.') + def get_connection(_): """Note that XenAPI doesn't have a read-only connection mode, so @@ -37,10 +48,14 @@ def get_connection(_): # This is loaded late so that there's no need to install this # library when not using XenAPI. global XenAPI - if XenAPI is not None: + if XenAPI is None: XenAPI = __import__('XenAPI') - return XenAPIConnection('http://eli.testdev.hq.xensource.com', - 'root', 'xensource') + url = FLAGS.xenapi_connection_url + username = FLAGS.xenapi_connection_username + password = FLAGS.xenapi_connection_password + if not url or password is None: + raise Exception('Must specify xenapi_connection_url, xenapi_connection_username (optionally), and xenapi_connection_password to use connection_type=xenapi') + return XenAPIConnection(url, username, password) class XenAPIConnection(object): From 04a6a0267e7dc0f4e587e43f23b4acf0dcef52fc Mon Sep 17 00:00:00 2001 From: Ewan Mellor Date: Thu, 29 Jul 2010 00:58:33 +0100 Subject: [PATCH 11/11] More merges from trunk. Not everything came over the first time. --- nova/auth/ldapdriver.py | 7 +++++-- nova/auth/manager.py | 6 +++--- nova/compute/linux_net.py | 7 ++++--- nova/datastore.py | 2 +- nova/endpoint/api.py | 2 +- nova/endpoint/cloud.py | 8 ++++++-- nova/objectstore/handler.py | 10 +++++----- nova/process.py | 13 +++++++------ nova/tests/auth_unittest.py | 6 ++++++ nova/tests/process_unittest.py | 7 ------- setup.py | 2 +- 11 files changed, 39 insertions(+), 31 deletions(-) diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py index 1591c88e90..055e8332bb 100644 --- a/nova/auth/ldapdriver.py +++ b/nova/auth/ldapdriver.py @@ -120,10 +120,13 @@ class LdapDriver(object): '(objectclass=novaKeyPair)') return [self.__to_key_pair(uid, attr) for attr in attrs] - def get_projects(self): + def get_projects(self, uid=None): """Retrieve list of projects""" + filter = '(objectclass=novaProject)' + if uid: + filter = "(&%s(member=%s))" % (filter, self.__uid_to_dn(uid)) attrs = self.__find_objects(FLAGS.ldap_project_subtree, - '(objectclass=novaProject)') + filter) return [self.__to_project(attr) for attr in attrs] def create_user(self, name, access_key, secret_key, is_admin): diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 66027f6c21..7307f673bc 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -547,10 +547,10 @@ class AuthManager(object): if project_dict: return Project(**project_dict) - def get_projects(self): - """Retrieves list of all projects""" + def get_projects(self, user=None): + """Retrieves list of projects, optionally filtered by user""" with self.driver() as drv: - project_list = drv.get_projects() + project_list = drv.get_projects(User.safe_id(user)) if not project_list: return [] return [Project(**project_dict) for project_dict in project_list] diff --git a/nova/compute/linux_net.py b/nova/compute/linux_net.py index 861ce779bb..4a4b4c8a8a 100644 --- a/nova/compute/linux_net.py +++ b/nova/compute/linux_net.py @@ -94,7 +94,7 @@ def bridge_create(net): execute("sudo ifconfig %s up" % net['bridge_name']) def dnsmasq_cmd(net): - cmd = ['sudo dnsmasq', + cmd = ['sudo -E dnsmasq', ' --strict-order', ' --bind-interfaces', ' --conf-file=', @@ -143,8 +143,9 @@ def start_dnsmasq(network): if os.path.exists(lease_file): os.unlink(lease_file) - # FLAGFILE in env - env = {'FLAGFILE' : FLAGS.dhcpbridge_flagfile} + # FLAGFILE and DNSMASQ_INTERFACE in env + env = {'FLAGFILE': FLAGS.dhcpbridge_flagfile, + 'DNSMASQ_INTERFACE': network['bridge_name']} execute(dnsmasq_cmd(network), addl_env=env) def stop_dnsmasq(network): diff --git a/nova/datastore.py b/nova/datastore.py index 660ad9d903..9c25923347 100644 --- a/nova/datastore.py +++ b/nova/datastore.py @@ -103,7 +103,7 @@ class BasicModel(object): @classmethod def _redis_name(cls): - return cls.override_type or cls.__name__ + return cls.override_type or cls.__name__.lower() @classmethod def lookup(cls, identifier): diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py index 8915e47425..78a18b9ea0 100755 --- a/nova/endpoint/api.py +++ b/nova/endpoint/api.py @@ -266,7 +266,7 @@ class APIRequestHandler(tornado.web.RequestHandler): # Authenticate the request. try: - (user, project) = users.UserManager.instance().authenticate( + (user, project) = manager.AuthManager().authenticate( access, signature, auth_params, diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 21581ffd2e..8a4edbc0b3 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -516,8 +516,12 @@ class CloudController(object): # get defaults from imagestore image_id = image['imageId'] - kernel_id = image.get('kernelId', None) - ramdisk_id = image.get('ramdiskId', None) + kernel_id = image.get('kernelId', FLAGS.default_kernel) + ramdisk_id = image.get('ramdiskId', FLAGS.default_ramdisk) + + # make sure we have access to kernel and ramdisk + self._get_image(context, kernel_id) + self._get_image(context, ramdisk_id) # API parameters overrides of defaults kernel_id = kwargs.get('kernel_id', kernel_id) diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index 344d75f6bf..b4d7e61790 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -297,12 +297,12 @@ class ImagesResource(Resource): def render_POST(self, request): """ update image attributes: public/private """ - image_id = self.get_argument('image_id', u'') - operation = self.get_argument('operation', u'') + image_id = get_argument(request, 'image_id', u'') + operation = get_argument(request, 'operation', u'') image_object = image.Image(image_id) - if not image.is_authorized(request.context): + if not image_object.is_authorized(request.context): raise exception.NotAuthorized image_object.set_public(operation=='add') @@ -311,10 +311,10 @@ class ImagesResource(Resource): def render_DELETE(self, request): """ delete a registered image """ - image_id = self.get_argument("image_id", u"") + image_id = get_argument(request, "image_id", u"") image_object = image.Image(image_id) - if not image.is_authorized(request.context): + if not image_object.is_authorized(request.context): raise exception.NotAuthorized image_object.delete() diff --git a/nova/process.py b/nova/process.py index 8ecef1584b..d3558ed2e7 100644 --- a/nova/process.py +++ b/nova/process.py @@ -205,12 +205,13 @@ class ProcessPool(object): self._pool.release() return rv -_instance = None -def SharedPool(): - global _instance - if _instance is None: - _instance = ProcessPool() - return _instance +class SharedPool(ProcessPool): + _instance = None + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super(SharedPool, cls).__new__( + cls, *args, **kwargs) + return cls._instance def simple_execute(cmd, **kwargs): return SharedPool().simple_execute(cmd, **kwargs) diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py index 073ff71d2e..2167c2385a 100644 --- a/nova/tests/auth_unittest.py +++ b/nova/tests/auth_unittest.py @@ -193,9 +193,15 @@ class AuthTestCase(test.BaseTestCase): for vpn in vpns: vpn.destroy() + def test_214_can_retrieve_project_by_user(self): + project = self.manager.create_project('testproj2', 'test2', 'Another test project', ['test2']) + self.assert_(len(self.manager.get_projects()) > 1) + self.assertEqual(len(self.manager.get_projects('test2')), 1) + def test_299_can_delete_project(self): self.manager.delete_project('testproj') self.assertFalse(filter(lambda p: p.name == 'testproj', self.manager.get_projects())) + self.manager.delete_project('testproj2') def test_999_can_delete_users(self): self.manager.delete_user('test1') diff --git a/nova/tests/process_unittest.py b/nova/tests/process_unittest.py index c96bb59136..1c15b69a0f 100644 --- a/nova/tests/process_unittest.py +++ b/nova/tests/process_unittest.py @@ -120,10 +120,3 @@ class ProcessTestCase(test.TrialTestCase): pool2 = process.SharedPool() self.assert_(id(pool1) == id(pool2)) - def test_shared_pool_works_as_singleton(self): - d1 = process.simple_execute('sleep 1') - d2 = process.simple_execute('sleep 0.005') - # lp609749: would have failed with - # exceptions.AssertionError: Someone released me too many times: - # too many tokens! - return d1 diff --git a/setup.py b/setup.py index 127d014b19..50d5f2a3d9 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ from setuptools import setup, find_packages setup(name='nova', - version='0.9.0', + version='0.9.1', description='cloud computing fabric controller', author='OpenStack', author_email='nova@lists.launchpad.net',