From c524508bc58aa561b81c3133526c981cce835a59 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 20 Sep 2010 01:50:08 -0700 Subject: added rescue mode support and made reboot work from any state --- nova/compute/manager.py | 37 +++++++++--- nova/virt/fake.py | 12 ++++ nova/virt/libvirt.rescue.qemu.xml.template | 33 +++++++++++ nova/virt/libvirt.rescue.uml.xml.template | 26 +++++++++ nova/virt/libvirt_conn.py | 94 +++++++++++++++++++++++++----- 5 files changed, 179 insertions(+), 23 deletions(-) create mode 100644 nova/virt/libvirt.rescue.qemu.xml.template create mode 100644 nova/virt/libvirt.rescue.uml.xml.template (limited to 'nova') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 954227b42..56e08f881 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -122,17 +122,8 @@ class ComputeManager(manager.Manager): @exception.wrap_exception def reboot_instance(self, context, instance_id): """Reboot an instance on this server.""" - self._update_state(context, instance_id) instance_ref = self.db.instance_get(context, instance_id) - if instance_ref['state'] != power_state.RUNNING: - raise exception.Error( - 'trying to reboot a non-running' - 'instance: %s (state: %s excepted: %s)' % - (instance_ref['str_id'], - instance_ref['state'], - power_state.RUNNING)) - logging.debug('instance %s: rebooting', instance_ref['name']) self.db.instance_set_state(context, instance_id, @@ -141,6 +132,34 @@ class ComputeManager(manager.Manager): yield self.driver.reboot(instance_ref) self._update_state(context, instance_id) + @defer.inlineCallbacks + @exception.wrap_exception + def rescue_instance(self, context, instance_id): + """Rescue an instance on this server.""" + instance_ref = self.db.instance_get(context, instance_id) + + logging.debug('instance %s: rescuing', instance_ref['name']) + self.db.instance_set_state(context, + instance_id, + power_state.NOSTATE, + 'rescuing') + yield self.driver.rescue(instance_ref) + self._update_state(context, instance_id) + + @defer.inlineCallbacks + @exception.wrap_exception + def unrescue_instance(self, context, instance_id): + """Rescue an instance on this server.""" + instance_ref = self.db.instance_get(context, instance_id) + + logging.debug('instance %s: unrescuing', instance_ref['name']) + self.db.instance_set_state(context, + instance_id, + power_state.NOSTATE, + 'unrescuing') + yield self.driver.unrescue(instance_ref) + self._update_state(context, instance_id) + @exception.wrap_exception def get_console_output(self, context, instance_id): """Send the console output for an instance.""" diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 4ae6afcc4..3e88060f6 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -119,6 +119,18 @@ class FakeConnection(object): """ return defer.succeed(None) + def rescue(self, instance): + """ + Rescue the specified instance. + """ + return defer.succeed(None) + + def unrescue(self, instance): + """ + Unrescue the specified instance. + """ + return defer.succeed(None) + def destroy(self, instance): """ Destroy (shutdown and delete) the specified instance. diff --git a/nova/virt/libvirt.rescue.qemu.xml.template b/nova/virt/libvirt.rescue.qemu.xml.template new file mode 100644 index 000000000..164326452 --- /dev/null +++ b/nova/virt/libvirt.rescue.qemu.xml.template @@ -0,0 +1,33 @@ + + %(name)s + + hvm + %(basepath)s/rescue-kernel + %(basepath)s/rescue-ramdisk + root=/dev/vda1 console=ttyS0 + + + + + %(memory_kb)s + %(vcpus)s + + + + + + + + + + + + + + + + + + + + diff --git a/nova/virt/libvirt.rescue.uml.xml.template b/nova/virt/libvirt.rescue.uml.xml.template new file mode 100644 index 000000000..836f47532 --- /dev/null +++ b/nova/virt/libvirt.rescue.uml.xml.template @@ -0,0 +1,26 @@ + + %(name)s + %(memory_kb)s + + %(type)s + /usr/bin/linux + /dev/ubda1 + + + + + + + + + + + + + + + + + + + diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index d868e083c..b9edc8e85 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -44,6 +44,16 @@ libxml2 = None FLAGS = flags.FLAGS +flags.DEFINE_string('libvirt_rescue_xml_template', + utils.abspath('virt/libvirt.rescue.qemu.xml.template'), + 'Libvirt RESCUE XML Template for QEmu/KVM') +flags.DEFINE_string('libvirt_rescue_uml_xml_template', + utils.abspath('virt/libvirt.rescue.uml.xml.template'), + 'Libvirt RESCUE XML Template for user-mode-linux') +# TODO(vish): These flags should probably go into a shared location +flags.DEFINE_string('rescue_image_id', 'ami-rescue', 'Rescue ami image') +flags.DEFINE_string('rescue_kernel_id', 'aki-rescue', 'Rescue aki image') +flags.DEFINE_string('rescue_ramdisk_id', 'ari-rescue', 'Rescue ari image') flags.DEFINE_string('libvirt_xml_template', utils.abspath('virt/libvirt.qemu.xml.template'), 'Libvirt XML Template for QEmu/KVM') @@ -76,9 +86,12 @@ def get_connection(read_only): class LibvirtConnection(object): def __init__(self, read_only): - self.libvirt_uri, template_file = self.get_uri_and_template() + (self.libvirt_uri, + template_file, + rescue_file)= self.get_uri_and_templates() self.libvirt_xml = open(template_file).read() + self.rescue_xml = open(rescue_file).read() self._wrapped_conn = None self.read_only = read_only @@ -100,14 +113,16 @@ class LibvirtConnection(object): return False raise - def get_uri_and_template(self): + def get_uri_and_templates(self): if FLAGS.libvirt_type == 'uml': uri = FLAGS.libvirt_uri or 'uml:///system' template_file = FLAGS.libvirt_uml_xml_template + rescue_file = FLAGS.libvirt_rescue_uml_xml_template else: uri = FLAGS.libvirt_uri or 'qemu:///system' template_file = FLAGS.libvirt_xml_template - return uri, template_file + rescue_file = FLAGS.libvirt_rescue_xml_template + return uri, template_file, rescue_file def _connect(self, uri, read_only): auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], @@ -123,7 +138,7 @@ class LibvirtConnection(object): return [self._conn.lookupByID(x).name() for x in self._conn.listDomainsID()] - def destroy(self, instance): + def destroy(self, instance, cleanup=True): try: virt_dom = self._conn.lookupByName(instance['name']) virt_dom.destroy() @@ -131,7 +146,8 @@ class LibvirtConnection(object): pass # If the instance is already terminated, we're still happy d = defer.Deferred() - d.addCallback(lambda _: self._cleanup(instance)) + if 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, @@ -181,8 +197,8 @@ class LibvirtConnection(object): @defer.inlineCallbacks @exception.wrap_exception def reboot(self, instance): + yield self.destroy(instance, False) xml = self.to_xml(instance) - yield self._conn.lookupByName(instance['name']).destroy() yield self._conn.createXML(xml, 0) d = defer.Deferred() @@ -206,6 +222,46 @@ class LibvirtConnection(object): timer.start(interval=0.5, now=True) yield d + @defer.inlineCallbacks + @exception.wrap_exception + def rescue(self, instance): + yield self.destroy(instance, False) + + xml = self.to_xml(instance, rescue=True) + rescue_images = {'image_id': FLAGS.rescue_image_id, + 'kernel_id': FLAGS.rescue_kernel_id, + 'ramdisk_id': FLAGS.rescue_ramdisk_id} + yield self._create_image(instance, xml, 'rescue-', rescue_images) + yield self._conn.createXML(xml, 0) + + d = defer.Deferred() + timer = task.LoopingCall(f=None) + def _wait_for_rescue(): + try: + state = self.get_info(instance['name'])['state'] + db.instance_set_state(None, instance['id'], state) + if state == power_state.RUNNING: + logging.debug('instance %s: rescued', instance['name']) + timer.stop() + d.callback(None) + except Exception, exn: + logging.error('_wait_for_rescue failed: %s', exn) + db.instance_set_state(None, + instance['id'], + power_state.SHUTDOWN) + timer.stop() + d.callback(None) + timer.f = _wait_for_rescue + timer.start(interval=0.5, now=True) + yield d + + @defer.inlineCallbacks + @exception.wrap_exception + def unrescue(self, instance): + # NOTE(vish): Because reboot destroys and recreates an instance using + # the normal xml file, we can just call reboot here + yield self.reboot(instance) + @defer.inlineCallbacks @exception.wrap_exception def spawn(self, instance): @@ -243,15 +299,16 @@ class LibvirtConnection(object): yield local_d @defer.inlineCallbacks - def _create_image(self, inst, libvirt_xml): + def _create_image(self, inst, libvirt_xml, prefix='', disk_images=None): # syntactic nicety - basepath = lambda fname='': os.path.join(FLAGS.instances_path, + basepath = lambda fname='', prefix=prefix: os.path.join( + FLAGS.instances_path, inst['name'], - fname) + prefix + fname) # ensure directories exist and are writable - yield process.simple_execute('mkdir -p %s' % basepath()) - yield process.simple_execute('chmod 0777 %s' % basepath()) + yield process.simple_execute('mkdir -p %s' % basepath(prefix='')) + yield process.simple_execute('chmod 0777 %s' % basepath(prefix='')) # TODO(termie): these are blocking calls, it would be great @@ -261,11 +318,17 @@ class LibvirtConnection(object): f.write(libvirt_xml) f.close() - os.close(os.open(basepath('console.log'), os.O_CREAT | os.O_WRONLY, 0660)) + # NOTE(vish): No need add the prefix to console.log + os.close(os.open(basepath('console.log', ''), + os.O_CREAT | os.O_WRONLY, 0660)) user = manager.AuthManager().get_user(inst['user_id']) project = manager.AuthManager().get_project(inst['project_id']) + if not disk_images: + disk_images = {'image_id': inst['image_id'], + 'kernel_id': inst['kernel_id'], + 'ramdisk_id': inst['ramdisk_id']} if not os.path.exists(basepath('disk')): yield images.fetch(inst.image_id, basepath('disk-raw'), user, project) if not os.path.exists(basepath('kernel')): @@ -311,7 +374,7 @@ class LibvirtConnection(object): yield process.simple_execute('sudo chown root %s' % basepath('disk')) - def to_xml(self, instance): + def to_xml(self, instance, rescue=False): # TODO(termie): cache? logging.debug('instance %s: starting toXML method', instance['name']) network = db.project_get_network(None, instance['project_id']) @@ -325,7 +388,10 @@ class LibvirtConnection(object): 'vcpus': instance_type['vcpus'], 'bridge_name': network['bridge'], 'mac_address': instance['mac_address']} - libvirt_xml = self.libvirt_xml % xml_info + if rescue: + libvirt_xml = self.rescue_xml % xml_info + else: + libvirt_xml = self.libvirt_xml % xml_info logging.debug('instance %s: finished toXML method', instance['name']) return libvirt_xml -- cgit From 198af0ef9e65bc4c2efe74b9d93cf40210eb77bc Mon Sep 17 00:00:00 2001 From: "jaypipes@gmail.com" <> Date: Thu, 21 Oct 2010 14:29:34 -0400 Subject: Moves db writes into compute manager class. Cleans up sqlalchemy model/api to remove redundant calls for updating what is really a dict. --- nova/api/ec2/cloud.py | 43 ++++++++++++--------- nova/api/openstack/servers.py | 25 ++++++------ nova/compute/manager.py | 35 +++++++++++++++++ nova/db/sqlalchemy/api.py | 66 +++++++++++--------------------- nova/db/sqlalchemy/models.py | 10 +++++ nova/tests/api/openstack/fakes.py | 1 + nova/tests/api/openstack/test_servers.py | 4 +- 7 files changed, 104 insertions(+), 80 deletions(-) (limited to 'nova') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 6d4f58499..096ddf668 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -97,6 +97,7 @@ class CloudController(object): """ def __init__(self): self.network_manager = utils.import_object(FLAGS.network_manager) + self.compute_manager = utils.import_object(FLAGS.compute_manager) self.setup() def __str__(self): @@ -846,27 +847,29 @@ class CloudController(object): elevated = context.elevated() for num in range(num_instances): - instance_ref = db.instance_create(context, base_options) - inst_id = instance_ref['id'] + + instance_data = base_options + instance_data['mac_address'] = utils.generate_mac() + instance_data['launch_index'] = num - for security_group_id in security_groups: - db.instance_add_security_group(elevated, - inst_id, - security_group_id) + instance_ref = self.compute_manager.create_instance(context, + instance_data, + security_groups) - inst = {} - inst['mac_address'] = utils.generate_mac() - inst['launch_index'] = num internal_id = instance_ref['internal_id'] ec2_id = internal_id_to_ec2_id(internal_id) - inst['hostname'] = ec2_id - db.instance_update(context, inst_id, inst) + instance_ref['hostname'] = ec2_id + + self.compute_manager.update_instance(context, + instance_ref['id'], + instance_ref) + # TODO(vish): This probably should be done in the scheduler # or in compute as a call. The network should be # allocated after the host is assigned and setup # can happen at the same time. address = self.network_manager.allocate_fixed_ip(context, - inst_id, + instance_ref['id'], vpn) network_topic = self._get_network_topic(context) rpc.cast(elevated, @@ -878,9 +881,9 @@ class CloudController(object): FLAGS.scheduler_topic, {"method": "run_instance", "args": {"topic": FLAGS.compute_topic, - "instance_id": inst_id}}) + "instance_id": instance_ref['id']}}) logging.debug("Casting to scheduler for %s/%s's instance %s" % - (context.project.name, context.user.name, inst_id)) + (context.project.name, context.user.name, instance_ref['id'])) return self._format_run_instances(context, reservation_id) @@ -907,11 +910,13 @@ class CloudController(object): id_str) continue now = datetime.datetime.utcnow() - db.instance_update(context, - instance_ref['id'], - {'state_description': 'terminating', - 'state': 0, - 'terminated_at': now}) + updated_data = {'state_description': 'terminating', + 'state': 0, + 'terminated_at': now} + self.compute_manager.update_instance(context, + instance_ref['id'], + updated_data) + # FIXME(ja): where should network deallocate occur? address = db.instance_get_floating_address(context, instance_ref['id']) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index a73591ccc..6ce364eb7 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -94,6 +94,7 @@ class Controller(wsgi.Controller): db_driver = FLAGS.db_driver self.db_driver = utils.import_object(db_driver) self.network_manager = utils.import_object(FLAGS.network_manager) + self.compute_manager = utils.import_object(FLAGS.compute_manager) super(Controller, self).__init__() def index(self, req): @@ -241,34 +242,30 @@ class Controller(wsgi.Controller): inst['memory_mb'] = flavor['memory_mb'] inst['vcpus'] = flavor['vcpus'] inst['local_gb'] = flavor['local_gb'] - - ref = self.db_driver.instance_create(ctxt, inst) - inst['id'] = ref.internal_id - inst['mac_address'] = utils.generate_mac() - - #TODO(dietz) is this necessary? inst['launch_index'] = 0 - inst['hostname'] = str(ref.internal_id) - self.db_driver.instance_update(ctxt, inst['id'], inst) + ref = self.compute_manager.create_instance(ctxt, inst) + inst['id'] = ref['internal_id'] + + inst['hostname'] = str(ref['internal_id']) + self.compute_manager.update_instance(ctxt, inst['id'], inst) - network_manager = utils.import_object(FLAGS.network_manager) - address = network_manager.allocate_fixed_ip(ctxt, - inst['id']) + address = self.network_manager.allocate_fixed_ip(ctxt, + inst['id']) # TODO(vish): This probably should be done in the scheduler # network is setup when host is assigned - network_topic = self._get_network_topic(ctxt, network_manager) + network_topic = self._get_network_topic(ctxt) rpc.call(ctxt, network_topic, {"method": "setup_fixed_ip", "args": {"address": address}}) return inst - def _get_network_topic(self, context, network_manager): + def _get_network_topic(self, context): """Retrieves the network host for a project""" - network_ref = network_manager.get_network(context) + network_ref = self.network_manager.get_network(context) host = network_ref['host'] if not host: host = rpc.call(context, diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 523bb8893..c752d954b 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -67,6 +67,41 @@ class ComputeManager(manager.Manager): def refresh_security_group(self, context, security_group_id, **_kwargs): yield self.driver.refresh_security_group(security_group_id) + + def create_instance(self, context, instance_data, security_groups=[]): + """Creates the instance in the datastore and returns the + new instance as a mapping + + :param context: The security context + :param instance_data: mapping of instance options + :param security_groups: list of security group ids to + attach to the instance + + :retval Returns a mapping of the instance information + that has just been created + + """ + instance_ref = self.db.instance_create(context, instance_data) + inst_id = instance_ref['id'] + + elevated = context.elevated() + for security_group_id in security_groups: + self.db.instance_add_security_group(elevated, + inst_id, + security_group_id) + return instance_ref + + def update_instance(self, context, instance_id, instance_data): + """Updates the instance in the datastore + + :param context: The security context + :param instance_data: mapping of instance options + + :retval None + + """ + self.db.instance_update(context, instance_id, instance_data) + @defer.inlineCallbacks @exception.wrap_exception def run_instance(self, context, instance_id, **_kwargs): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 209d6e51f..74fd0fdc8 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -235,8 +235,7 @@ def service_get_by_args(context, host, binary): @require_admin_context def service_create(context, values): service_ref = models.Service() - for (key, value) in values.iteritems(): - service_ref[key] = value + service_ref.update(values) service_ref.save() return service_ref @@ -246,8 +245,7 @@ def service_update(context, service_id, values): session = get_session() with session.begin(): service_ref = service_get(context, service_id, session=session) - for (key, value) in values.iteritems(): - service_ref[key] = value + service_ref.update(values) service_ref.save(session=session) @@ -278,8 +276,7 @@ def floating_ip_allocate_address(context, host, project_id): @require_context def floating_ip_create(context, values): floating_ip_ref = models.FloatingIp() - for (key, value) in values.iteritems(): - floating_ip_ref[key] = value + floating_ip_ref.update(values) floating_ip_ref.save() return floating_ip_ref['address'] @@ -450,8 +447,7 @@ def fixed_ip_associate_pool(context, network_id, instance_id): @require_context def fixed_ip_create(_context, values): fixed_ip_ref = models.FixedIp() - for (key, value) in values.iteritems(): - fixed_ip_ref[key] = value + fixed_ip_ref.update(values) fixed_ip_ref.save() return fixed_ip_ref['address'] @@ -520,8 +516,7 @@ def fixed_ip_update(context, address, values): fixed_ip_ref = fixed_ip_get_by_address(context, address, session=session) - for (key, value) in values.iteritems(): - fixed_ip_ref[key] = value + fixed_ip_ref.update(values) fixed_ip_ref.save(session=session) @@ -534,8 +529,7 @@ def fixed_ip_update(context, address, values): @require_context def instance_create(context, values): instance_ref = models.Instance() - for (key, value) in values.iteritems(): - instance_ref[key] = value + instance_ref.update(values) session = get_session() with session.begin(): @@ -727,8 +721,7 @@ def instance_update(context, instance_id, values): session = get_session() with session.begin(): instance_ref = instance_get(context, instance_id, session=session) - for (key, value) in values.iteritems(): - instance_ref[key] = value + instance_ref.update(values) instance_ref.save(session=session) @@ -750,8 +743,7 @@ def instance_add_security_group(context, instance_id, security_group_id): @require_context def key_pair_create(context, values): key_pair_ref = models.KeyPair() - for (key, value) in values.iteritems(): - key_pair_ref[key] = value + key_pair_ref.update(values) key_pair_ref.save() return key_pair_ref @@ -866,8 +858,7 @@ def network_count_reserved_ips(context, network_id): @require_admin_context def network_create_safe(context, values): network_ref = models.Network() - for (key, value) in values.iteritems(): - network_ref[key] = value + network_ref.update(values) try: network_ref.save() return network_ref @@ -976,8 +967,7 @@ def network_update(context, network_id, values): session = get_session() with session.begin(): network_ref = network_get(context, network_id, session=session) - for (key, value) in values.iteritems(): - network_ref[key] = value + network_ref.update(values) network_ref.save(session=session) @@ -1027,8 +1017,7 @@ def export_device_count(context): @require_admin_context def export_device_create_safe(context, values): export_device_ref = models.ExportDevice() - for (key, value) in values.iteritems(): - export_device_ref[key] = value + export_device_ref.update(values) try: export_device_ref.save() return export_device_ref @@ -1054,8 +1043,7 @@ def auth_get_token(_context, token_hash): def auth_create_token(_context, token): tk = models.AuthToken() - for k,v in token.iteritems(): - tk[k] = v + tk.update(token) tk.save() return tk @@ -1081,8 +1069,7 @@ def quota_get(context, project_id, session=None): @require_admin_context def quota_create(context, values): quota_ref = models.Quota() - for (key, value) in values.iteritems(): - quota_ref[key] = value + quota_ref.update(values) quota_ref.save() return quota_ref @@ -1092,8 +1079,7 @@ def quota_update(context, project_id, values): session = get_session() with session.begin(): quota_ref = quota_get(context, project_id, session=session) - for (key, value) in values.iteritems(): - quota_ref[key] = value + quota_ref.update(values) quota_ref.save(session=session) @@ -1141,8 +1127,7 @@ def volume_attached(context, volume_id, instance_id, mountpoint): @require_context def volume_create(context, values): volume_ref = models.Volume() - for (key, value) in values.iteritems(): - volume_ref[key] = value + volume_ref.update(values) session = get_session() with session.begin(): @@ -1298,8 +1283,7 @@ def volume_update(context, volume_id, values): session = get_session() with session.begin(): volume_ref = volume_get(context, volume_id, session=session) - for (key, value) in values.iteritems(): - volume_ref[key] = value + volume_ref.update(values) volume_ref.save(session=session) @@ -1392,8 +1376,7 @@ def security_group_create(context, values): # FIXME(devcamcar): Unless I do this, rules fails with lazy load exception # once save() is called. This will get cleaned up in next orm pass. security_group_ref.rules - for (key, value) in values.iteritems(): - security_group_ref[key] = value + security_group_ref.update(values) security_group_ref.save() return security_group_ref @@ -1446,8 +1429,7 @@ def security_group_rule_get(context, security_group_rule_id, session=None): @require_context def security_group_rule_create(context, values): security_group_rule_ref = models.SecurityGroupIngressRule() - for (key, value) in values.iteritems(): - security_group_rule_ref[key] = value + security_group_rule_ref.update(values) security_group_rule_ref.save() return security_group_rule_ref @@ -1498,8 +1480,7 @@ def user_get_by_access_key(context, access_key, session=None): @require_admin_context def user_create(_context, values): user_ref = models.User() - for (key, value) in values.iteritems(): - user_ref[key] = value + user_ref.update(values) user_ref.save() return user_ref @@ -1527,8 +1508,7 @@ def user_get_all(context): def project_create(_context, values): project_ref = models.Project() - for (key, value) in values.iteritems(): - project_ref[key] = value + project_ref.update(values) project_ref.save() return project_ref @@ -1590,8 +1570,7 @@ def user_update(context, user_id, values): session = get_session() with session.begin(): user_ref = user_get(context, user_id, session=session) - for (key, value) in values.iteritems(): - user_ref[key] = value + user_ref.update(values) user_ref.save(session=session) @@ -1599,8 +1578,7 @@ def project_update(context, project_id, values): session = get_session() with session.begin(): project_ref = project_get(context, project_id, session=session) - for (key, value) in values.iteritems(): - project_ref[key] = value + project_ref.update(values) project_ref.save(session=session) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index a63bca2b0..853c320e4 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -90,6 +90,16 @@ class NovaBase(object): n = self._i.next().name return n, getattr(self, n) + def update(self, values): + """Make the model object behave like a dict""" + for k, v in values.iteritems(): + setattr(self, k, v) + + def iteritems(self): + """Make the model object behave like a dict""" + return iter(self) + + # TODO(vish): Store images in the database instead of file system #class Image(BASE, NovaBase): # """Represents an image in the datastore""" diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 14170fbb2..f12c7b610 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -30,6 +30,7 @@ from nova import exception as exc import nova.api.openstack.auth from nova.image import service from nova.image.services import glance +from nova.tests import fake_flags from nova.wsgi import Router diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index d1ee533b6..f4a09fd97 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -92,9 +92,7 @@ class ServersTest(unittest.TestCase): pass def instance_create(context, inst): - class Foo(object): - internal_id = 1 - return Foo() + return {'id': 1, 'internal_id': 1} def fake_method(*args, **kwargs): pass -- cgit From 5fdcbd6c831cb3ab2cb04c0eecc68e4b0b9d5a66 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 24 Oct 2010 15:06:42 -0700 Subject: update tests --- nova/tests/virt_unittest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/tests/virt_unittest.py b/nova/tests/virt_unittest.py index ce78d450c..d49383fb7 100644 --- a/nova/tests/virt_unittest.py +++ b/nova/tests/virt_unittest.py @@ -91,7 +91,7 @@ class LibvirtConnTestCase(test.TrialTestCase): FLAGS.libvirt_type = libvirt_type conn = libvirt_conn.LibvirtConnection(True) - uri, template = conn.get_uri_and_template() + uri, _template, _rescue = conn.get_uri_and_templates() self.assertEquals(uri, expected_uri) xml = conn.to_xml(instance_ref) @@ -114,7 +114,7 @@ class LibvirtConnTestCase(test.TrialTestCase): for (libvirt_type, (expected_uri, checks)) in type_uri_map.iteritems(): FLAGS.libvirt_type = libvirt_type conn = libvirt_conn.LibvirtConnection(True) - uri, template = conn.get_uri_and_template() + uri, _template, _rescue = conn.get_uri_and_templates() self.assertEquals(uri, testuri) def tearDown(self): -- cgit From eecef70fcdd173cc54ad8ac7edba9e9b31855134 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 24 Oct 2010 15:37:55 -0700 Subject: add methods to cloud for rescue and unrescue --- nova/api/cloud.py | 18 ++++++++++++++++++ nova/api/ec2/cloud.py | 17 +++++++++++++++-- nova/virt/libvirt_conn.py | 2 +- 3 files changed, 34 insertions(+), 3 deletions(-) (limited to 'nova') diff --git a/nova/api/cloud.py b/nova/api/cloud.py index aa84075dc..46b342d32 100644 --- a/nova/api/cloud.py +++ b/nova/api/cloud.py @@ -36,3 +36,21 @@ def reboot(instance_id, context=None): db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "reboot_instance", "args": {"instance_id": instance_ref['id']}}) + +def rescue(instance_id, context): + """Rescue the given instance.""" + instance_ref = db.instance_get_by_internal_id(context, instance_id) + host = instance_ref['host'] + rpc.cast(context, + db.queue_get_for(context, FLAGS.compute_topic, host), + {"method": "rescue_instance", + "args": {"instance_id": instance_ref['id']}}) + +def unrescue(instance_id, context): + """Unrescue the given instance.""" + instance_ref = db.instance_get_by_internal_id(context, instance_id) + host = instance_ref['host'] + rpc.cast(context, + db.queue_get_for(context, FLAGS.compute_topic, host), + {"method": "unrescue_instance", + "args": {"instance_id": instance_ref['id']}}) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 784697b01..73f0dcc16 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -932,8 +932,21 @@ class CloudController(object): def reboot_instances(self, context, instance_id, **kwargs): """instance_id is a list of instance ids""" - for id_str in instance_id: - cloud.reboot(id_str, context=context) + for ec2_id in instance_id: + internal_id = ec2_id_to_internal_id(ec2_id) + cloud.reboot(internal_id, context=context) + return True + + def rescue_instance(self, context, instance_id, **kwargs): + """This is an extension to the normal ec2_api""" + internal_id = ec2_id_to_internal_id(instance_id) + cloud.rescue(internal_id, context=context) + return True + + def unrescue_instance(self, context, instance_id, **kwargs): + """This is an extension to the normal ec2_api""" + internal_id = ec2_id_to_internal_id(instance_id) + cloud.unrescue(internal_id, context=context) return True def update_instance(self, context, ec2_id, **kwargs): diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 1c75150ea..7d66d8454 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -484,7 +484,7 @@ class LibvirtConnection(object): try: virt_dom = self._conn.lookupByName(instance_name) except: - raise NotFound("Instance %s not found" % instance_name) + raise exception.NotFound("Instance %s not found" % instance_name) (state, max_mem, mem, num_cpu, cpu_time) = virt_dom.info() return {'state': state, 'max_mem': max_mem, -- cgit From 4948fed24d5e16d95f237ec95c1cd305fcc4df95 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 24 Oct 2010 16:04:35 -0700 Subject: pep 8 cleanup and typo in resize --- nova/api/cloud.py | 2 ++ nova/virt/libvirt_conn.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/api/cloud.py b/nova/api/cloud.py index 46b342d32..b8f15019f 100644 --- a/nova/api/cloud.py +++ b/nova/api/cloud.py @@ -37,6 +37,7 @@ def reboot(instance_id, context=None): {"method": "reboot_instance", "args": {"instance_id": instance_ref['id']}}) + def rescue(instance_id, context): """Rescue the given instance.""" instance_ref = db.instance_get_by_internal_id(context, instance_id) @@ -46,6 +47,7 @@ def rescue(instance_id, context): {"method": "rescue_instance", "args": {"instance_id": instance_ref['id']}}) + def unrescue(instance_id, context): """Unrescue the given instance.""" instance_ref = db.instance_get_by_internal_id(context, instance_id) diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 7d66d8454..0096b1400 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -99,7 +99,7 @@ class LibvirtConnection(object): def __init__(self, read_only): (self.libvirt_uri, template_file, - rescue_file)= self.get_uri_and_templates() + rescue_file) = self.get_uri_and_templates() self.libvirt_xml = open(template_file).read() self.rescue_xml = open(rescue_file).read() @@ -258,6 +258,7 @@ class LibvirtConnection(object): d = defer.Deferred() timer = task.LoopingCall(f=None) + def _wait_for_rescue(): try: state = self.get_info(instance['name'])['state'] @@ -273,6 +274,7 @@ class LibvirtConnection(object): power_state.SHUTDOWN) timer.stop() d.callback(None) + timer.f = _wait_for_rescue timer.start(interval=0.5, now=True) yield d @@ -441,7 +443,7 @@ class LibvirtConnection(object): * 1024 * 1024 * 1024) resize = True - if inst['instance_type'] == 'm1.tiny' or prefix=='rescue': + if inst['instance_type'] == 'm1.tiny' or prefix == 'rescue-': resize = False yield disk.partition(basepath('disk-raw'), basepath('disk'), local_bytes, resize, execute=execute) -- cgit From a80b06285d993696ccb90eff00bb2963df49ecc6 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 24 Oct 2010 17:18:24 -0700 Subject: add in the xen rescue template --- nova/virt/libvirt.rescue.xen.xml.template | 34 +++++++++++++++++++++++++++++++ nova/virt/libvirt.xen.xml.template | 6 +----- nova/virt/libvirt_conn.py | 3 +++ 3 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 nova/virt/libvirt.rescue.xen.xml.template (limited to 'nova') diff --git a/nova/virt/libvirt.rescue.xen.xml.template b/nova/virt/libvirt.rescue.xen.xml.template new file mode 100644 index 000000000..3b8d27237 --- /dev/null +++ b/nova/virt/libvirt.rescue.xen.xml.template @@ -0,0 +1,34 @@ + + %(name)s + + linux + %(basepath)s/kernel + %(basepath)s/ramdisk + /dev/xvda1 + ro + + + + + %(memory_kb)s + %(vcpus)s + + + + + + + + + + + + + + + + + + + + diff --git a/nova/virt/libvirt.xen.xml.template b/nova/virt/libvirt.xen.xml.template index 3b8d27237..9677902c6 100644 --- a/nova/virt/libvirt.xen.xml.template +++ b/nova/virt/libvirt.xen.xml.template @@ -13,13 +13,9 @@ %(memory_kb)s %(vcpus)s - - - - - + diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 0096b1400..e32945fa5 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -51,6 +51,9 @@ FLAGS = flags.FLAGS flags.DEFINE_string('libvirt_rescue_xml_template', utils.abspath('virt/libvirt.rescue.qemu.xml.template'), 'Libvirt RESCUE XML Template for QEmu/KVM') +flags.DEFINE_string('libvirt_rescue_xen_xml_template', + utils.abspath('virt/libvirt.rescue.xen.xml.template'), + 'Libvirt RESCUE XML Template for xen') flags.DEFINE_string('libvirt_rescue_uml_xml_template', utils.abspath('virt/libvirt.rescue.uml.xml.template'), 'Libvirt RESCUE XML Template for user-mode-linux') -- cgit From 9ee74816c0c2a28f7d056d524111cd940513766a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 24 Oct 2010 17:22:29 -0700 Subject: add NotFound to fake.py and document it --- nova/virt/fake.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/virt/fake.py b/nova/virt/fake.py index dae2a2410..66eff4c66 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -22,10 +22,9 @@ A fake (in-memory) hypervisor+api. Allows nova testing w/o a hypervisor. This module also documents the semantics of real hypervisor connections. """ -import logging - from twisted.internet import defer +from nova import exception from nova.compute import power_state @@ -160,7 +159,12 @@ class FakeConnection(object): current memory the instance has, in KiB, 'num_cpu': The current number of virtual CPUs the instance has, 'cpu_time': The total CPU time used by the instance, in nanoseconds. + + This method should raise exception.NotFound if the hypervisor has no + knowledge of the instance """ + if instance_name not in self.instances: + raise exception.NotFound("Instance %s Not Found" % instance_name) i = self.instances[instance_name] return {'state': i._state, 'max_mem': 0, -- cgit From 0c7b1ea7972defe67d8bebf4f23d189cc7b0422c Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 24 Oct 2010 19:52:02 -0700 Subject: logging.warn not raise logging.Warn --- nova/compute/manager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'nova') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index fb9a4cb39..574feec7c 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -132,11 +132,11 @@ class ComputeManager(manager.Manager): self._update_state(context, instance_id) if instance_ref['state'] != power_state.RUNNING: - raise logging.Warn('trying to reboot a non-running ' - 'instance: %s (state: %s excepted: %s)', - instance_ref['internal_id'], - instance_ref['state'], - power_state.RUNNING) + logging.warn('trying to reboot a non-running ' + 'instance: %s (state: %s excepted: %s)', + instance_ref['internal_id'], + instance_ref['state'], + power_state.RUNNING) logging.debug('instance %s: rebooting', instance_ref['name']) self.db.instance_set_state(context, -- cgit From 0e98d027d1deb8cd46ddb9a1df4558a5c8b2edfc Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sun, 24 Oct 2010 23:26:03 -0700 Subject: Removed unused imports and left over references to str_id --- nova/db/sqlalchemy/models.py | 25 +------------------------ nova/network/manager.py | 2 +- 2 files changed, 2 insertions(+), 25 deletions(-) (limited to 'nova') diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 38c96bdec..2a3cfa94c 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -20,11 +20,9 @@ SQLAlchemy models for nova data """ -import sys import datetime -# TODO(vish): clean up these imports -from sqlalchemy.orm import relationship, backref, exc, object_mapper +from sqlalchemy.orm import relationship, backref, object_mapper from sqlalchemy import Column, Integer, String, schema from sqlalchemy import ForeignKey, DateTime, Boolean, Text from sqlalchemy.exc import IntegrityError @@ -46,17 +44,11 @@ class NovaBase(object): """Base class for Nova Models""" __table_args__ = {'mysql_engine': 'InnoDB'} __table_initialized__ = False - __prefix__ = 'none' created_at = Column(DateTime, default=datetime.datetime.utcnow) updated_at = Column(DateTime, onupdate=datetime.datetime.utcnow) deleted_at = Column(DateTime) deleted = Column(Boolean, default=False) - @property - def str_id(self): - """Get string id of object (generally prefix + '-' + id)""" - return "%s-%s" % (self.__prefix__, self.id) - def save(self, session=None): """Save this object""" if not session: @@ -94,7 +86,6 @@ class NovaBase(object): #class Image(BASE, NovaBase): # """Represents an image in the datastore""" # __tablename__ = 'images' -# __prefix__ = 'ami' # id = Column(Integer, primary_key=True) # ec2_id = Column(String(12), unique=True) # user_id = Column(String(255)) @@ -150,7 +141,6 @@ class Service(BASE, NovaBase): class Instance(BASE, NovaBase): """Represents a guest vm""" __tablename__ = 'instances' - __prefix__ = 'i' id = Column(Integer, primary_key=True) internal_id = Column(Integer, unique=True) @@ -227,7 +217,6 @@ class Instance(BASE, NovaBase): class Volume(BASE, NovaBase): """Represents a block storage device that can be attached to a vm""" __tablename__ = 'volumes' - __prefix__ = 'vol' id = Column(Integer, primary_key=True) ec2_id = Column(String(12), unique=True) @@ -269,10 +258,6 @@ class Quota(BASE, NovaBase): gigabytes = Column(Integer) floating_ips = Column(Integer) - @property - def str_id(self): - return self.project_id - class ExportDevice(BASE, NovaBase): """Represates a shelf and blade that a volume can be exported on""" @@ -361,10 +346,6 @@ class KeyPair(BASE, NovaBase): fingerprint = Column(String(255)) public_key = Column(Text) - @property - def str_id(self): - return '%s.%s' % (self.user_id, self.name) - class Network(BASE, NovaBase): """Represents a network""" @@ -426,10 +407,6 @@ class FixedIp(BASE, NovaBase): leased = Column(Boolean, default=False) reserved = Column(Boolean, default=False) - @property - def str_id(self): - return self.address - class User(BASE, NovaBase): """Represents a user""" diff --git a/nova/network/manager.py b/nova/network/manager.py index fddb77663..8a20cb491 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -171,7 +171,7 @@ class NetworkManager(manager.Manager): if not fixed_ip_ref['leased']: logging.warn("IP %s released that was not leased", address) self.db.fixed_ip_update(context, - fixed_ip_ref['str_id'], + fixed_ip_ref['address'], {'leased': False}) if not fixed_ip_ref['allocated']: self.db.fixed_ip_disassociate(context, address) -- cgit From 3e2715b21a51c10451e6275e649385f0a846b033 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 25 Oct 2010 00:45:33 -0700 Subject: ISCSI Volume support * Rewrite of Volume code to make VolumeManager more generic * AoE vs. iscsi moved to driver layer * Added db support for target ids * Added code to re-export volumes on restart of VolumeManager * Includes a script to create /dev/iscsi volumes on remote hosts --- nova/compute/manager.py | 25 ++++- nova/db/api.py | 43 +++++++- nova/db/sqlalchemy/api.py | 71 ++++++++++++ nova/db/sqlalchemy/models.py | 23 +++- nova/flags.py | 2 +- nova/tests/fake_flags.py | 8 +- nova/tests/volume_unittest.py | 26 ++--- nova/volume/driver.py | 250 ++++++++++++++++++++++++++++++++++++------ nova/volume/manager.py | 103 +++++++++-------- 9 files changed, 439 insertions(+), 112 deletions(-) (limited to 'nova') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 523bb8893..81b568f80 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -164,10 +164,18 @@ class ComputeManager(manager.Manager): instance_ref = self.db.instance_get(context, instance_id) dev_path = yield self.volume_manager.setup_compute_volume(context, volume_id) - yield self.driver.attach_volume(instance_ref['ec2_id'], - dev_path, - mountpoint) - self.db.volume_attached(context, volume_id, instance_id, mountpoint) + try: + yield self.driver.attach_volume(instance_ref['name'], + dev_path, + mountpoint) + self.db.volume_attached(context, + volume_id, + instance_id, + mountpoint) + except Exception: + yield self.volume_manager.remove_compute_volume(context, + volume_id) + raise defer.returnValue(True) @defer.inlineCallbacks @@ -180,7 +188,12 @@ class ComputeManager(manager.Manager): volume_id) instance_ref = self.db.instance_get(context, instance_id) volume_ref = self.db.volume_get(context, volume_id) - yield self.driver.detach_volume(instance_ref['ec2_id'], - volume_ref['mountpoint']) + if instance_ref['name'] not in self.driver.list_instances(): + logging.warn("Detaching volume from instance %s that isn't running", + instance_ref['name']) + else: + yield self.driver.detach_volume(instance_ref['name'], + volume_ref['mountpoint']) + yield self.volume_manager.remove_compute_volume(context, volume_id) self.db.volume_detached(context, volume_id) defer.returnValue(True) diff --git a/nova/db/api.py b/nova/db/api.py index 0731e2e05..5967b8ded 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -48,6 +48,11 @@ class NoMoreNetworks(exception.Error): pass +class NoMoreTargets(exception.Error): + """No more available blades""" + pass + + ################### @@ -481,6 +486,23 @@ def export_device_create_safe(context, values): ################### +def target_id_count_by_host(context, host): + """Return count of export devices.""" + return IMPL.target_id_count_by_host(context, host) + + +def target_id_create_safe(context, values): + """Create an target_id from the values dictionary. + + The device is not returned. If the create violates the unique + constraints because the target_id and host already exist, + no exception is raised.""" + return IMPL.target_id_create_safe(context, values) + + +############### + + def auth_destroy_token(context, token): """Destroy an auth token""" return IMPL.auth_destroy_token(context, token) @@ -527,6 +549,11 @@ def volume_allocate_shelf_and_blade(context, volume_id): return IMPL.volume_allocate_shelf_and_blade(context, volume_id) +def volume_allocate_target_id(context, volume_id, host): + """Atomically allocate a free target_id from the pool.""" + return IMPL.volume_allocate_target_id(context, volume_id, host) + + def volume_attached(context, volume_id, instance_id, mountpoint): """Ensure that a volume is set as attached.""" return IMPL.volume_attached(context, volume_id, instance_id, mountpoint) @@ -562,9 +589,9 @@ def volume_get_all(context): return IMPL.volume_get_all(context) -def volume_get_instance(context, volume_id): - """Get the instance that a volume is attached to.""" - return IMPL.volume_get_instance(context, volume_id) +def volume_get_all_by_host(context, host): + """Get all volumes belonging to a host.""" + return IMPL.volume_get_all_by_host(context, host) def volume_get_all_by_project(context, project_id): @@ -577,11 +604,21 @@ def volume_get_by_ec2_id(context, ec2_id): return IMPL.volume_get_by_ec2_id(context, ec2_id) +def volume_get_instance(context, volume_id): + """Get the instance that a volume is attached to.""" + return IMPL.volume_get_instance(context, volume_id) + + def volume_get_shelf_and_blade(context, volume_id): """Get the shelf and blade allocated to the volume.""" return IMPL.volume_get_shelf_and_blade(context, volume_id) +def volume_get_target_id(context, volume_id): + """Get the target id allocated to the volume.""" + return IMPL.volume_get_target_id(context, volume_id) + + def volume_update(context, volume_id, values): """Set the given properties on an volume and update it. diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 0cbe56499..538a4d94b 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1041,6 +1041,30 @@ def export_device_create_safe(context, values): ################### +@require_admin_context +def target_id_count_by_host(context, host): + session = get_session() + return session.query(models.TargetId).\ + filter_by(deleted=can_read_deleted(context)).\ + filter_by(host=host).\ + count() + + +@require_admin_context +def target_id_create_safe(context, values): + target_id_ref = models.TargetId() + for (key, value) in values.iteritems(): + target_id_ref[key] = value + try: + target_id_ref.save() + return target_id_ref + except IntegrityError: + return None + + +################### + + def auth_destroy_token(_context, token): session = get_session() session.delete(token) @@ -1130,6 +1154,25 @@ def volume_allocate_shelf_and_blade(context, volume_id): return (export_device.shelf_id, export_device.blade_id) +@require_admin_context +def volume_allocate_target_id(context, volume_id, host): + session = get_session() + with session.begin(): + target_id_ref = session.query(models.TargetId).\ + filter_by(volume=None).\ + filter_by(host=host).\ + filter_by(deleted=False).\ + with_lockmode('update').\ + first() + # NOTE(vish): if with_lockmode isn't supported, as in sqlite, + # then this has concurrency issues + if not target_id_ref: + raise db.NoMoreTargets() + target_id_ref.volume_id = volume_id + session.add(target_id_ref) + return target_id_ref.target_id + + @require_admin_context def volume_attached(context, volume_id, instance_id, mountpoint): session = get_session() @@ -1181,6 +1224,9 @@ def volume_destroy(context, volume_id): session.execute('update export_devices set volume_id=NULL ' 'where volume_id=:id', {'id': volume_id}) + session.execute('update target_ids set volume_id=NULL ' + 'where volume_id=:id', + {'id': volume_id}) @require_admin_context @@ -1222,6 +1268,17 @@ def volume_get(context, volume_id, session=None): def volume_get_all(context): session = get_session() return session.query(models.Volume).\ + options(joinedload('instance')).\ + filter_by(deleted=can_read_deleted(context)).\ + all() + + +@require_admin_context +def volume_get_all_by_host(context, host): + session = get_session() + return session.query(models.Volume).\ + options(joinedload('instance')).\ + filter_by(host=host).\ filter_by(deleted=can_read_deleted(context)).\ all() @@ -1232,6 +1289,7 @@ def volume_get_all_by_project(context, project_id): session = get_session() return session.query(models.Volume).\ + options(joinedload('instance')).\ filter_by(project_id=project_id).\ filter_by(deleted=can_read_deleted(context)).\ all() @@ -1299,6 +1357,19 @@ def volume_get_shelf_and_blade(context, volume_id): return (result.shelf_id, result.blade_id) +@require_admin_context +def volume_get_target_id(context, volume_id): + session = get_session() + result = session.query(models.TargetId).\ + filter_by(volume_id=volume_id).\ + first() + if not result: + raise exception.NotFound('No target id found for volume %s' % + volume_id) + + return result.target_id + + @require_context def volume_update(context, volume_id, values): session = get_session() diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 38c96bdec..18d837e6b 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -255,6 +255,11 @@ class Volume(BASE, NovaBase): display_name = Column(String(255)) display_description = Column(String(255)) + @property + def name(self): + return self.ec2_id + + class Quota(BASE, NovaBase): """Represents quota overrides for a project""" @@ -290,6 +295,22 @@ class ExportDevice(BASE, NovaBase): 'ExportDevice.deleted==False)') +class TargetId(BASE, NovaBase): + """Represates an iscsi target_id for a given host""" + __tablename__ = 'target_ids' + __table_args__ = (schema.UniqueConstraint("target_id", "host"), + {'mysql_engine': 'InnoDB'}) + id = Column(Integer, primary_key=True) + target_id = Column(Integer) + host = Column(String(255)) + volume_id = Column(Integer, ForeignKey('volumes.id'), nullable=True) + volume = relationship(Volume, + backref=backref('target_id', uselist=False), + foreign_keys=volume_id, + primaryjoin='and_(TargetId.volume_id==Volume.id,' + 'TargetId.deleted==False)') + + class SecurityGroupInstanceAssociation(BASE, NovaBase): __tablename__ = 'security_group_instance_association' id = Column(Integer, primary_key=True) @@ -510,7 +531,7 @@ class FloatingIp(BASE, NovaBase): def register_models(): """Register Models and create metadata""" from sqlalchemy import create_engine - models = (Service, Instance, Volume, ExportDevice, FixedIp, + models = (Service, Instance, Volume, ExportDevice, TargetId, FixedIp, FloatingIp, Network, SecurityGroup, SecurityGroupIngressRule, SecurityGroupInstanceAssociation, AuthToken, User, Project) # , Image, Host diff --git a/nova/flags.py b/nova/flags.py index f3b0384ad..380382a7a 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -224,7 +224,7 @@ DEFINE_string('compute_manager', 'nova.compute.manager.ComputeManager', 'Manager for compute') DEFINE_string('network_manager', 'nova.network.manager.VlanManager', 'Manager for network') -DEFINE_string('volume_manager', 'nova.volume.manager.AOEManager', +DEFINE_string('volume_manager', 'nova.volume.manager.VolumeManager', 'Manager for volume') DEFINE_string('scheduler_manager', 'nova.scheduler.manager.SchedulerManager', 'Manager for scheduler') diff --git a/nova/tests/fake_flags.py b/nova/tests/fake_flags.py index 4bbef8832..d695d68a6 100644 --- a/nova/tests/fake_flags.py +++ b/nova/tests/fake_flags.py @@ -21,7 +21,7 @@ from nova import flags FLAGS = flags.FLAGS flags.DECLARE('volume_driver', 'nova.volume.manager') -FLAGS.volume_driver = 'nova.volume.driver.FakeAOEDriver' +FLAGS.volume_driver = 'nova.volume.driver.FakeISCSIDriver' FLAGS.connection_type = 'fake' FLAGS.fake_rabbit = True FLAGS.auth_driver = 'nova.auth.dbdriver.DbDriver' @@ -31,9 +31,11 @@ flags.DECLARE('fake_network', 'nova.network.manager') FLAGS.network_size = 16 FLAGS.num_networks = 5 FLAGS.fake_network = True -flags.DECLARE('num_shelves', 'nova.volume.manager') -flags.DECLARE('blades_per_shelf', 'nova.volume.manager') +flags.DECLARE('num_shelves', 'nova.volume.driver') +flags.DECLARE('blades_per_shelf', 'nova.volume.driver') +flags.DECLARE('iscsi_target_ids', 'nova.volume.driver') FLAGS.num_shelves = 2 FLAGS.blades_per_shelf = 4 +FLAGS.iscsi_target_ids = 8 FLAGS.verbose = True FLAGS.sql_connection = 'sqlite:///nova.sqlite' diff --git a/nova/tests/volume_unittest.py b/nova/tests/volume_unittest.py index fdee30b48..34e04c8b8 100644 --- a/nova/tests/volume_unittest.py +++ b/nova/tests/volume_unittest.py @@ -83,9 +83,9 @@ class VolumeTestCase(test.TrialTestCase): @defer.inlineCallbacks def test_too_many_volumes(self): - """Ensure that NoMoreBlades is raised when we run out of volumes""" + """Ensure that NoMoreTargets is raised when we run out of volumes""" vols = [] - total_slots = FLAGS.num_shelves * FLAGS.blades_per_shelf + total_slots = FLAGS.iscsi_target_ids for _index in xrange(total_slots): volume_id = self._create_volume() yield self.volume.create_volume(self.context, volume_id) @@ -93,7 +93,7 @@ class VolumeTestCase(test.TrialTestCase): volume_id = self._create_volume() self.assertFailure(self.volume.create_volume(self.context, volume_id), - db.NoMoreBlades) + db.NoMoreTargets) db.volume_destroy(context.get_admin_context(), volume_id) for volume_id in vols: yield self.volume.delete_volume(self.context, volume_id) @@ -148,23 +148,21 @@ class VolumeTestCase(test.TrialTestCase): db.instance_destroy(self.context, instance_id) @defer.inlineCallbacks - def test_concurrent_volumes_get_different_blades(self): - """Ensure multiple concurrent volumes get different blades""" + def test_concurrent_volumes_get_different_targets(self): + """Ensure multiple concurrent volumes get different targets""" volume_ids = [] - shelf_blades = [] + targets = [] def _check(volume_id): - """Make sure blades aren't duplicated""" + """Make sure targets aren't duplicated""" volume_ids.append(volume_id) admin_context = context.get_admin_context() - (shelf_id, blade_id) = db.volume_get_shelf_and_blade(admin_context, - volume_id) - shelf_blade = '%s.%s' % (shelf_id, blade_id) - self.assert_(shelf_blade not in shelf_blades) - shelf_blades.append(shelf_blade) - logging.debug("Blade %s allocated", shelf_blade) + target_id = db.volume_get_target_id(admin_context, volume_id) + self.assert_(target_id not in targets) + targets.append(target_id) + logging.debug("Target %s allocated", target_id) deferreds = [] - total_slots = FLAGS.num_shelves * FLAGS.blades_per_shelf + total_slots = FLAGS.iscsi_target_ids for _index in xrange(total_slots): volume_id = self._create_volume() d = self.volume.create_volume(self.context, volume_id) diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 3fa29ba37..b69076244 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -27,6 +27,7 @@ from twisted.internet import defer from nova import exception from nova import flags from nova import process +from nova import utils FLAGS = flags.FLAGS @@ -36,12 +37,29 @@ flags.DEFINE_string('aoe_eth_dev', 'eth0', 'Which device to export the volumes on') flags.DEFINE_string('num_shell_tries', 3, 'number of times to attempt to run flakey shell commands') +flags.DEFINE_integer('num_shelves', + 100, + 'Number of vblade shelves') +flags.DEFINE_integer('blades_per_shelf', + 16, + 'Number of vblade blades per shelf') +flags.DEFINE_integer('iscsi_target_ids', + 100, + 'Number of iscsi target ids per host') +flags.DEFINE_string('iscsi_target_prefix', 'iqn.2010-10.org.openstack:', + 'prefix for iscsi volumes') +flags.DEFINE_string('iscsi_ip_prefix', '127.0.0', + 'only connect to the specified ip') -class AOEDriver(object): - """Executes commands relating to AOE volumes""" - def __init__(self, execute=process.simple_execute, *args, **kwargs): +class VolumeDriver(object): + """Executes commands relating to Volumes""" + def __init__(self, execute=process.simple_execute, + sync_exec=utils.execute, *args, **kwargs): + # NOTE(vish): db is set by Manager + self.db = None self._execute = execute + self._sync_exec = sync_exec @defer.inlineCallbacks def _try_execute(self, command): @@ -61,55 +79,93 @@ class AOEDriver(object): "Try number %s", tries) yield self._execute("sleep %s" % tries ** 2) + def check_for_setup_error(self): + """Returns an error if prerequesits aren't met""" + # NOTE(vish): makes sure that the volume group exists + (_out, err) = self._sync_exec("vgs %s" % FLAGS.volume_group, + check_exit_code=False) + if err: + raise exception.Error(err) + @defer.inlineCallbacks - def create_volume(self, volume_name, size): + def create_volume(self, volume): """Creates a logical volume""" - # NOTE(vish): makes sure that the volume group exists - yield self._execute("vgs %s" % FLAGS.volume_group) - if int(size) == 0: + if int(volume['size']) == 0: sizestr = '100M' else: - sizestr = '%sG' % size + sizestr = '%sG' % volume['size'] yield self._try_execute("sudo lvcreate -L %s -n %s %s" % (sizestr, - volume_name, + volume['name'], FLAGS.volume_group)) @defer.inlineCallbacks - def delete_volume(self, volume_name): + def delete_volume(self, volume): """Deletes a logical volume""" yield self._try_execute("sudo lvremove -f %s/%s" % (FLAGS.volume_group, - volume_name)) + volume['name'])) @defer.inlineCallbacks - def create_export(self, volume_name, shelf_id, blade_id): - """Creates an export for a logical volume""" - yield self._try_execute( - "sudo vblade-persist setup %s %s %s /dev/%s/%s" % - (shelf_id, - blade_id, - FLAGS.aoe_eth_dev, - FLAGS.volume_group, - volume_name)) + def local_path(self, volume): + defer.returnValue("/dev/%s/%s" % (FLAGS.volume_group, volume['name'])) + + def ensure_export(self, context, volume): + """Safely and synchronously recreates an export for a logical volume""" + raise NotImplementedError() @defer.inlineCallbacks - def discover_volume(self, _volume_name): - """Discover volume on a remote host""" - yield self._execute("sudo aoe-discover") - yield self._execute("sudo aoe-stat") + def create_export(self, context, volume): + """Exports the volume""" + raise NotImplementedError() @defer.inlineCallbacks - def remove_export(self, _volume_name, shelf_id, blade_id): + def remove_export(self, context, volume): """Removes an export for a logical volume""" - yield self._try_execute("sudo vblade-persist stop %s %s" % - (shelf_id, blade_id)) - yield self._try_execute("sudo vblade-persist destroy %s %s" % - (shelf_id, blade_id)) + raise NotImplementedError() + + @defer.inlineCallbacks + def discover_volume(self, volume): + """Discover volume on a remote host""" + raise NotImplementedError() @defer.inlineCallbacks - def ensure_exports(self): - """Runs all existing exports""" + def undiscover_volume(self, volume): + """Undiscover volume on a remote host""" + raise NotImplementedError() + + +class AOEDriver(VolumeDriver): + """Implements AOE specific volume commands""" + + def ensure_export(self, context, volume): + # NOTE(vish): we depend on vblade-persist for recreating exports + pass + + def _ensure_blades(self, context): + """Ensure that blades have been created in datastore""" + total_blades = FLAGS.num_shelves * FLAGS.blades_per_shelf + if self.db.export_device_count(context) >= total_blades: + return + for shelf_id in xrange(FLAGS.num_shelves): + for blade_id in xrange(FLAGS.blades_per_shelf): + dev = {'shelf_id': shelf_id, 'blade_id': blade_id} + self.db.export_device_create_safe(context, dev) + + @defer.inlineCallbacks + def create_export(self, context, volume): + """Creates an export for a logical volume""" + self._ensure_blades(context) + (shelf_id, + blade_id) = self.db.volume_allocate_shelf_and_blade(context, + volume['id']) + yield self._try_execute( + "sudo vblade-persist setup %s %s %s /dev/%s/%s" % + (shelf_id, + blade_id, + FLAGS.aoe_eth_dev, + FLAGS.volume_group, + volume['name'])) # NOTE(vish): The standard _try_execute does not work here # because these methods throw errors if other # volumes on this host are in the process of @@ -123,13 +179,143 @@ class AOEDriver(object): yield self._execute("sudo vblade-persist start all", check_exit_code=False) + @defer.inlineCallbacks + def remove_export(self, context, volume): + """Removes an export for a logical volume""" + (shelf_id, + blade_id) = self.db.volume_get_shelf_and_blade(context, + volume['id']) + yield self._try_execute("sudo vblade-persist stop %s %s" % + (shelf_id, blade_id)) + yield self._try_execute("sudo vblade-persist destroy %s %s" % + (shelf_id, blade_id)) + + @defer.inlineCallbacks + def discover_volume(self, _volume): + """Discover volume on a remote host""" + yield self._execute("sudo aoe-discover") + yield self._execute("sudo aoe-stat", check_exit_code=False) + + @defer.inlineCallbacks + def undiscover_volume(self, _volume): + """Undiscover volume on a remote host""" + yield + class FakeAOEDriver(AOEDriver): """Logs calls instead of executing""" def __init__(self, *args, **kwargs): - super(FakeAOEDriver, self).__init__(self.fake_execute) + super(FakeAOEDriver, self).__init__(execute=self.fake_execute, + sync_exec=self.fake_execute, + *args, **kwargs) @staticmethod def fake_execute(cmd, *_args, **_kwargs): """Execute that simply logs the command""" logging.debug("FAKE AOE: %s", cmd) + return (None, None) + + +class ISCSIDriver(VolumeDriver): + """Executes commands relating to ISCSI volumes""" + + def ensure_export(self, context, volume): + """Safely and synchronously recreates an export for a logical volume""" + target_id = self.db.volume_get_target_id(context, volume['id']) + iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name']) + volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name']) + self._sync_exec("sudo ietadm --op new " + "--tid=%s --params Name=%s" % + (target_id, iscsi_name), + check_exit_code=False) + self._sync_exec("sudo ietadm --op new --tid=%s " + "--lun=0 --params Path=%s,Type=fileio" % + (target_id, volume_path), + check_exit_code=False) + + def _ensure_target_ids(self, context, host): + """Ensure that target ids have been created in datastore""" + host_target_ids = self.db.target_id_count_by_host(context, host) + if host_target_ids >= FLAGS.iscsi_target_ids: + return + # NOTE(vish): Target ids start at 1, not 0. + for target_id in xrange(1, FLAGS.iscsi_target_ids + 1): + target = {'host': host, 'target_id': target_id} + self.db.target_id_create_safe(context, target) + + @defer.inlineCallbacks + def create_export(self, context, volume): + """Creates an export for a logical volume""" + self._ensure_target_ids(context, volume['host']) + target_id = self.db.volume_allocate_target_id(context, + volume['id'], + volume['host']) + iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name']) + volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name']) + yield self._execute("sudo ietadm --op new " + "--tid=%s --params Name=%s" % + (target_id, iscsi_name)) + yield self._execute("sudo ietadm --op new --tid=%s " + "--lun=0 --params Path=%s,Type=fileio" % + (target_id, volume_path)) + + @defer.inlineCallbacks + def remove_export(self, context, volume): + """Removes an export for a logical volume""" + target_id = self.db.volume_get_target_id(context, volume['name']) + yield self._execute("sudo ietadm --op delete --tid=%s " + "--lun=0" % target_id) + yield self._execute("sudo ietadm --op delete --tid=%s" % + target_id) + + @defer.inlineCallbacks + def _get_name_and_portal(self, volume_name, host): + (out, _err) = yield self._execute("sudo iscsiadm -m discovery -t " + "sendtargets -p %s" % host) + for target in out.splitlines(): + if FLAGS.iscsi_ip_prefix in target and volume_name in target: + (location, _sep, iscsi_name) = target.partition(" ") + break + iscsi_portal = location.split(",")[0] + defer.returnValue((iscsi_name, iscsi_portal)) + + @defer.inlineCallbacks + def discover_volume(self, volume): + """Discover volume on a remote host""" + (iscsi_name, + iscsi_portal) = yield self._get_name_and_portal(volume['id'], + volume['host']) + yield self._execute("sudo iscsiadm -m node -T %s -p %s --login" % + (iscsi_name, iscsi_portal)) + yield self._execute("sudo iscsiadm -m node -T %s -p %s --op update " + "-n node.startup -v automatic" % + (iscsi_name, iscsi_portal)) + defer.returnValue("/dev/iscsi/%s" % volume['name']) + + @defer.inlineCallbacks + def undiscover_volume(self, volume): + """Undiscover volume on a remote host""" + (iscsi_name, + iscsi_portal) = yield self._get_name_and_portal(volume['name'], + volume['host']) + yield self._execute("sudo iscsiadm -m node -T %s -p %s --op update " + "-n node.startup -v manual" % + (iscsi_name, iscsi_portal)) + yield self._execute("sudo iscsiadm -m node -T %s -p %s --logout " % + (iscsi_name, iscsi_portal)) + yield self._execute("sudo iscsiadm -m node --op delete " + "--targetname %s" % iscsi_name) + + +class FakeISCSIDriver(ISCSIDriver): + """Logs calls instead of executing""" + def __init__(self, *args, **kwargs): + super(FakeISCSIDriver, self).__init__(execute=self.fake_execute, + sync_exec=self.fake_execute, + *args, **kwargs) + + @staticmethod + def fake_execute(cmd, *_args, **_kwargs): + """Execute that simply logs the command""" + logging.debug("FAKE ISCSI: %s", cmd) + return (None, None) diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 2874459f9..910e71c9e 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -26,6 +26,7 @@ import datetime from twisted.internet import defer +from nova import context from nova import exception from nova import flags from nova import manager @@ -36,70 +37,58 @@ FLAGS = flags.FLAGS flags.DEFINE_string('storage_availability_zone', 'nova', 'availability zone of this service') -flags.DEFINE_string('volume_driver', 'nova.volume.driver.AOEDriver', +flags.DEFINE_string('volume_driver', 'nova.volume.driver.ISCSIDriver', 'Driver to use for volume creation') -flags.DEFINE_integer('num_shelves', - 100, - 'Number of vblade shelves') -flags.DEFINE_integer('blades_per_shelf', - 16, - 'Number of vblade blades per shelf') -class AOEManager(manager.Manager): - """Manages Ata-Over_Ethernet volumes""" +class VolumeManager(manager.Manager): + """Manages attachable block storage devices""" def __init__(self, volume_driver=None, *args, **kwargs): if not volume_driver: volume_driver = FLAGS.volume_driver self.driver = utils.import_object(volume_driver) - super(AOEManager, self).__init__(*args, **kwargs) - - def _ensure_blades(self, context): - """Ensure that blades have been created in datastore""" - total_blades = FLAGS.num_shelves * FLAGS.blades_per_shelf - if self.db.export_device_count(context) >= total_blades: - return - for shelf_id in xrange(FLAGS.num_shelves): - for blade_id in xrange(FLAGS.blades_per_shelf): - dev = {'shelf_id': shelf_id, 'blade_id': blade_id} - self.db.export_device_create_safe(context, dev) + super(VolumeManager, self).__init__(*args, **kwargs) + # NOTE(vish): Implementation specific db handling is done + # by the driver. + self.driver.db = self.db + + def init_host(self): + """Do any initialization that needs to be run if this is a + standalone service. + """ + self.driver.check_for_setup_error() + ctxt = context.get_admin_context() + volumes = self.db.volume_get_all_by_host(ctxt, self.host) + logging.debug("Re-exporting %s volumes", len(volumes)) + for volume in volumes: + self.driver.ensure_export(context, volume) @defer.inlineCallbacks def create_volume(self, context, volume_id): """Creates and exports the volume""" context = context.elevated() - logging.info("volume %s: creating", volume_id) - volume_ref = self.db.volume_get(context, volume_id) + logging.info("volume %s: creating", volume_ref['name']) self.db.volume_update(context, volume_id, {'host': self.host}) + # NOTE(vish): so we don't have to get volume from db again + # before passing it to the driver. + volume_ref['host'] = self.host - size = volume_ref['size'] - logging.debug("volume %s: creating lv of size %sG", volume_id, size) - yield self.driver.create_volume(volume_ref['ec2_id'], size) - - logging.debug("volume %s: allocating shelf & blade", volume_id) - self._ensure_blades(context) - rval = self.db.volume_allocate_shelf_and_blade(context, volume_id) - (shelf_id, blade_id) = rval - - logging.debug("volume %s: exporting shelf %s & blade %s", volume_id, - shelf_id, blade_id) - - yield self.driver.create_export(volume_ref['ec2_id'], - shelf_id, - blade_id) + logging.debug("volume %s: creating lv of size %sG", + volume_ref['name'], volume_ref['size']) + yield self.driver.create_volume(volume_ref) - logging.debug("volume %s: re-exporting all values", volume_id) - yield self.driver.ensure_exports() + logging.debug("volume %s: creating export", volume_ref['name']) + yield self.driver.create_export(context, volume_ref) now = datetime.datetime.utcnow() self.db.volume_update(context, volume_ref['id'], {'status': 'available', 'launched_at': now}) - logging.debug("volume %s: created successfully", volume_id) + logging.debug("volume %s: created successfully", volume_ref['name']) defer.returnValue(volume_id) @defer.inlineCallbacks @@ -111,14 +100,10 @@ class AOEManager(manager.Manager): raise exception.Error("Volume is still attached") if volume_ref['host'] != self.host: raise exception.Error("Volume is not local to this node") - logging.debug("Deleting volume with id of: %s", volume_id) - shelf_id, blade_id = self.db.volume_get_shelf_and_blade(context, - volume_id) - yield self.driver.remove_export(volume_ref['ec2_id'], - shelf_id, - blade_id) - yield self.driver.delete_volume(volume_ref['ec2_id']) + logging.debug("volume %s: deleting", volume_ref['name']) + yield self.driver.delete_volume(volume_ref) self.db.volume_destroy(context, volume_id) + logging.debug("volume %s: deleted successfully", volume_ref['name']) defer.returnValue(True) @defer.inlineCallbacks @@ -127,9 +112,23 @@ class AOEManager(manager.Manager): Returns path to device. """ - context = context.elevated() + context = context.admin() volume_ref = self.db.volume_get(context, volume_id) - yield self.driver.discover_volume(volume_ref['ec2_id']) - shelf_id, blade_id = self.db.volume_get_shelf_and_blade(context, - volume_id) - defer.returnValue("/dev/etherd/e%s.%s" % (shelf_id, blade_id)) + if volume_ref['host'] == self.host: + # NOTE(vish): No need to discover local volumes. + path = yield self.driver.local_path(volume_ref) + else: + path = yield self.driver.discover_volume(volume_ref) + defer.returnValue(path) + + @defer.inlineCallbacks + def remove_compute_volume(self, context, volume_id): + """Remove remote volume on compute host """ + context = context.admin() + volume_ref = self.db.volume_get(context, volume_id) + if volume_ref['host'] == self.host: + # NOTE(vish): No need to undiscover local volumes. + defer.returnValue(True) + else: + yield self.driver.undiscover_volume(volume_ref) + -- cgit From bde0d8d0f0e864d5b5d0f87e55ab23839846f71e Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 25 Oct 2010 01:37:01 -0700 Subject: fix bugs, describe volumes, detach on terminate --- nova/api/ec2/cloud.py | 13 ++++++++++--- nova/compute/manager.py | 3 +++ nova/volume/driver.py | 11 +++++------ nova/volume/manager.py | 4 ++-- 4 files changed, 20 insertions(+), 11 deletions(-) (limited to 'nova') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 784697b01..a1899c47f 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -463,24 +463,31 @@ class CloudController(object): return {'volumeSet': volumes} def _format_volume(self, context, volume): + instance_ec2_id = None + instance_data = None + if volume.get('instance', None): + internal_id = volume['instance']['internal_id'] + ec2_id = internal_id_to_ec2_id(internal_id) + instance_data = '%s[%s]' % (instance_ec2_id, + volume['instance']['host']) v = {} v['volumeId'] = volume['ec2_id'] v['status'] = volume['status'] v['size'] = volume['size'] v['availabilityZone'] = volume['availability_zone'] v['createTime'] = volume['created_at'] - if context.user.is_admin(): + if context.is_admin: v['status'] = '%s (%s, %s, %s, %s)' % ( volume['status'], volume['user_id'], volume['host'], - volume['instance_id'], + instance_data, volume['mountpoint']) if volume['attach_status'] == 'attached': v['attachmentSet'] = [{'attachTime': volume['attach_time'], 'deleteOnTermination': False, 'device': volume['mountpoint'], - 'instanceId': volume['instance_id'], + 'instanceId': instance_ec2_id, 'status': 'attached', 'volume_id': volume['ec2_id']}] else: diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 81b568f80..3b3208fea 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -111,6 +111,9 @@ class ComputeManager(manager.Manager): logging.debug("instance %s: terminating", instance_id) instance_ref = self.db.instance_get(context, instance_id) + volumes = instance_ref.get('volumes', []) or [] + for volume in volumes: + self.detach_volume(instance_id, volume['id']) if instance_ref['state'] == power_state.SHUTOFF: self.db.instance_destroy(context, instance_id) raise exception.Error('trying to destroy already destroyed' diff --git a/nova/volume/driver.py b/nova/volume/driver.py index b69076244..eff56d9c6 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -21,6 +21,7 @@ Drivers for volumes """ import logging +import os from twisted.internet import defer @@ -80,12 +81,10 @@ class VolumeDriver(object): yield self._execute("sleep %s" % tries ** 2) def check_for_setup_error(self): - """Returns an error if prerequesits aren't met""" - # NOTE(vish): makes sure that the volume group exists - (_out, err) = self._sync_exec("vgs %s" % FLAGS.volume_group, - check_exit_code=False) - if err: - raise exception.Error(err) + """Returns an error if prerequisites aren't met""" + if not os.path.isdir("/dev/%s" % FLAGS.volume_group): + raise exception.Error("volume group %s doesn't exist" + % FLAGS.volume_group) @defer.inlineCallbacks def create_volume(self, volume): diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 910e71c9e..f6146efe9 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -112,7 +112,7 @@ class VolumeManager(manager.Manager): Returns path to device. """ - context = context.admin() + context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) if volume_ref['host'] == self.host: # NOTE(vish): No need to discover local volumes. @@ -124,7 +124,7 @@ class VolumeManager(manager.Manager): @defer.inlineCallbacks def remove_compute_volume(self, context, volume_id): """Remove remote volume on compute host """ - context = context.admin() + context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) if volume_ref['host'] == self.host: # NOTE(vish): No need to undiscover local volumes. -- cgit From 43a545a8bd8f763eba7741a240c29da447aef61e Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 25 Oct 2010 03:11:00 -0700 Subject: more bugfixes, flag for local volumes --- nova/api/ec2/__init__.py | 1 + nova/api/ec2/cloud.py | 7 +++++-- nova/compute/manager.py | 4 +++- nova/db/sqlalchemy/models.py | 6 ++++++ nova/volume/driver.py | 9 +++++---- nova/volume/manager.py | 12 +++++++----- 6 files changed, 27 insertions(+), 12 deletions(-) (limited to 'nova') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 0df4d3710..c53ce6f5e 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -238,6 +238,7 @@ class Executor(wsgi.Application): return self._error(req, type(ex).__name__, str(ex)) def _error(self, req, code, message): + logging.error("%s: %s", code, message) resp = webob.Response() resp.status = 400 resp.headers['Content-Type'] = 'text/xml' diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index a1899c47f..7a057396c 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -467,7 +467,7 @@ class CloudController(object): instance_data = None if volume.get('instance', None): internal_id = volume['instance']['internal_id'] - ec2_id = internal_id_to_ec2_id(internal_id) + instance_ec2_id = internal_id_to_ec2_id(internal_id) instance_data = '%s[%s]' % (instance_ec2_id, volume['instance']['host']) v = {} @@ -522,7 +522,10 @@ class CloudController(object): "args": {"topic": FLAGS.volume_topic, "volume_id": volume_ref['id']}}) - return {'volumeSet': [self._format_volume(context, volume_ref)]} + # TODO(vish): Instance should be None at db layer instead of + # trying to lazy load, but for now we turn it into + # a dict to avoid an error. + return {'volumeSet': [self._format_volume(context, dict(volume_ref))]} def attach_volume(self, context, volume_id, instance_id, device, **kwargs): volume_ref = db.volume_get_by_ec2_id(context, volume_id) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 3b3208fea..116bf11cc 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -113,7 +113,7 @@ class ComputeManager(manager.Manager): instance_ref = self.db.instance_get(context, instance_id) volumes = instance_ref.get('volumes', []) or [] for volume in volumes: - self.detach_volume(instance_id, volume['id']) + self.detach_volume(context, instance_id, volume['id']) if instance_ref['state'] == power_state.SHUTOFF: self.db.instance_destroy(context, instance_id) raise exception.Error('trying to destroy already destroyed' @@ -176,6 +176,8 @@ class ComputeManager(manager.Manager): instance_id, mountpoint) except Exception: + logging.debug("instance %s: attach failed to %s, removing export", + instance_id, mountpoint) yield self.volume_manager.remove_compute_volume(context, volume_id) raise diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 18d837e6b..b0adc3a2a 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -82,6 +82,12 @@ class NovaBase(object): def __getitem__(self, key): return getattr(self, key) + def get(self, key, default=None): + try: + return getattr(self, key) + except AttributeError, KeyError: + return default + def __iter__(self): self._i = iter(object_mapper(self).columns) return self diff --git a/nova/volume/driver.py b/nova/volume/driver.py index eff56d9c6..bffe4d6b5 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -49,8 +49,8 @@ flags.DEFINE_integer('iscsi_target_ids', 'Number of iscsi target ids per host') flags.DEFINE_string('iscsi_target_prefix', 'iqn.2010-10.org.openstack:', 'prefix for iscsi volumes') -flags.DEFINE_string('iscsi_ip_prefix', '127.0.0', - 'only connect to the specified ip') +flags.DEFINE_string('iscsi_ip_prefix', '127.0', + 'discover volumes on the ip that starts with this prefix') class VolumeDriver(object): @@ -107,6 +107,7 @@ class VolumeDriver(object): @defer.inlineCallbacks def local_path(self, volume): + yield # NOTE(vish): stops deprecation warning defer.returnValue("/dev/%s/%s" % (FLAGS.volume_group, volume['name'])) def ensure_export(self, context, volume): @@ -261,7 +262,7 @@ class ISCSIDriver(VolumeDriver): @defer.inlineCallbacks def remove_export(self, context, volume): """Removes an export for a logical volume""" - target_id = self.db.volume_get_target_id(context, volume['name']) + target_id = self.db.volume_get_target_id(context, volume['id']) yield self._execute("sudo ietadm --op delete --tid=%s " "--lun=0" % target_id) yield self._execute("sudo ietadm --op delete --tid=%s" % @@ -282,7 +283,7 @@ class ISCSIDriver(VolumeDriver): def discover_volume(self, volume): """Discover volume on a remote host""" (iscsi_name, - iscsi_portal) = yield self._get_name_and_portal(volume['id'], + iscsi_portal) = yield self._get_name_and_portal(volume['name'], volume['host']) yield self._execute("sudo iscsiadm -m node -T %s -p %s --login" % (iscsi_name, iscsi_portal)) diff --git a/nova/volume/manager.py b/nova/volume/manager.py index f6146efe9..bc49e28ee 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -39,6 +39,8 @@ flags.DEFINE_string('storage_availability_zone', 'availability zone of this service') flags.DEFINE_string('volume_driver', 'nova.volume.driver.ISCSIDriver', 'Driver to use for volume creation') +flags.DEFINE_boolean('use_local_volumes', True, + 'if True, will not discover local volumes') class VolumeManager(manager.Manager): @@ -61,7 +63,7 @@ class VolumeManager(manager.Manager): volumes = self.db.volume_get_all_by_host(ctxt, self.host) logging.debug("Re-exporting %s volumes", len(volumes)) for volume in volumes: - self.driver.ensure_export(context, volume) + self.driver.ensure_export(ctxt, volume) @defer.inlineCallbacks def create_volume(self, context, volume_id): @@ -100,6 +102,8 @@ class VolumeManager(manager.Manager): raise exception.Error("Volume is still attached") if volume_ref['host'] != self.host: raise exception.Error("Volume is not local to this node") + logging.debug("volume %s: removing export", volume_ref['name']) + yield self.driver.remove_export(context, volume_ref) logging.debug("volume %s: deleting", volume_ref['name']) yield self.driver.delete_volume(volume_ref) self.db.volume_destroy(context, volume_id) @@ -114,8 +118,7 @@ class VolumeManager(manager.Manager): """ context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) - if volume_ref['host'] == self.host: - # NOTE(vish): No need to discover local volumes. + if volume_ref['host'] == self.host and FLAGS.use_local_volumes: path = yield self.driver.local_path(volume_ref) else: path = yield self.driver.discover_volume(volume_ref) @@ -126,8 +129,7 @@ class VolumeManager(manager.Manager): """Remove remote volume on compute host """ context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) - if volume_ref['host'] == self.host: - # NOTE(vish): No need to undiscover local volumes. + if volume_ref['host'] == self.host and FLAGS.use_local_volumes: defer.returnValue(True) else: yield self.driver.undiscover_volume(volume_ref) -- cgit From 3508fe6e6fe56d86119158d1631d624c76087bf6 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 25 Oct 2010 11:12:56 -0700 Subject: use libvirt connection for attaching disks and avoid the symlink --- nova/virt/libvirt_conn.py | 45 ++++++++++++++++++++++++++++++++++++--------- nova/volume/driver.py | 5 ++++- 2 files changed, 40 insertions(+), 10 deletions(-) (limited to 'nova') diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 509ed97a0..85edfff08 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -182,19 +182,46 @@ class LibvirtConnection(object): @defer.inlineCallbacks @exception.wrap_exception def attach_volume(self, instance_name, device_path, mountpoint): - yield process.simple_execute("sudo virsh attach-disk %s %s %s" % - (instance_name, - device_path, - mountpoint.rpartition('/dev/')[2])) + virt_dom = self._conn.lookupByName(instance_name) + mount_device = mountpoint.rpartition("/")[2] + xml = """ + + + + """ % (device_path, mount_device) + virt_dom.attachDevice(xml) + yield + + def _get_disk_xml(self, xml, device): + """Returns the xml for the disk mounted at device""" + try: + doc = libxml2.parseDoc(xml) + except: + return None + ctx = doc.xpathNewContext() + try: + ret = ctx.xpathEval('/domain/devices/disk') + for node in ret: + for child in node.children: + if child.name == 'target': + if child.prop('dev') == device: + return str(node) + finally: + if ctx != None: + ctx.xpathFreeContext() + if doc != None: + doc.freeDoc() @defer.inlineCallbacks @exception.wrap_exception def detach_volume(self, instance_name, mountpoint): - # NOTE(vish): despite the documentation, virsh detach-disk just - # wants the device name without the leading /dev/ - yield process.simple_execute("sudo virsh detach-disk %s %s" % - (instance_name, - mountpoint.rpartition('/dev/')[2])) + virt_dom = self._conn.lookupByName(instance_name) + mount_device = mountpoint.rpartition("/")[2] + xml = self._get_disk_xml(virt_dom.XMLDesc(0), mount_device) + if not xml: + raise exception.NotFound("No disk at %s" % mount_device) + virt_dom.detachDevice(xml) + yield @defer.inlineCallbacks @exception.wrap_exception diff --git a/nova/volume/driver.py b/nova/volume/driver.py index bffe4d6b5..8d98d2491 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -108,7 +108,10 @@ class VolumeDriver(object): @defer.inlineCallbacks def local_path(self, volume): yield # NOTE(vish): stops deprecation warning - defer.returnValue("/dev/%s/%s" % (FLAGS.volume_group, volume['name'])) + escaped_group = FLAGS.volume_group.replace('-', '--') + escaped_name = volume['name'].replace('-', '--') + defer.returnValue("/dev/mapper/%s-%s" % (escaped_group, + escaped_name)) def ensure_export(self, context, volume): """Safely and synchronously recreates an export for a logical volume""" -- cgit From 2738a380816b73f35e73f111bd9b4f3ef3101012 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 25 Oct 2010 12:17:12 -0700 Subject: print the exception on fail, because it doesn't seem to reraise it --- nova/compute/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 116bf11cc..80931a309 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -176,8 +176,8 @@ class ComputeManager(manager.Manager): instance_id, mountpoint) except Exception: - logging.debug("instance %s: attach failed to %s, removing export", - instance_id, mountpoint) + logging.exception("instance %s: attach failed %s, removing", + instance_id, mountpoint) yield self.volume_manager.remove_compute_volume(context, volume_id) raise -- cgit From 60f3b009f3f846539dfeb2101eec73259553f8ea Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 25 Oct 2010 12:54:22 -0700 Subject: pep8 cleanup --- nova/compute/manager.py | 4 ++-- nova/db/sqlalchemy/models.py | 1 - nova/volume/manager.py | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) (limited to 'nova') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 80931a309..f2e80bff3 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -194,8 +194,8 @@ class ComputeManager(manager.Manager): instance_ref = self.db.instance_get(context, instance_id) volume_ref = self.db.volume_get(context, volume_id) if instance_ref['name'] not in self.driver.list_instances(): - logging.warn("Detaching volume from instance %s that isn't running", - instance_ref['name']) + logging.warn("Detaching volume from unknown instance %s", + instance_ref['name']) else: yield self.driver.detach_volume(instance_ref['name'], volume_ref['mountpoint']) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index b0adc3a2a..f0424cc64 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -266,7 +266,6 @@ class Volume(BASE, NovaBase): return self.ec2_id - class Quota(BASE, NovaBase): """Represents quota overrides for a project""" __tablename__ = 'quotas' diff --git a/nova/volume/manager.py b/nova/volume/manager.py index bc49e28ee..ee1c019ad 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -133,4 +133,3 @@ class VolumeManager(manager.Manager): defer.returnValue(True) else: yield self.driver.undiscover_volume(volume_ref) - -- cgit From 5318bf110019d820e6f000662194d6e29f3e315f Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 25 Oct 2010 17:15:56 -0700 Subject: fix tests by removing missed reference to prefix and unnecessary conditional in generate_uid --- nova/db/sqlalchemy/api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 0cbe56499..a3d8dde2f 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -19,6 +19,7 @@ Implementation of SQLAlchemy backend """ +import random import warnings from nova import db @@ -542,7 +543,8 @@ def instance_create(context, values): session = get_session() with session.begin(): while instance_ref.internal_id == None: - internal_id = utils.generate_uid(instance_ref.__prefix__) + # Instances have integer internal ids. + internal_id = random.randint(0, 2 ** 32 - 1) if not instance_internal_id_exists(context, internal_id, session=session): instance_ref.internal_id = internal_id @@ -1152,7 +1154,7 @@ def volume_create(context, values): session = get_session() with session.begin(): while volume_ref.ec2_id == None: - ec2_id = utils.generate_uid(volume_ref.__prefix__) + ec2_id = utils.generate_uid('vol') if not volume_ec2_id_exists(context, ec2_id, session=session): volume_ref.ec2_id = ec2_id volume_ref.save(session=session) -- cgit From 8ccdae97558d9660a9a0fac8dad809a09cbd3c71 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 25 Oct 2010 17:20:10 -0700 Subject: actually remove the conditional --- nova/utils.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) (limited to 'nova') diff --git a/nova/utils.py b/nova/utils.py index 7683fc9f4..7f6311209 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -131,13 +131,9 @@ def runthis(prompt, cmd, check_exit_code=True): def generate_uid(topic, size=8): - if topic == "i": - # Instances have integer internal ids. - return random.randint(0, 2 ** 32 - 1) - else: - characters = '01234567890abcdefghijklmnopqrstuvwxyz' - choices = [random.choice(characters) for x in xrange(size)] - return '%s-%s' % (topic, ''.join(choices)) + characters = '01234567890abcdefghijklmnopqrstuvwxyz' + choices = [random.choice(characters) for x in xrange(size)] + return '%s-%s' % (topic, ''.join(choices)) def generate_mac(): -- cgit From 627a968e79ed21d970225e5ece332d9100abe022 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Mon, 25 Oct 2010 23:04:49 -0700 Subject: fix completely broken ServiceTestCase --- nova/tests/service_unittest.py | 125 +++++++++++++++++++++++------------------ 1 file changed, 70 insertions(+), 55 deletions(-) (limited to 'nova') diff --git a/nova/tests/service_unittest.py b/nova/tests/service_unittest.py index e74e0f726..a268bc4fe 100644 --- a/nova/tests/service_unittest.py +++ b/nova/tests/service_unittest.py @@ -23,8 +23,8 @@ Unit Tests for remote procedure calls using queue import mox from twisted.application.app import startApplication +from twisted.internet import defer -from nova import context from nova import exception from nova import flags from nova import rpc @@ -48,7 +48,7 @@ class ExtendedService(service.Service): return 'service' -class ServiceManagerTestCase(test.BaseTestCase): +class ServiceManagerTestCase(test.TrialTestCase): """Test cases for Services""" def test_attribute_error_for_no_manager(self): @@ -75,13 +75,12 @@ class ServiceManagerTestCase(test.BaseTestCase): self.assertEqual(serv.test_method(), 'service') -class ServiceTestCase(test.BaseTestCase): +class ServiceTestCase(test.TrialTestCase): """Test cases for Services""" def setUp(self): super(ServiceTestCase, self).setUp() self.mox.StubOutWithMock(service, 'db') - self.context = context.get_admin_context() def test_create(self): host = 'foo' @@ -144,87 +143,103 @@ class ServiceTestCase(test.BaseTestCase): # whether it is disconnected, it looks for a variable on itself called # 'model_disconnected' and report_state doesn't really do much so this # these are mostly just for coverage - def test_report_state(self): - host = 'foo' - binary = 'bar' - service_ref = {'host': host, - 'binary': binary, - 'report_count': 0, - 'id': 1} - service.db.__getattr__('report_state') - service.db.service_get_by_args(self.context, - host, - binary).AndReturn(service_ref) - service.db.service_update(self.context, service_ref['id'], - mox.ContainsKeyValue('report_count', 1)) - - self.mox.ReplayAll() - s = service.Service() - rv = yield s.report_state(host, binary) - + @defer.inlineCallbacks def test_report_state_no_service(self): host = 'foo' binary = 'bar' + topic = 'test' service_create = {'host': host, 'binary': binary, + 'topic': topic, 'report_count': 0} service_ref = {'host': host, - 'binary': binary, - 'report_count': 0, - 'id': 1} + 'binary': binary, + 'topic': topic, + 'report_count': 0, + 'id': 1} - service.db.__getattr__('report_state') - service.db.service_get_by_args(self.context, + service.db.service_get_by_args(mox.IgnoreArg(), host, binary).AndRaise(exception.NotFound()) - service.db.service_create(self.context, + service.db.service_create(mox.IgnoreArg(), service_create).AndReturn(service_ref) - service.db.service_get(self.context, + service.db.service_get(mox.IgnoreArg(), service_ref['id']).AndReturn(service_ref) - service.db.service_update(self.context, service_ref['id'], + service.db.service_update(mox.IgnoreArg(), service_ref['id'], mox.ContainsKeyValue('report_count', 1)) self.mox.ReplayAll() - s = service.Service() - rv = yield s.report_state(host, binary) + serv = service.Service(host, + binary, + topic, + 'nova.tests.service_unittest.FakeManager') + serv.startService() + yield serv.report_state() + @defer.inlineCallbacks def test_report_state_newly_disconnected(self): host = 'foo' binary = 'bar' + topic = 'test' + service_create = {'host': host, + 'binary': binary, + 'topic': topic, + 'report_count': 0} service_ref = {'host': host, - 'binary': binary, - 'report_count': 0, - 'id': 1} + 'binary': binary, + 'topic': topic, + 'report_count': 0, + 'id': 1} - service.db.__getattr__('report_state') - service.db.service_get_by_args(self.context, - host, - binary).AndRaise(Exception()) + service.db.service_get_by_args(mox.IgnoreArg(), + host, + binary).AndRaise(exception.NotFound()) + service.db.service_create(mox.IgnoreArg(), + service_create).AndReturn(service_ref) + service.db.service_get(mox.IgnoreArg(), + mox.IgnoreArg()).AndRaise(Exception()) self.mox.ReplayAll() - s = service.Service() - rv = yield s.report_state(host, binary) - - self.assert_(s.model_disconnected) + serv = service.Service(host, + binary, + topic, + 'nova.tests.service_unittest.FakeManager') + serv.startService() + yield serv.report_state() + self.assert_(serv.model_disconnected) + @defer.inlineCallbacks def test_report_state_newly_connected(self): host = 'foo' binary = 'bar' + topic = 'test' + service_create = {'host': host, + 'binary': binary, + 'topic': topic, + 'report_count': 0} service_ref = {'host': host, - 'binary': binary, - 'report_count': 0, - 'id': 1} + 'binary': binary, + 'topic': topic, + 'report_count': 0, + 'id': 1} - service.db.__getattr__('report_state') - service.db.service_get_by_args(self.context, - host, - binary).AndReturn(service_ref) - service.db.service_update(self.context, service_ref['id'], + service.db.service_get_by_args(mox.IgnoreArg(), + host, + binary).AndRaise(exception.NotFound()) + service.db.service_create(mox.IgnoreArg(), + service_create).AndReturn(service_ref) + service.db.service_get(mox.IgnoreArg(), + service_ref['id']).AndReturn(service_ref) + service.db.service_update(mox.IgnoreArg(), service_ref['id'], mox.ContainsKeyValue('report_count', 1)) self.mox.ReplayAll() - s = service.Service() - s.model_disconnected = True - rv = yield s.report_state(host, binary) + serv = service.Service(host, + binary, + topic, + 'nova.tests.service_unittest.FakeManager') + serv.startService() + serv.model_disconnected = True + yield serv.report_state() - self.assert_(not s.model_disconnected) + self.assert_(not serv.model_disconnected) -- cgit From ba6d9293204284a7c74b5b0cffe63767941fd25c Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 26 Oct 2010 11:48:20 -0400 Subject: Delete BaseTestCase and with it the last reference to tornado. Requires commenting out some service_unittest tests which were silently failing under BaseTestCase and which now fail under TrialTestCase. vishy says he wrote the code and thinks he knows what is going wrong. --- nova/test.py | 156 ---------------------------------- nova/tests/api_unittest.py | 4 +- nova/tests/service_unittest.py | 184 +++++++++++++++++++++-------------------- 3 files changed, 96 insertions(+), 248 deletions(-) (limited to 'nova') diff --git a/nova/test.py b/nova/test.py index 8ef7eca1a..5c2a72819 100644 --- a/nova/test.py +++ b/nova/test.py @@ -28,7 +28,6 @@ import time import mox import stubout -from tornado import ioloop from twisted.internet import defer from twisted.trial import unittest @@ -159,158 +158,3 @@ class TrialTestCase(unittest.TestCase): _wrapped.func_name = self.originalAttach.func_name rpc.Consumer.attach_to_twisted = _wrapped - - -class BaseTestCase(TrialTestCase): - # TODO(jaypipes): Can this be moved into the TrialTestCase class? - """Base test case class for all unit tests. - - DEPRECATED: This is being removed once Tornado is gone, use TrialTestCase. - """ - def setUp(self): - """Run before each test method to initialize test environment""" - super(BaseTestCase, self).setUp() - # TODO(termie): we could possibly keep a more global registry of - # the injected listeners... this is fine for now though - self.ioloop = ioloop.IOLoop.instance() - - self._waiting = None - self._done_waiting = False - self._timed_out = False - - def _wait_for_test(self, timeout=60): - """ Push the ioloop along to wait for our test to complete. """ - self._waiting = self.ioloop.add_timeout(time.time() + timeout, - self._timeout) - - def _wait(): - - """Wrapped wait function. Called on timeout.""" - if self._timed_out: - self.fail('test timed out') - self._done() - if self._done_waiting: - self.ioloop.stop() - return - # we can use add_callback here but this uses less cpu when testing - self.ioloop.add_timeout(time.time() + 0.01, _wait) - - self.ioloop.add_callback(_wait) - self.ioloop.start() - - def _done(self): - """Callback used for cleaning up deferred test methods.""" - if self._waiting: - try: - self.ioloop.remove_timeout(self._waiting) - except Exception: # pylint: disable-msg=W0703 - # TODO(jaypipes): This produces a pylint warning. Should - # we really be catching Exception and then passing here? - pass - self._waiting = None - self._done_waiting = True - - def _maybe_inline_callbacks(self, func): - """ If we're doing async calls in our tests, wait on them. - - This is probably the most complicated hunk of code we have so far. - - First up, if the function is normal (not async) we just act normal - and return. - - Async tests will use the "Inline Callbacks" pattern, which means - you yield Deferreds at every "waiting" step of your code instead - of making epic callback chains. - - Example (callback chain, ugly): - - # A deferred instance - d = self.compute.terminate_instance(instance_id) - def _describe(_): - # Another deferred instance - d_desc = self.compute.describe_instances() - return d_desc - def _checkDescribe(rv): - self.assertEqual(rv, []) - d.addCallback(_describe) - d.addCallback(_checkDescribe) - d.addCallback(lambda x: self._done()) - self._wait_for_test() - - Example (inline callbacks! yay!): - - yield self.compute.terminate_instance(instance_id) - rv = yield self.compute.describe_instances() - self.assertEqual(rv, []) - - If the test fits the Inline Callbacks pattern we will automatically - handle calling wait and done. - """ - # TODO(termie): this can be a wrapper function instead and - # and we can make a metaclass so that we don't - # have to copy all that "run" code below. - g = func() - if not hasattr(g, 'send'): - self._done() - return defer.succeed(g) - - inlined = defer.inlineCallbacks(func) - d = inlined() - return d - - def _catch_exceptions(self, result, failure): - """Catches all exceptions and handles keyboard interrupts.""" - exc = (failure.type, failure.value, failure.getTracebackObject()) - if isinstance(failure.value, self.failureException): - result.addFailure(self, exc) - elif isinstance(failure.value, KeyboardInterrupt): - raise - else: - result.addError(self, exc) - - self._done() - - def _timeout(self): - """Helper method which trips the timeouts""" - self._waiting = False - self._timed_out = True - - def run(self, result=None): - """Runs the test case""" - - result.startTest(self) - test_method = getattr(self, self._testMethodName) - try: - try: - self.setUp() - except KeyboardInterrupt: - raise - except: - result.addError(self, sys.exc_info()) - return - - ok = False - try: - d = self._maybe_inline_callbacks(test_method) - d.addErrback(lambda x: self._catch_exceptions(result, x)) - d.addBoth(lambda x: self._done() and x) - self._wait_for_test() - ok = True - except self.failureException: - result.addFailure(self, sys.exc_info()) - except KeyboardInterrupt: - raise - except: - result.addError(self, sys.exc_info()) - - try: - self.tearDown() - except KeyboardInterrupt: - raise - except: - result.addError(self, sys.exc_info()) - ok = False - if ok: - result.addSuccess(self) - finally: - result.stopTest(self) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 0b1c3e353..0a81c575b 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -83,7 +83,7 @@ class FakeHttplibConnection(object): pass -class XmlConversionTestCase(test.BaseTestCase): +class XmlConversionTestCase(test.TrialTestCase): """Unit test api xml conversion""" def test_number_conversion(self): conv = apirequest._try_convert @@ -100,7 +100,7 @@ class XmlConversionTestCase(test.BaseTestCase): self.assertEqual(conv('-0'), 0) -class ApiEc2TestCase(test.BaseTestCase): +class ApiEc2TestCase(test.TrialTestCase): """Unit test for the cloud controller on an EC2 API""" def setUp(self): super(ApiEc2TestCase, self).setUp() diff --git a/nova/tests/service_unittest.py b/nova/tests/service_unittest.py index e74e0f726..142c2ebea 100644 --- a/nova/tests/service_unittest.py +++ b/nova/tests/service_unittest.py @@ -48,7 +48,7 @@ class ExtendedService(service.Service): return 'service' -class ServiceManagerTestCase(test.BaseTestCase): +class ServiceManagerTestCase(test.TrialTestCase): """Test cases for Services""" def test_attribute_error_for_no_manager(self): @@ -75,7 +75,7 @@ class ServiceManagerTestCase(test.BaseTestCase): self.assertEqual(serv.test_method(), 'service') -class ServiceTestCase(test.BaseTestCase): +class ServiceTestCase(test.TrialTestCase): """Test cases for Services""" def setUp(self): @@ -140,91 +140,95 @@ class ServiceTestCase(test.BaseTestCase): startApplication(app, False) self.assert_(app) - # We're testing sort of weird behavior in how report_state decides - # whether it is disconnected, it looks for a variable on itself called - # 'model_disconnected' and report_state doesn't really do much so this - # these are mostly just for coverage - def test_report_state(self): - host = 'foo' - binary = 'bar' - service_ref = {'host': host, - 'binary': binary, - 'report_count': 0, - 'id': 1} - service.db.__getattr__('report_state') - service.db.service_get_by_args(self.context, - host, - binary).AndReturn(service_ref) - service.db.service_update(self.context, service_ref['id'], - mox.ContainsKeyValue('report_count', 1)) - - self.mox.ReplayAll() - s = service.Service() - rv = yield s.report_state(host, binary) - - def test_report_state_no_service(self): - host = 'foo' - binary = 'bar' - service_create = {'host': host, - 'binary': binary, - 'report_count': 0} - service_ref = {'host': host, - 'binary': binary, - 'report_count': 0, - 'id': 1} - - service.db.__getattr__('report_state') - service.db.service_get_by_args(self.context, - host, - binary).AndRaise(exception.NotFound()) - service.db.service_create(self.context, - service_create).AndReturn(service_ref) - service.db.service_get(self.context, - service_ref['id']).AndReturn(service_ref) - service.db.service_update(self.context, service_ref['id'], - mox.ContainsKeyValue('report_count', 1)) - - self.mox.ReplayAll() - s = service.Service() - rv = yield s.report_state(host, binary) - - def test_report_state_newly_disconnected(self): - host = 'foo' - binary = 'bar' - service_ref = {'host': host, - 'binary': binary, - 'report_count': 0, - 'id': 1} - - service.db.__getattr__('report_state') - service.db.service_get_by_args(self.context, - host, - binary).AndRaise(Exception()) - - self.mox.ReplayAll() - s = service.Service() - rv = yield s.report_state(host, binary) - - self.assert_(s.model_disconnected) - - def test_report_state_newly_connected(self): - host = 'foo' - binary = 'bar' - service_ref = {'host': host, - 'binary': binary, - 'report_count': 0, - 'id': 1} - - service.db.__getattr__('report_state') - service.db.service_get_by_args(self.context, - host, - binary).AndReturn(service_ref) - service.db.service_update(self.context, service_ref['id'], - mox.ContainsKeyValue('report_count', 1)) - - self.mox.ReplayAll() - s = service.Service() - s.model_disconnected = True - rv = yield s.report_state(host, binary) - - self.assert_(not s.model_disconnected) +# TODO(gundlach): These tests were "passing" when this class inherited from +# BaseTestCase. In reality, they were failing, but BaseTestCase was +# swallowing the error. Now that we inherit from TrialTestCase, these tests +# are failing, and need to get fixed. +# # We're testing sort of weird behavior in how report_state decides +# # whether it is disconnected, it looks for a variable on itself called +# # 'model_disconnected' and report_state doesn't really do much so this +# # these are mostly just for coverage +# def test_report_state(self): +# host = 'foo' +# binary = 'bar' +# service_ref = {'host': host, +# 'binary': binary, +# 'report_count': 0, +# 'id': 1} +# service.db.__getattr__('report_state') +# service.db.service_get_by_args(self.context, +# host, +# binary).AndReturn(service_ref) +# service.db.service_update(self.context, service_ref['id'], +# mox.ContainsKeyValue('report_count', 1)) +# +# self.mox.ReplayAll() +# s = service.Service() +# rv = yield s.report_state(host, binary) +# +# def test_report_state_no_service(self): +# host = 'foo' +# binary = 'bar' +# service_create = {'host': host, +# 'binary': binary, +# 'report_count': 0} +# service_ref = {'host': host, +# 'binary': binary, +# 'report_count': 0, +# 'id': 1} +# +# service.db.__getattr__('report_state') +# service.db.service_get_by_args(self.context, +# host, +# binary).AndRaise(exception.NotFound()) +# service.db.service_create(self.context, +# service_create).AndReturn(service_ref) +# service.db.service_get(self.context, +# service_ref['id']).AndReturn(service_ref) +# service.db.service_update(self.context, service_ref['id'], +# mox.ContainsKeyValue('report_count', 1)) +# +# self.mox.ReplayAll() +# s = service.Service() +# rv = yield s.report_state(host, binary) +# +# def test_report_state_newly_disconnected(self): +# host = 'foo' +# binary = 'bar' +# service_ref = {'host': host, +# 'binary': binary, +# 'report_count': 0, +# 'id': 1} +# +# service.db.__getattr__('report_state') +# service.db.service_get_by_args(self.context, +# host, +# binary).AndRaise(Exception()) +# +# self.mox.ReplayAll() +# s = service.Service() +# rv = yield s.report_state(host, binary) +# +# self.assert_(s.model_disconnected) +# +# def test_report_state_newly_connected(self): +# host = 'foo' +# binary = 'bar' +# service_ref = {'host': host, +# 'binary': binary, +# 'report_count': 0, +# 'id': 1} +# +# service.db.__getattr__('report_state') +# service.db.service_get_by_args(self.context, +# host, +# binary).AndReturn(service_ref) +# service.db.service_update(self.context, service_ref['id'], +# mox.ContainsKeyValue('report_count', 1)) +# +# self.mox.ReplayAll() +# s = service.Service() +# s.model_disconnected = True +# rv = yield s.report_state(host, binary) +# +# self.assert_(not s.model_disconnected) -- cgit From d8d12549a5e47c7c44f449f12d6b556e2c56483d Mon Sep 17 00:00:00 2001 From: Eric Day Date: Tue, 26 Oct 2010 15:37:32 -0700 Subject: More PEP8 fixes that were introduced in the last couple commits. --- nova/tests/api/openstack/test_api.py | 6 ++++++ nova/utils.py | 5 ++--- 2 files changed, 8 insertions(+), 3 deletions(-) (limited to 'nova') diff --git a/nova/tests/api/openstack/test_api.py b/nova/tests/api/openstack/test_api.py index a8c0ff9f8..dd83991b9 100644 --- a/nova/tests/api/openstack/test_api.py +++ b/nova/tests/api/openstack/test_api.py @@ -24,22 +24,28 @@ from nova.api.openstack import API from nova.api.openstack import faults from webob import Request + class APITest(unittest.TestCase): def test_exceptions_are_converted_to_faults(self): + @webob.dec.wsgify def succeed(req): return 'Succeeded' + @webob.dec.wsgify def raise_webob_exc(req): raise webob.exc.HTTPNotFound(explanation='Raised a webob.exc') + @webob.dec.wsgify def fail(req): raise Exception("Threw an exception") + @webob.dec.wsgify def raise_api_fault(req): exc = webob.exc.HTTPNotFound(explanation='Raised a webob.exc') return faults.Fault(exc) + api = API() api.application = succeed diff --git a/nova/utils.py b/nova/utils.py index 2c53b027e..bc495a691 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -213,10 +213,10 @@ def deferredToThread(f): def xhtml_escape(value): """Escapes a string so it is valid within XML or XHTML. - + Code is directly from the utf8 function in http://github.com/facebook/tornado/blob/master/tornado/escape.py - + """ return saxutils.escape(value, {'"': """}) @@ -232,4 +232,3 @@ def utf8(value): return value.encode("utf-8") assert isinstance(value, str) return value - -- cgit From 79acdcca7d37e81d626be7a3369394ef9dface1b Mon Sep 17 00:00:00 2001 From: "jaypipes@gmail.com" <> Date: Wed, 27 Oct 2010 11:10:50 -0400 Subject: Style cleanups and review from Eric. --- nova/api/ec2/cloud.py | 23 ++++++++++++----------- nova/compute/manager.py | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 14 deletions(-) (limited to 'nova') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 51e972aa7..9084958a1 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -836,29 +836,30 @@ class CloudController(object): elevated = context.elevated() for num in range(num_instances): - + instance_data = base_options - instance_data['mac_address'] = utils.generate_mac() - instance_data['launch_index'] = num instance_ref = self.compute_manager.create_instance(context, - instance_data, - security_groups) + instance_data, + security_groups, + mac_address=utils.generate_mac(), + launch_index=num) + inst_id = instance_ref['id'] internal_id = instance_ref['internal_id'] ec2_id = internal_id_to_ec2_id(internal_id) - instance_ref['hostname'] = ec2_id self.compute_manager.update_instance(context, - instance_ref['id'], - instance_ref) + inst_id, + instance_ref, + hostname=ec2_id) # TODO(vish): This probably should be done in the scheduler # or in compute as a call. The network should be # allocated after the host is assigned and setup # can happen at the same time. address = self.network_manager.allocate_fixed_ip(context, - instance_ref['id'], + inst_id, vpn) network_topic = self._get_network_topic(context) rpc.cast(elevated, @@ -870,9 +871,9 @@ class CloudController(object): FLAGS.scheduler_topic, {"method": "run_instance", "args": {"topic": FLAGS.compute_topic, - "instance_id": instance_ref['id']}}) + "instance_id": inst_id}}) logging.debug("Casting to scheduler for %s/%s's instance %s" % - (context.project.name, context.user.name, instance_ref['id'])) + (context.project.name, context.user.name, inst_id)) return self._format_run_instances(context, reservation_id) def terminate_instances(self, context, instance_id, **kwargs): diff --git a/nova/compute/manager.py b/nova/compute/manager.py index d99d938af..c04dd213a 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -69,8 +69,8 @@ class ComputeManager(manager.Manager): def refresh_security_group(self, context, security_group_id, **_kwargs): yield self.driver.refresh_security_group(security_group_id) - - def create_instance(self, context, instance_data, security_groups=[]): + def create_instance(self, context, instance_data, security_groups=[], + **kwargs): """Creates the instance in the datastore and returns the new instance as a mapping @@ -78,11 +78,15 @@ class ComputeManager(manager.Manager): :param instance_data: mapping of instance options :param security_groups: list of security group ids to attach to the instance + :param **kwargs: All additional keyword args are treated + as data fields of the instance to be + created :retval Returns a mapping of the instance information that has just been created """ + instance_data.update(kwargs) instance_ref = self.db.instance_create(context, instance_data) inst_id = instance_ref['id'] @@ -93,15 +97,20 @@ class ComputeManager(manager.Manager): security_group_id) return instance_ref - def update_instance(self, context, instance_id, instance_data): + def update_instance(self, context, instance_id, instance_data, + **kwargs): """Updates the instance in the datastore :param context: The security context :param instance_data: mapping of instance options + :param **kwargs: All additional keyword args are treated + as data fields of the instance to be + updated :retval None """ + instance_data.update(kwargs) self.db.instance_update(context, instance_id, instance_data) @defer.inlineCallbacks -- cgit From 7c74613eb801679c67f551e307265b4af1dc12a6 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 27 Oct 2010 10:28:52 -0700 Subject: updates from review, fix models.get and note about exception raising --- nova/compute/manager.py | 7 +++++-- nova/db/sqlalchemy/models.py | 5 +---- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'nova') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 69ac398c0..b9ba6852a 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -208,12 +208,15 @@ class ComputeManager(manager.Manager): volume_id, instance_id, mountpoint) - except Exception: + except Exception as exc: # pylint: disable-msg=W0702 + # NOTE(vish): The inline callback eats the exception info so we + # log the traceback here and reraise the same + # ecxception below. logging.exception("instance %s: attach failed %s, removing", instance_id, mountpoint) yield self.volume_manager.remove_compute_volume(context, volume_id) - raise + raise exc defer.returnValue(True) @defer.inlineCallbacks diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index ed1bf6c85..7d65cd371 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -75,10 +75,7 @@ class NovaBase(object): return getattr(self, key) def get(self, key, default=None): - try: - return getattr(self, key) - except AttributeError, KeyError: - return default + return getattr(self, key, default) def __iter__(self): self._i = iter(object_mapper(self).columns) -- cgit From 213b9987365c4b336b63e08e1ca187a43d00fa3d Mon Sep 17 00:00:00 2001 From: "jaypipes@gmail.com" <> Date: Wed, 27 Oct 2010 14:55:01 -0400 Subject: OK, let's try this one more time. --- nova/api/ec2/cloud.py | 13 ++++++------- nova/api/openstack/servers.py | 4 ++-- nova/compute/manager.py | 16 ++++++---------- 3 files changed, 14 insertions(+), 19 deletions(-) (limited to 'nova') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 9084958a1..7b6144ba5 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -837,13 +837,11 @@ class CloudController(object): for num in range(num_instances): - instance_data = base_options - instance_ref = self.compute_manager.create_instance(context, - instance_data, security_groups, mac_address=utils.generate_mac(), - launch_index=num) + launch_index=num, + **base_options) inst_id = instance_ref['id'] internal_id = instance_ref['internal_id'] @@ -851,7 +849,6 @@ class CloudController(object): self.compute_manager.update_instance(context, inst_id, - instance_ref, hostname=ec2_id) # TODO(vish): This probably should be done in the scheduler @@ -903,8 +900,10 @@ class CloudController(object): 'state': 0, 'terminated_at': now} self.compute_manager.update_instance(context, - instance_ref['id'], - updated_data) + instance_ref['id'], + state_description='terminating', + state=0, + terminated_at=now) # FIXME(ja): where should network deallocate occur? address = db.instance_get_floating_address(context, diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index e1a254d4e..1d8aa2fa4 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -246,11 +246,11 @@ class Controller(wsgi.Controller): inst['mac_address'] = utils.generate_mac() inst['launch_index'] = 0 - ref = self.compute_manager.create_instance(ctxt, inst) + ref = self.compute_manager.create_instance(ctxt, **inst) inst['id'] = ref['internal_id'] inst['hostname'] = str(ref['internal_id']) - self.compute_manager.update_instance(ctxt, inst['id'], inst) + self.compute_manager.update_instance(ctxt, inst['id'], **inst) address = self.network_manager.allocate_fixed_ip(ctxt, inst['id']) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index c04dd213a..7cdd6b110 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -69,13 +69,11 @@ class ComputeManager(manager.Manager): def refresh_security_group(self, context, security_group_id, **_kwargs): yield self.driver.refresh_security_group(security_group_id) - def create_instance(self, context, instance_data, security_groups=[], - **kwargs): + def create_instance(self, context, security_groups=[], **kwargs): """Creates the instance in the datastore and returns the new instance as a mapping :param context: The security context - :param instance_data: mapping of instance options :param security_groups: list of security group ids to attach to the instance :param **kwargs: All additional keyword args are treated @@ -86,23 +84,22 @@ class ComputeManager(manager.Manager): that has just been created """ - instance_data.update(kwargs) - instance_ref = self.db.instance_create(context, instance_data) + instance_ref = self.db.instance_create(context, kwargs) inst_id = instance_ref['id'] elevated = context.elevated() + security_groups = kwargs.get('security_groups', []) for security_group_id in security_groups: self.db.instance_add_security_group(elevated, inst_id, security_group_id) return instance_ref - def update_instance(self, context, instance_id, instance_data, - **kwargs): + def update_instance(self, context, instance_id, **kwargs): """Updates the instance in the datastore :param context: The security context - :param instance_data: mapping of instance options + :param instance_id: ID of the instance to update :param **kwargs: All additional keyword args are treated as data fields of the instance to be updated @@ -110,8 +107,7 @@ class ComputeManager(manager.Manager): :retval None """ - instance_data.update(kwargs) - self.db.instance_update(context, instance_id, instance_data) + self.db.instance_update(context, instance_id, kwargs) @defer.inlineCallbacks @exception.wrap_exception -- cgit From a1287cf4e15d469163ec6465ea5f6ce200c20543 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Wed, 27 Oct 2010 17:31:46 -0400 Subject: cleanup rrd doc generation. --- nova/auth/fakeldap.py | 1 - nova/auth/manager.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) (limited to 'nova') diff --git a/nova/auth/fakeldap.py b/nova/auth/fakeldap.py index cf3a84a5d..1a49b73fe 100644 --- a/nova/auth/fakeldap.py +++ b/nova/auth/fakeldap.py @@ -79,7 +79,6 @@ def _match_query(query, attrs): &, |, and ! are supported in the query. No syntax checking is performed, so malformed querys will not work correctly. - """ # cut off the parentheses inner = query[1:-1] diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 001a96875..7b2b68161 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -84,12 +84,11 @@ class AuthBase(object): @classmethod def safe_id(cls, obj): - """Safe get object id + """Safely get object id. This method will return the id of the object if the object is of this class, otherwise it will return the original object. This allows methods to accept objects or ids as paramaters. - """ if isinstance(obj, cls): return obj.id -- cgit From 4012860b57593632d1f0061099e0d211dba58a59 Mon Sep 17 00:00:00 2001 From: "jaypipes@gmail.com" <> Date: Thu, 28 Oct 2010 11:43:08 -0400 Subject: Remove unused updated_data variable --- nova/api/ec2/cloud.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'nova') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 7b6144ba5..f2a6dc3b0 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -896,9 +896,6 @@ class CloudController(object): id_str) continue now = datetime.datetime.utcnow() - updated_data = {'state_description': 'terminating', - 'state': 0, - 'terminated_at': now} self.compute_manager.update_instance(context, instance_ref['id'], state_description='terminating', -- cgit From e85ba051c27ab7d50914c7bf91db74d7cf7faa97 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Thu, 28 Oct 2010 12:00:25 -0400 Subject: clean up the compute documentation a bit. --- nova/compute/manager.py | 24 +++++++++++++++++++----- nova/virt/connection.py | 11 ++++++++++- 2 files changed, 29 insertions(+), 6 deletions(-) (limited to 'nova') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 574feec7c..c5102c35a 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -17,7 +17,20 @@ # under the License. """ -Handles all code relating to instances (guest vms) +Handles all processes relating to instances (guest vms). + +The :py:class:`ComputeManager` class is a :py:class:`nova.manager.Manager` that +handles RPC calls relating to creating instances. It is responsible for +building a disk image, launching it via the underlying virtualization driver, +responding to calls to check it state, attaching persistent as well as termination. + +Related Flags +------------- +:instances_path: Where instances are kept on disk +:compute_driver: Name of class that is used to handle virtualization, loaded + by `nova.utils.import_object` +:volume_manager: Name of class that handles persistent storage, loaded by + `nova.utils.import_object` """ import datetime @@ -40,12 +53,12 @@ flags.DEFINE_string('compute_driver', 'nova.virt.connection.get_connection', class ComputeManager(manager.Manager): - """ - Manages the running instances. - """ + """Manages the running instances from creation to destruction.""" + def __init__(self, compute_driver=None, *args, **kwargs): """Load configuration options and connect to the hypervisor.""" # TODO(vish): sync driver creation logic with the rest of the system + # and redocument the module docstring if not compute_driver: compute_driver = FLAGS.compute_driver self.driver = utils.import_object(compute_driver) @@ -54,7 +67,7 @@ class ComputeManager(manager.Manager): super(ComputeManager, self).__init__(*args, **kwargs) def _update_state(self, context, instance_id): - """Update the state of an instance from the driver info""" + """Update the state of an instance from the driver info.""" # FIXME(ja): include other fields from state? instance_ref = self.db.instance_get(context, instance_id) try: @@ -67,6 +80,7 @@ class ComputeManager(manager.Manager): @defer.inlineCallbacks @exception.wrap_exception def refresh_security_group(self, context, security_group_id, **_kwargs): + """This call passes stright through to the virtualization driver.""" yield self.driver.refresh_security_group(security_group_id) @defer.inlineCallbacks diff --git a/nova/virt/connection.py b/nova/virt/connection.py index 34e37adf7..ceb7f1e4b 100644 --- a/nova/virt/connection.py +++ b/nova/virt/connection.py @@ -17,7 +17,7 @@ # License for the specific language governing permissions and limitations # under the License. -"""Abstraction of the underlying virtualization API""" +"""Abstraction of the underlying virtualization API.""" import logging import sys @@ -39,6 +39,15 @@ def get_connection(read_only=False): Any object returned here must conform to the interface documented by FakeConnection. + + Related flags + ------------- + :connection_type: A string literal that falls through a if/elif structure + to determine what virtualization mechanism to use. + Values may be: + * fake + * libvirt + * xenapi """ # TODO(termie): maybe lazy load after initial check for permissions # TODO(termie): check whether we can be disconnected -- cgit From 208da85e85131a9b60a1fadea3e4242fa70dcde2 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Thu, 28 Oct 2010 12:25:39 -0400 Subject: Whitespace and docstring cleanups --- nova/auth/fakeldap.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'nova') diff --git a/nova/auth/fakeldap.py b/nova/auth/fakeldap.py index 1a49b73fe..176a00f06 100644 --- a/nova/auth/fakeldap.py +++ b/nova/auth/fakeldap.py @@ -15,12 +15,14 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + """ -Fake LDAP server for test harnesses. +Fake LDAP server for test harness, backs to ReDIS. This class does very little error checking, and knows nothing about ldap -class definitions. It implements the minimum emulation of the python ldap +class definitions. It implements the minimum emulation of the python ldap library to work with nova. + """ import json @@ -77,8 +79,8 @@ def initialize(_uri): def _match_query(query, attrs): """Match an ldap query to an attribute dictionary. - &, |, and ! are supported in the query. No syntax checking is performed, - so malformed querys will not work correctly. + The characters &, |, and ! are supported in the query. No syntax checking + is performed, so malformed querys will not work correctly. """ # cut off the parentheses inner = query[1:-1] -- cgit From 4bd42d5ee9eadb9affb40ee6ed0f98b13609c895 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Thu, 28 Oct 2010 12:26:29 -0400 Subject: Another heading was too distracting, use instead. --- nova/compute/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index c5102c35a..174fb0aca 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -24,8 +24,8 @@ handles RPC calls relating to creating instances. It is responsible for building a disk image, launching it via the underlying virtualization driver, responding to calls to check it state, attaching persistent as well as termination. -Related Flags -------------- +**Related Flags** + :instances_path: Where instances are kept on disk :compute_driver: Name of class that is used to handle virtualization, loaded by `nova.utils.import_object` -- cgit From a592636054511382105dc81d4a6b2a44df0dad9a Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Thu, 28 Oct 2010 17:08:13 -0400 Subject: :func: links to python functions in the documentation. --- nova/compute/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 174fb0aca..3346d1299 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -28,9 +28,9 @@ responding to calls to check it state, attaching persistent as well as terminati :instances_path: Where instances are kept on disk :compute_driver: Name of class that is used to handle virtualization, loaded - by `nova.utils.import_object` + by :func:`nova.utils.import_object` :volume_manager: Name of class that handles persistent storage, loaded by - `nova.utils.import_object` + :func:`nova.utils.import_object` """ import datetime -- cgit From 7cc4bcd344221d517054641171f759b88112a459 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Thu, 28 Oct 2010 20:13:40 -0400 Subject: Pep-257 cleanups. --- nova/db/api.py | 128 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 70 insertions(+), 58 deletions(-) (limited to 'nova') diff --git a/nova/db/api.py b/nova/db/api.py index 0731e2e05..7cce591ad 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -16,7 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. """ -Defines interface for DB access +Defines interface for DB access. """ from nova import exception @@ -34,17 +34,17 @@ IMPL = utils.LazyPluggable(FLAGS['db_backend'], class NoMoreAddresses(exception.Error): - """No more available addresses""" + """No more available addresses.""" pass class NoMoreBlades(exception.Error): - """No more available blades""" + """No more available blades.""" pass class NoMoreNetworks(exception.Error): - """No more available networks""" + """No more available networks.""" pass @@ -62,30 +62,33 @@ def service_get(context, service_id): def service_get_all_by_topic(context, topic): - """Get all compute services for a given topic """ + """Get all compute services for a given topic.""" return IMPL.service_get_all_by_topic(context, topic) def service_get_all_compute_sorted(context): - """Get all compute services sorted by instance count + """Get all compute services sorted by instance count. + + Returns a list of (Service, instance_count) tuples. - Returns a list of (Service, instance_count) tuples """ return IMPL.service_get_all_compute_sorted(context) def service_get_all_network_sorted(context): - """Get all network services sorted by network count + """Get all network services sorted by network count. + + Returns a list of (Service, network_count) tuples. - Returns a list of (Service, network_count) tuples """ return IMPL.service_get_all_network_sorted(context) def service_get_all_volume_sorted(context): - """Get all volume services sorted by volume count + """Get all volume services sorted by volume count. + + Returns a list of (Service, volume_count) tuples. - Returns a list of (Service, volume_count) tuples """ return IMPL.service_get_all_volume_sorted(context) @@ -116,6 +119,7 @@ def floating_ip_allocate_address(context, host, project_id): """Allocate free floating ip and return the address. Raises if one is not available. + """ return IMPL.floating_ip_allocate_address(context, host, project_id) @@ -144,6 +148,7 @@ def floating_ip_disassociate(context, address): """Disassociate an floating ip from a fixed ip by address. Returns the address of the existing fixed ip. + """ return IMPL.floating_ip_disassociate(context, address) @@ -182,6 +187,7 @@ def fixed_ip_associate(context, address, instance_id): """Associate fixed ip to instance. Raises if fixed ip is not available. + """ return IMPL.fixed_ip_associate(context, address, instance_id) @@ -190,6 +196,7 @@ def fixed_ip_associate_pool(context, network_id, instance_id): """Find free ip in network and associate it to instance. Raises if one is not available. + """ return IMPL.fixed_ip_associate_pool(context, network_id, instance_id) @@ -205,7 +212,7 @@ def fixed_ip_disassociate(context, address): def fixed_ip_disassociate_all_by_timeout(context, host, time): - """Disassociate old fixed ips from host""" + """Disassociate old fixed ips from host.""" return IMPL.fixed_ip_disassociate_all_by_timeout(context, host, time) @@ -283,7 +290,7 @@ def instance_get_floating_address(context, instance_id): def instance_get_by_internal_id(context, internal_id): - """Get an instance by ec2 id.""" + """Get an instance by internal id.""" return IMPL.instance_get_by_internal_id(context, internal_id) @@ -307,7 +314,7 @@ def instance_update(context, instance_id, values): def instance_add_security_group(context, instance_id, security_group_id): - """Associate the given security group with the given instance""" + """Associate the given security group with the given instance.""" return IMPL.instance_add_security_group(context, instance_id, security_group_id) @@ -369,10 +376,12 @@ def network_count_reserved_ips(context, network_id): def network_create_safe(context, values): - """Create a network from the values dict + """Create a network from the values dict. The network is only returned if the create succeeds. If the create violates - constraints because the network already exists, no exception is raised.""" + constraints because the network already exists, no exception is raised. + + """ return IMPL.network_create_safe(context, values) @@ -413,22 +422,22 @@ def network_get_by_instance(context, instance_id): def network_get_index(context, network_id): - """Get non-conflicting index for network""" + """Get non-conflicting index for network.""" return IMPL.network_get_index(context, network_id) def network_get_vpn_ip(context, network_id): - """Get non-conflicting index for network""" + """Get non-conflicting index for network.""" return IMPL.network_get_vpn_ip(context, network_id) def network_set_cidr(context, network_id, cidr): - """Set the Classless Inner Domain Routing for the network""" + """Set the Classless Inner Domain Routing for the network.""" return IMPL.network_set_cidr(context, network_id, cidr) def network_set_host(context, network_id, host_id): - """Safely set the host for network""" + """Safely set the host for network.""" return IMPL.network_set_host(context, network_id, host_id) @@ -474,7 +483,9 @@ def export_device_create_safe(context, values): The device is not returned. If the create violates the unique constraints because the shelf_id and blade_id already exist, - no exception is raised.""" + no exception is raised. + + """ return IMPL.export_device_create_safe(context, values) @@ -482,17 +493,17 @@ def export_device_create_safe(context, values): def auth_destroy_token(context, token): - """Destroy an auth token""" + """Destroy an auth token.""" return IMPL.auth_destroy_token(context, token) def auth_get_token(context, token_hash): - """Retrieves a token given the hash representing it""" + """Retrieves a token given the hash representing it.""" return IMPL.auth_get_token(context, token_hash) def auth_create_token(context, token): - """Creates a new token""" + """Creates a new token.""" return IMPL.auth_create_token(context, token) @@ -595,47 +606,47 @@ def volume_update(context, volume_id, values): def security_group_get_all(context): - """Get all security groups""" + """Get all security groups.""" return IMPL.security_group_get_all(context) def security_group_get(context, security_group_id): - """Get security group by its internal id""" + """Get security group by its internal id.""" return IMPL.security_group_get(context, security_group_id) def security_group_get_by_name(context, project_id, group_name): - """Returns a security group with the specified name from a project""" + """Returns a security group with the specified name from a project.""" return IMPL.security_group_get_by_name(context, project_id, group_name) def security_group_get_by_project(context, project_id): - """Get all security groups belonging to a project""" + """Get all security groups belonging to a project.""" return IMPL.security_group_get_by_project(context, project_id) def security_group_get_by_instance(context, instance_id): - """Get security groups to which the instance is assigned""" + """Get security groups to which the instance is assigned.""" return IMPL.security_group_get_by_instance(context, instance_id) def security_group_exists(context, project_id, group_name): - """Indicates if a group name exists in a project""" + """Indicates if a group name exists in a project.""" return IMPL.security_group_exists(context, project_id, group_name) def security_group_create(context, values): - """Create a new security group""" + """Create a new security group.""" return IMPL.security_group_create(context, values) def security_group_destroy(context, security_group_id): - """Deletes a security group""" + """Deletes a security group.""" return IMPL.security_group_destroy(context, security_group_id) def security_group_destroy_all(context): - """Deletes a security group""" + """Deletes a security group.""" return IMPL.security_group_destroy_all(context) @@ -643,18 +654,18 @@ def security_group_destroy_all(context): def security_group_rule_create(context, values): - """Create a new security group""" + """Create a new security group.""" return IMPL.security_group_rule_create(context, values) def security_group_rule_get_by_security_group(context, security_group_id): - """Get all rules for a a given security group""" + """Get all rules for a a given security group.""" return IMPL.security_group_rule_get_by_security_group(context, security_group_id) def security_group_rule_destroy(context, security_group_rule_id): - """Deletes a security group rule""" + """Deletes a security group rule.""" return IMPL.security_group_rule_destroy(context, security_group_rule_id) @@ -662,107 +673,107 @@ def security_group_rule_destroy(context, security_group_rule_id): def user_get(context, id): - """Get user by id""" + """Get user by id.""" return IMPL.user_get(context, id) def user_get_by_uid(context, uid): - """Get user by uid""" + """Get user by uid.""" return IMPL.user_get_by_uid(context, uid) def user_get_by_access_key(context, access_key): - """Get user by access key""" + """Get user by access key.""" return IMPL.user_get_by_access_key(context, access_key) def user_create(context, values): - """Create a new user""" + """Create a new user.""" return IMPL.user_create(context, values) def user_delete(context, id): - """Delete a user""" + """Delete a user.""" return IMPL.user_delete(context, id) def user_get_all(context): - """Create a new user""" + """Create a new user.""" return IMPL.user_get_all(context) def user_add_role(context, user_id, role): - """Add another global role for user""" + """Add another global role for user.""" return IMPL.user_add_role(context, user_id, role) def user_remove_role(context, user_id, role): - """Remove global role from user""" + """Remove global role from user.""" return IMPL.user_remove_role(context, user_id, role) def user_get_roles(context, user_id): - """Get global roles for user""" + """Get global roles for user.""" return IMPL.user_get_roles(context, user_id) def user_add_project_role(context, user_id, project_id, role): - """Add project role for user""" + """Add project role for user.""" return IMPL.user_add_project_role(context, user_id, project_id, role) def user_remove_project_role(context, user_id, project_id, role): - """Remove project role from user""" + """Remove project role from user.""" return IMPL.user_remove_project_role(context, user_id, project_id, role) def user_get_roles_for_project(context, user_id, project_id): - """Return list of roles a user holds on project""" + """Return list of roles a user holds on project.""" return IMPL.user_get_roles_for_project(context, user_id, project_id) def user_update(context, user_id, values): - """Update user""" + """Update user.""" return IMPL.user_update(context, user_id, values) def project_get(context, id): - """Get project by id""" + """Get project by id.""" return IMPL.project_get(context, id) def project_create(context, values): - """Create a new project""" + """Create a new project.""" return IMPL.project_create(context, values) def project_add_member(context, project_id, user_id): - """Add user to project""" + """Add user to project.""" return IMPL.project_add_member(context, project_id, user_id) def project_get_all(context): - """Get all projects""" + """Get all projects.""" return IMPL.project_get_all(context) def project_get_by_user(context, user_id): - """Get all projects of which the given user is a member""" + """Get all projects of which the given user is a member.""" return IMPL.project_get_by_user(context, user_id) def project_remove_member(context, project_id, user_id): - """Remove the given user from the given project""" + """Remove the given user from the given project.""" return IMPL.project_remove_member(context, project_id, user_id) def project_update(context, project_id, values): - """Update Remove the given user from the given project""" + """Update Remove the given user from the given project.""" return IMPL.project_update(context, project_id, values) def project_delete(context, project_id): - """Delete project""" + """Delete project.""" return IMPL.project_delete(context, project_id) @@ -771,6 +782,7 @@ def project_delete(context, project_id): def host_get_networks(context, host): """Return all networks for which the given host is the designated - network host + network host. + """ return IMPL.host_get_networks(context, host) -- cgit From 2132c0de46fd3f1b938e4b3b01b73fb2efaf6a38 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Thu, 28 Oct 2010 20:28:13 -0400 Subject: Pep-257 --- nova/db/sqlalchemy/models.py | 47 +++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 22 deletions(-) (limited to 'nova') diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 2a3cfa94c..894ebcddd 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -15,7 +15,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - """ SQLAlchemy models for nova data """ @@ -35,13 +34,13 @@ from nova import auth from nova import exception from nova import flags -FLAGS = flags.FLAGS +FLAGS = flags.FLAGS BASE = declarative_base() class NovaBase(object): - """Base class for Nova Models""" + """Base class for Nova Models.""" __table_args__ = {'mysql_engine': 'InnoDB'} __table_initialized__ = False created_at = Column(DateTime, default=datetime.datetime.utcnow) @@ -50,7 +49,7 @@ class NovaBase(object): deleted = Column(Boolean, default=False) def save(self, session=None): - """Save this object""" + """Save this object.""" if not session: session = get_session() session.add(self) @@ -63,7 +62,7 @@ class NovaBase(object): raise def delete(self, session=None): - """Delete this object""" + """Delete this object.""" self.deleted = True self.deleted_at = datetime.datetime.utcnow() self.save(session=session) @@ -128,7 +127,8 @@ class NovaBase(object): class Service(BASE, NovaBase): - """Represents a running service on a host""" + """Represents a running service on a host.""" + __tablename__ = 'services' id = Column(Integer, primary_key=True) host = Column(String(255)) # , ForeignKey('hosts.id')) @@ -139,7 +139,7 @@ class Service(BASE, NovaBase): class Instance(BASE, NovaBase): - """Represents a guest vm""" + """Represents a guest vm.""" __tablename__ = 'instances' id = Column(Integer, primary_key=True) internal_id = Column(Integer, unique=True) @@ -215,7 +215,7 @@ class Instance(BASE, NovaBase): class Volume(BASE, NovaBase): - """Represents a block storage device that can be attached to a vm""" + """Represents a block storage device that can be attached to a vm.""" __tablename__ = 'volumes' id = Column(Integer, primary_key=True) ec2_id = Column(String(12), unique=True) @@ -246,7 +246,7 @@ class Volume(BASE, NovaBase): class Quota(BASE, NovaBase): - """Represents quota overrides for a project""" + """Represents quota overrides for a project.""" __tablename__ = 'quotas' id = Column(Integer, primary_key=True) @@ -260,7 +260,7 @@ class Quota(BASE, NovaBase): class ExportDevice(BASE, NovaBase): - """Represates a shelf and blade that a volume can be exported on""" + """Represates a shelf and blade that a volume can be exported on.""" __tablename__ = 'export_devices' __table_args__ = (schema.UniqueConstraint("shelf_id", "blade_id"), {'mysql_engine': 'InnoDB'}) @@ -283,7 +283,7 @@ class SecurityGroupInstanceAssociation(BASE, NovaBase): class SecurityGroup(BASE, NovaBase): - """Represents a security group""" + """Represents a security group.""" __tablename__ = 'security_groups' id = Column(Integer, primary_key=True) @@ -313,7 +313,7 @@ class SecurityGroup(BASE, NovaBase): class SecurityGroupIngressRule(BASE, NovaBase): - """Represents a rule in a security group""" + """Represents a rule in a security group.""" __tablename__ = 'security_group_rules' id = Column(Integer, primary_key=True) @@ -335,7 +335,7 @@ class SecurityGroupIngressRule(BASE, NovaBase): class KeyPair(BASE, NovaBase): - """Represents a public key pair for ssh""" + """Represents a public key pair for ssh.""" __tablename__ = 'key_pairs' id = Column(Integer, primary_key=True) @@ -348,7 +348,7 @@ class KeyPair(BASE, NovaBase): class Network(BASE, NovaBase): - """Represents a network""" + """Represents a network.""" __tablename__ = 'networks' __table_args__ = (schema.UniqueConstraint("vpn_public_address", "vpn_public_port"), @@ -377,9 +377,12 @@ class Network(BASE, NovaBase): class AuthToken(BASE, NovaBase): - """Represents an authorization token for all API transactions. Fields - are a string representing the actual token and a user id for mapping - to the actual user""" + """Represents an authorization token for all API transactions. + + Fields are a string representing the actual token and a user id for + mapping to the actual user + + """ __tablename__ = 'auth_tokens' token_hash = Column(String(255), primary_key=True) user_id = Column(Integer) @@ -390,7 +393,7 @@ class AuthToken(BASE, NovaBase): # TODO(vish): can these both come from the same baseclass? class FixedIp(BASE, NovaBase): - """Represents a fixed ip for an instance""" + """Represents a fixed ip for an instance.""" __tablename__ = 'fixed_ips' id = Column(Integer, primary_key=True) address = Column(String(255)) @@ -409,7 +412,7 @@ class FixedIp(BASE, NovaBase): class User(BASE, NovaBase): - """Represents a user""" + """Represents a user.""" __tablename__ = 'users' id = Column(String(255), primary_key=True) @@ -421,7 +424,7 @@ class User(BASE, NovaBase): class Project(BASE, NovaBase): - """Represents a project""" + """Represents a project.""" __tablename__ = 'projects' id = Column(String(255), primary_key=True) name = Column(String(255)) @@ -469,7 +472,7 @@ class UserProjectAssociation(BASE, NovaBase): class FloatingIp(BASE, NovaBase): - """Represents a floating ip that dynamically forwards to a fixed ip""" + """Represents a floating ip that dynamically forwards to a fixed ip.""" __tablename__ = 'floating_ips' id = Column(Integer, primary_key=True) address = Column(String(255)) @@ -485,7 +488,7 @@ class FloatingIp(BASE, NovaBase): def register_models(): - """Register Models and create metadata""" + """Register Models and create metadata.""" from sqlalchemy import create_engine models = (Service, Instance, Volume, ExportDevice, FixedIp, FloatingIp, Network, SecurityGroup, -- cgit From 133cd9973e17458bea3594490e70ccd3c524cf12 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Fri, 29 Oct 2010 11:58:57 -0400 Subject: Document Fakes --- nova/auth/fakeldap.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'nova') diff --git a/nova/auth/fakeldap.py b/nova/auth/fakeldap.py index 176a00f06..46e0135b4 100644 --- a/nova/auth/fakeldap.py +++ b/nova/auth/fakeldap.py @@ -15,9 +15,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - -""" -Fake LDAP server for test harness, backs to ReDIS. +"""Fake LDAP server for test harness, backs to ReDIS. This class does very little error checking, and knows nothing about ldap class definitions. It implements the minimum emulation of the python ldap -- cgit From 3ec095bed60490c844067c8d58ed43dbedee5f0a Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Fri, 29 Oct 2010 12:35:46 -0400 Subject: Update database page a bit. --- nova/db/sqlalchemy/api.py | 2 +- nova/db/sqlalchemy/models.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) (limited to 'nova') diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index a3d8dde2f..d9b98655e 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -16,7 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. """ -Implementation of SQLAlchemy backend +Implementation of SQLAlchemy backend. """ import random diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 894ebcddd..29c3b74da 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -16,7 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. """ -SQLAlchemy models for nova data +SQLAlchemy models for nova data. """ import datetime @@ -488,7 +488,11 @@ class FloatingIp(BASE, NovaBase): def register_models(): - """Register Models and create metadata.""" + """Register Models and create metadata. + + Called from nova.db.sqlalchemy.__init__ as part of loading the driver, + it will never need to be called explicitly elsewhere. + """ from sqlalchemy import create_engine models = (Service, Instance, Volume, ExportDevice, FixedIp, FloatingIp, Network, SecurityGroup, -- cgit From 489ddea1668c742f62acd6fd3e9af78f2f782912 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Fri, 29 Oct 2010 15:30:39 -0400 Subject: Update database docs. --- nova/db/api.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'nova') diff --git a/nova/db/api.py b/nova/db/api.py index 7cce591ad..659bfd6b8 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -17,6 +17,16 @@ # under the License. """ Defines interface for DB access. + +The underlying driver is loaded as a :class:`LazyPluggable`. + +**Related Flags** + +:db_backend: string to lookup in the list of LazyPluggable backends. + `sqlalchemy` is the only supported backend right now. + +:sql_connection: string specifying the sqlalchemy connection to use, like: + `sqlite:///var/lib/nova/nova.sqlite`. """ from nova import exception -- cgit From 7ca505a729ee3caae968cf25059721ab51d1327c Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Fri, 29 Oct 2010 16:18:00 -0400 Subject: Volume documentation. --- nova/tests/volume_unittest.py | 21 ++++++++++++--------- nova/volume/driver.py | 24 +++++++++++++----------- nova/volume/manager.py | 39 +++++++++++++++++++++++++++++++-------- 3 files changed, 56 insertions(+), 28 deletions(-) (limited to 'nova') diff --git a/nova/tests/volume_unittest.py b/nova/tests/volume_unittest.py index fdee30b48..896800cea 100644 --- a/nova/tests/volume_unittest.py +++ b/nova/tests/volume_unittest.py @@ -16,7 +16,8 @@ # License for the specific language governing permissions and limitations # under the License. """ -Tests for Volume Code +Tests for Volume Code. + """ import logging @@ -33,7 +34,8 @@ FLAGS = flags.FLAGS class VolumeTestCase(test.TrialTestCase): - """Test Case for volumes""" + """Test Case for volumes.""" + def setUp(self): logging.getLogger().setLevel(logging.DEBUG) super(VolumeTestCase, self).setUp() @@ -44,7 +46,7 @@ class VolumeTestCase(test.TrialTestCase): @staticmethod def _create_volume(size='0'): - """Create a volume object""" + """Create a volume object.""" vol = {} vol['size'] = size vol['user_id'] = 'fake' @@ -56,7 +58,7 @@ class VolumeTestCase(test.TrialTestCase): @defer.inlineCallbacks def test_create_delete_volume(self): - """Test volume can be created and deleted""" + """Test volume can be created and deleted.""" volume_id = self._create_volume() yield self.volume.create_volume(self.context, volume_id) self.assertEqual(volume_id, db.volume_get(context.get_admin_context(), @@ -70,7 +72,7 @@ class VolumeTestCase(test.TrialTestCase): @defer.inlineCallbacks def test_too_big_volume(self): - """Ensure failure if a too large of a volume is requested""" + """Ensure failure if a too large of a volume is requested.""" # FIXME(vish): validation needs to move into the data layer in # volume_create defer.returnValue(True) @@ -83,7 +85,7 @@ class VolumeTestCase(test.TrialTestCase): @defer.inlineCallbacks def test_too_many_volumes(self): - """Ensure that NoMoreBlades is raised when we run out of volumes""" + """Ensure that NoMoreBlades is raised when we run out of volumes.""" vols = [] total_slots = FLAGS.num_shelves * FLAGS.blades_per_shelf for _index in xrange(total_slots): @@ -100,7 +102,7 @@ class VolumeTestCase(test.TrialTestCase): @defer.inlineCallbacks def test_run_attach_detach_volume(self): - """Make sure volume can be attached and detached from instance""" + """Make sure volume can be attached and detached from instance.""" inst = {} inst['image_id'] = 'ami-test' inst['reservation_id'] = 'r-fakeres' @@ -149,12 +151,13 @@ class VolumeTestCase(test.TrialTestCase): @defer.inlineCallbacks def test_concurrent_volumes_get_different_blades(self): - """Ensure multiple concurrent volumes get different blades""" + """Ensure multiple concurrent volumes get different blades.""" + volume_ids = [] shelf_blades = [] def _check(volume_id): - """Make sure blades aren't duplicated""" + """Make sure blades aren't duplicated.""" volume_ids.append(volume_id) admin_context = context.get_admin_context() (shelf_id, blade_id) = db.volume_get_shelf_and_blade(admin_context, diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 3fa29ba37..b99089374 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -15,9 +15,9 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - """ -Drivers for volumes +Drivers for volumes. + """ import logging @@ -39,7 +39,8 @@ flags.DEFINE_string('num_shell_tries', 3, class AOEDriver(object): - """Executes commands relating to AOE volumes""" + """Executes commands relating to AOE volumes.""" + def __init__(self, execute=process.simple_execute, *args, **kwargs): self._execute = execute @@ -63,7 +64,7 @@ class AOEDriver(object): @defer.inlineCallbacks def create_volume(self, volume_name, size): - """Creates a logical volume""" + """Creates a logical volume.""" # NOTE(vish): makes sure that the volume group exists yield self._execute("vgs %s" % FLAGS.volume_group) if int(size) == 0: @@ -77,14 +78,14 @@ class AOEDriver(object): @defer.inlineCallbacks def delete_volume(self, volume_name): - """Deletes a logical volume""" + """Deletes a logical volume.""" yield self._try_execute("sudo lvremove -f %s/%s" % (FLAGS.volume_group, volume_name)) @defer.inlineCallbacks def create_export(self, volume_name, shelf_id, blade_id): - """Creates an export for a logical volume""" + """Creates an export for a logical volume.""" yield self._try_execute( "sudo vblade-persist setup %s %s %s /dev/%s/%s" % (shelf_id, @@ -95,13 +96,13 @@ class AOEDriver(object): @defer.inlineCallbacks def discover_volume(self, _volume_name): - """Discover volume on a remote host""" + """Discover volume on a remote host.""" yield self._execute("sudo aoe-discover") yield self._execute("sudo aoe-stat") @defer.inlineCallbacks def remove_export(self, _volume_name, shelf_id, blade_id): - """Removes an export for a logical volume""" + """Removes an export for a logical volume.""" yield self._try_execute("sudo vblade-persist stop %s %s" % (shelf_id, blade_id)) yield self._try_execute("sudo vblade-persist destroy %s %s" % @@ -109,7 +110,7 @@ class AOEDriver(object): @defer.inlineCallbacks def ensure_exports(self): - """Runs all existing exports""" + """Runs all existing exports.""" # NOTE(vish): The standard _try_execute does not work here # because these methods throw errors if other # volumes on this host are in the process of @@ -125,11 +126,12 @@ class AOEDriver(object): class FakeAOEDriver(AOEDriver): - """Logs calls instead of executing""" + """Logs calls instead of executing.""" + def __init__(self, *args, **kwargs): super(FakeAOEDriver, self).__init__(self.fake_execute) @staticmethod def fake_execute(cmd, *_args, **_kwargs): - """Execute that simply logs the command""" + """Execute that simply logs the command.""" logging.debug("FAKE AOE: %s", cmd) diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 2874459f9..f6e220c5f 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -15,10 +15,31 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - """ -Volume manager manages creating, attaching, detaching, and -destroying persistent storage volumes, ala EBS. +Volume manager manages creating, attaching, detaching, and persistent storage. + +Persistant storage volumes keep their state independent of instances. You can +attach to an instance, terminate the instance, spawn a new instance (even +one from a different image) and re-attach the volume with the same data +intact. + +**Related Flags** + +:volume_topic: What :mod:`rpc` topic to listen to (default: `volume`). +:volume_manager: The module name of a class derived from + :class:`manager.Manager` (default: + :class:`nova.volume.manager.AOEManager`). +:storage_availability_zone: Defaults to `nova`. +:volume_driver: Used by :class:`AOEManager`. Defaults to + :class:`nova.volume.driver.AOEDriver`. +:num_shelves: Number of shelves for AoE (default: 100). +:num_blades: Number of vblades per shelf to allocate AoE storage from + (default: 16). +:volume_group: Name of the group that will contain exported volumes (default: + `nova-volumes`) +:aoe_eth_dev: Device name the volumes will be exported on (default: `eth0`). +:num_shell_tries: Number of times to attempt to run AoE commands (default: 3) + """ import logging @@ -47,15 +68,17 @@ flags.DEFINE_integer('blades_per_shelf', class AOEManager(manager.Manager): - """Manages Ata-Over_Ethernet volumes""" + """Manages Ata-Over_Ethernet volumes.""" + def __init__(self, volume_driver=None, *args, **kwargs): + """Load the driver from the one specified in args, or from flags.""" if not volume_driver: volume_driver = FLAGS.volume_driver self.driver = utils.import_object(volume_driver) super(AOEManager, self).__init__(*args, **kwargs) def _ensure_blades(self, context): - """Ensure that blades have been created in datastore""" + """Ensure that blades have been created in datastore.""" total_blades = FLAGS.num_shelves * FLAGS.blades_per_shelf if self.db.export_device_count(context) >= total_blades: return @@ -66,7 +89,7 @@ class AOEManager(manager.Manager): @defer.inlineCallbacks def create_volume(self, context, volume_id): - """Creates and exports the volume""" + """Creates and exports the volume.""" context = context.elevated() logging.info("volume %s: creating", volume_id) @@ -104,7 +127,7 @@ class AOEManager(manager.Manager): @defer.inlineCallbacks def delete_volume(self, context, volume_id): - """Deletes and unexports volume""" + """Deletes and unexports volume.""" context = context.elevated() volume_ref = self.db.volume_get(context, volume_id) if volume_ref['attach_status'] == "attached": @@ -123,7 +146,7 @@ class AOEManager(manager.Manager): @defer.inlineCallbacks def setup_compute_volume(self, context, volume_id): - """Setup remote volume on compute host + """Setup remote volume on compute host. Returns path to device. """ -- cgit From bf15a6eb3de8c688dc1364959dd3e00d3e26a563 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Sat, 30 Oct 2010 20:05:31 -0400 Subject: Update compute/disk.py docs. --- nova/compute/disk.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) (limited to 'nova') diff --git a/nova/compute/disk.py b/nova/compute/disk.py index e362b4507..0b8568d33 100644 --- a/nova/compute/disk.py +++ b/nova/compute/disk.py @@ -15,10 +15,11 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - """ Utility methods to resize, repartition, and modify disk images. + Includes injection of SSH PGP keys into authorized_keys file. + """ import logging @@ -41,20 +42,23 @@ flags.DEFINE_integer('block_size', 1024 * 1024 * 256, @defer.inlineCallbacks def partition(infile, outfile, local_bytes=0, resize=True, local_type='ext2', execute=None): - """Takes a single partition represented by infile and writes a bootable - drive image into outfile. + """ + Turns a partition (infile) into a bootable drive image (outfile). The first 63 sectors (0-62) of the resulting image is a master boot record. Infile becomes the first primary partition. If local bytes is specified, a second primary partition is created and formatted as ext2. - In the diagram below, dashes represent drive sectors. - +-----+------. . .-------+------. . .------+ - | 0 a| b c|d e| - +-----+------. . .-------+------. . .------+ - | mbr | primary partiton | local partition | - +-----+------. . .-------+------. . .------+ + :: + + In the diagram below, dashes represent drive sectors. + +-----+------. . .-------+------. . .------+ + | 0 a| b c|d e| + +-----+------. . .-------+------. . .------+ + | mbr | primary partiton | local partition | + +-----+------. . .-------+------. . .------+ + """ sector_size = 512 file_size = os.path.getsize(infile) -- cgit From fad337b648ea887bb713aab73335aa4602746b62 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 30 Oct 2010 19:58:15 -0700 Subject: don't check for vgroup in fake mode --- nova/volume/driver.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'nova') diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 1fb0386eb..6b0510704 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -212,6 +212,10 @@ class FakeAOEDriver(AOEDriver): sync_exec=self.fake_execute, *args, **kwargs) + def check_for_setup_error(self): + """Returns an error if prerequisites aren't met""" + pass + @staticmethod def fake_execute(cmd, *_args, **_kwargs): """Execute that simply logs the command""" @@ -319,6 +323,10 @@ class FakeISCSIDriver(ISCSIDriver): sync_exec=self.fake_execute, *args, **kwargs) + def check_for_setup_error(self): + """Returns an error if prerequisites aren't met""" + pass + @staticmethod def fake_execute(cmd, *_args, **_kwargs): """Execute that simply logs the command""" -- cgit From 3bc28df8a1369dd9a717a5986000226c2c1d8c02 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Sat, 30 Oct 2010 20:57:18 -0700 Subject: Change retrieval of security groups from kwargs so they are associated properly and add test to verify --- nova/compute/manager.py | 5 +++-- nova/tests/compute_unittest.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/compute/manager.py b/nova/compute/manager.py index d50607aca..850cded8a 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -69,7 +69,7 @@ class ComputeManager(manager.Manager): def refresh_security_group(self, context, security_group_id, **_kwargs): yield self.driver.refresh_security_group(security_group_id) - def create_instance(self, context, security_groups=[], **kwargs): + def create_instance(self, context, security_groups=None, **kwargs): """Creates the instance in the datastore and returns the new instance as a mapping @@ -88,7 +88,8 @@ class ComputeManager(manager.Manager): inst_id = instance_ref['id'] elevated = context.elevated() - security_groups = kwargs.get('security_groups', []) + if not security_groups: + security_groups = [] for security_group_id in security_groups: self.db.instance_add_security_group(elevated, inst_id, diff --git a/nova/tests/compute_unittest.py b/nova/tests/compute_unittest.py index 01b5651df..71a1a4457 100644 --- a/nova/tests/compute_unittest.py +++ b/nova/tests/compute_unittest.py @@ -66,6 +66,27 @@ class ComputeTestCase(test.TrialTestCase): inst['ami_launch_index'] = 0 return db.instance_create(self.context, inst)['id'] + def test_create_instance_associates_security_groups(self): + """Make sure create_instance associates security groups""" + inst = {} + inst['user_id'] = self.user.id + inst['project_id'] = self.project.id + values = {'name': 'default', + 'description': 'default', + 'user_id': self.user.id, + 'project_id': self.project.id} + group = db.security_group_create(self.context, values) + ref = self.compute.create_instance(self.context, + security_groups=[group['id']], + **inst) + # reload to get groups + instance_ref = db.instance_get(self.context, ref['id']) + try: + self.assertEqual(len(instance_ref['security_groups']), 1) + finally: + db.security_group_destroy(self.context, group['id']) + db.instance_destroy(self.context, instance_ref['id']) + @defer.inlineCallbacks def test_run_terminate(self): """Make sure it is possible to run and terminate instance""" -- cgit From 878eb4d25075f8d78f24ad9f78eb5d43702192ca Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Mon, 1 Nov 2010 16:13:18 -0400 Subject: Virt documentation. --- nova/virt/connection.py | 20 ++++++++++++-------- nova/virt/fake.py | 7 +++++-- nova/virt/libvirt_conn.py | 22 +++++++++++++++++++++- nova/virt/xenapi.py | 12 ++++++++++++ 4 files changed, 50 insertions(+), 11 deletions(-) (limited to 'nova') diff --git a/nova/virt/connection.py b/nova/virt/connection.py index ceb7f1e4b..11f0fa8ce 100644 --- a/nova/virt/connection.py +++ b/nova/virt/connection.py @@ -32,19 +32,23 @@ FLAGS = flags.FLAGS def get_connection(read_only=False): - """Returns an object representing the connection to a virtualization - platform. This could be nova.virt.fake.FakeConnection in test mode, - a connection to KVM or QEMU via libvirt, or a connection to XenServer - or Xen Cloud Platform via XenAPI. + """ + Returns an object representing the connection to a virtualization + platform. + + This could be :mod:`nova.virt.fake.FakeConnection` in test mode, + a connection to KVM, QEMU, or UML via :mod:`libvirt_conn`, or a connection + to XenServer or Xen Cloud Platform via :mod:`xenapi`. Any object returned here must conform to the interface documented by - FakeConnection. + :mod:`FakeConnection`. + + **Related flags** - Related flags - ------------- :connection_type: A string literal that falls through a if/elif structure to determine what virtualization mechanism to use. - Values may be: + Values may be + * fake * libvirt * xenapi diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 66eff4c66..f855523d3 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -18,8 +18,11 @@ # under the License. """ -A fake (in-memory) hypervisor+api. Allows nova testing w/o a hypervisor. -This module also documents the semantics of real hypervisor connections. +A fake (in-memory) hypervisor+api. + +Allows nova testing w/o a hypervisor. This module also documents the +semantics of real hypervisor connections. + """ from twisted.internet import defer diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index e32945fa5..0170fc6d1 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -18,7 +18,27 @@ # under the License. """ -A connection to a hypervisor (e.g. KVM) through libvirt. +A connection to a hypervisor through libvirt. + +Supports KVM, QEMU, UML, and XEN. + +**Related Flags** + +:libvirt_type: Libvirt domain type. Can be kvm, qemu, uml, xen + (default: kvm). +:libvirt_uri: Override for the default libvirt URI (depends on libvirt_type). +:libvirt_xml_template: Libvirt XML Template (QEmu/KVM). +:libvirt_xen_xml_template: Libvirt XML Template (Xen). +:libvirt_uml_xml_template: Libvirt XML Template (User Mode Linux). +:libvirt_rescue_xml_template: XML template for rescue mode (KVM & QEMU). +:libvirt_rescue_xen_xml_template: XML templage for rescue mode (XEN). +:libvirt_rescue_uml_xml_template: XML template for rescue mode (UML). +:rescue_image_id: Rescue ami image (default: ami-rescue). +:rescue_kernel_id: Rescue aki image (default: aki-rescue). +:rescue_ramdisk_id: Rescue ari image (default: ari-rescue). +:injected_network_template: Template file for injected network +:allow_project_net_traffic: Whether to allow in project network traffic + """ import logging diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py index a17e405ab..0f563aa41 100644 --- a/nova/virt/xenapi.py +++ b/nova/virt/xenapi.py @@ -33,6 +33,18 @@ long-running operations. FIXME: get_info currently doesn't conform to these rules, and will block the reactor thread if the VM.get_by_name_label or VM.get_record calls block. + +**Related Flags** + +:xenapi_connection_url: URL for connection to XenServer/Xen Cloud Platform. +:xenapi_connection_username: Username for connection to XenServer/Xen Cloud + Platform (default: root). +:xenapi_connection_password: Password for connection to XenServer/Xen Cloud + Platform. +:xenapi_task_poll_interval: The interval (seconds) used for polling of + remote tasks (Async.VM.start, etc) + (default: 0.5). + """ import logging -- cgit From c8e2341c98ffacfafffbadb7d204f10ff87cf89c Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Mon, 1 Nov 2010 20:33:03 -0400 Subject: API endpoint documentation. --- nova/api/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 8a1d9fe32..27b8199db 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -15,9 +15,15 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - """ Root WSGI middleware for all API controllers. + +**Related Flags** + +:osapi_subdomain: subdomain running the OpenStack API (default: api) +:ec2api_subdomain: subdomain running the EC2 API (default: ec2) +:FAKE_subdomain: set to 'api' or 'ec2', requests default to that endpoint + """ import routes -- cgit From e0f889443f5c0732db28871f350c45e7c8e8d031 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Mon, 1 Nov 2010 21:47:16 -0400 Subject: Add ec2 api docs. --- nova/api/ec2/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 0df4d3710..816314901 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -15,8 +15,10 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +""" +Starting point for routing EC2 requests. -"""Starting point for routing EC2 requests""" +""" import logging import routes -- cgit From 2e2dce7ebf478258f67a9122c6b158ba5e89c1ed Mon Sep 17 00:00:00 2001 From: Eric Day Date: Tue, 2 Nov 2010 11:28:14 -0700 Subject: Added support for OpenStack and EC2 APIs to run on different ports. --- nova/api/__init__.py | 34 +++++++++++++++------------------- nova/wsgi.py | 25 +++++++++++++++++++++---- 2 files changed, 36 insertions(+), 23 deletions(-) (limited to 'nova') diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 8a1d9fe32..707c1623e 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -35,37 +35,31 @@ flags.DEFINE_string('osapi_subdomain', 'api', 'subdomain running the OpenStack API') flags.DEFINE_string('ec2api_subdomain', 'ec2', 'subdomain running the EC2 API') -flags.DEFINE_string('FAKE_subdomain', None, - 'set to api or ec2 to fake the subdomain of the host ' - 'for testing') FLAGS = flags.FLAGS class API(wsgi.Router): """Routes top-level requests to the appropriate controller.""" - def __init__(self): - osapidomain = {'sub_domain': [FLAGS.osapi_subdomain]} - ec2domain = {'sub_domain': [FLAGS.ec2api_subdomain]} - # If someone wants to pretend they're hitting the OSAPI subdomain - # on their local box, they can set FAKE_subdomain to 'api', which - # removes subdomain restrictions from the OpenStack API routes below. - if FLAGS.FAKE_subdomain == 'api': - osapidomain = {} - elif FLAGS.FAKE_subdomain == 'ec2': - ec2domain = {} + def __init__(self, default_api): + osapi_subdomain = {'sub_domain': [FLAGS.osapi_subdomain]} + ec2api_subdomain = {'sub_domain': [FLAGS.ec2api_subdomain]} + if default_api == 'os': + osapi_subdomain = {} + elif default_api == 'ec2': + ec2api_subdomain = {} mapper = routes.Mapper() mapper.sub_domains = True + mapper.connect("/", controller=self.osapi_versions, - conditions=osapidomain) + conditions=osapi_subdomain) mapper.connect("/v1.0/{path_info:.*}", controller=openstack.API(), - conditions=osapidomain) + conditions=osapi_subdomain) mapper.connect("/", controller=self.ec2api_versions, - conditions=ec2domain) + conditions=ec2api_subdomain) mapper.connect("/services/{path_info:.*}", controller=ec2.API(), - conditions=ec2domain) - mapper.connect("/cloudpipe/{path_info:.*}", controller=cloudpipe.API()) + conditions=ec2api_subdomain) mrh = metadatarequesthandler.MetadataRequestHandler() for s in ['/latest', '/2009-04-04', @@ -78,7 +72,9 @@ class API(wsgi.Router): '/2007-01-19', '/1.0']: mapper.connect('%s/{path_info:.*}' % s, controller=mrh, - conditions=ec2domain) + conditions=ec2api_subdomain) + + mapper.connect("/cloudpipe/{path_info:.*}", controller=cloudpipe.API()) super(API, self).__init__(mapper) @webob.dec.wsgify diff --git a/nova/wsgi.py b/nova/wsgi.py index eb305a3d3..b04b487ea 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -39,10 +39,27 @@ import webob.exc logging.getLogger("routes.middleware").addHandler(logging.StreamHandler()) -def run_server(application, port): - """Run a WSGI server with the given application.""" - sock = eventlet.listen(('0.0.0.0', port)) - eventlet.wsgi.server(sock, application) +class Server(object): + """Server class to manage multiple WSGI sockets and applications.""" + + def __init__(self, threads=1000): + self.pool = eventlet.GreenPool(threads) + + def start(self, application, port, host='0.0.0.0', backlog=128): + """Run a WSGI server with the given application.""" + socket = eventlet.listen((host, port), backlog=backlog) + self.pool.spawn_n(self._run, application, socket) + + def wait(self): + """Wait until all servers have completed running.""" + try: + self.pool.waitall() + except KeyboardInterrupt: + pass + + def _run(self, application, socket): + """Start a WSGI server in a new green thread.""" + eventlet.wsgi.server(socket, application, custom_pool=self.pool) class Application(object): -- cgit From 785d60c9492a8d4583eb27b214abefda6c1fbcfc Mon Sep 17 00:00:00 2001 From: Eric Day Date: Tue, 2 Nov 2010 12:02:42 -0700 Subject: Fixed tests to work with new default API argument. --- nova/tests/api/__init__.py | 2 +- nova/tests/api/openstack/fakes.py | 4 ---- nova/tests/api/openstack/test_auth.py | 18 +++++++++--------- nova/tests/api/openstack/test_flavors.py | 2 +- nova/tests/api/openstack/test_images.py | 4 ++-- nova/tests/api/openstack/test_servers.py | 28 ++++++++++++++-------------- nova/tests/api_unittest.py | 6 +----- 7 files changed, 28 insertions(+), 36 deletions(-) (limited to 'nova') diff --git a/nova/tests/api/__init__.py b/nova/tests/api/__init__.py index 46f09e906..9caa8c9d0 100644 --- a/nova/tests/api/__init__.py +++ b/nova/tests/api/__init__.py @@ -42,7 +42,7 @@ class Test(unittest.TestCase): environ_keys = {'HTTP_HOST': '%s.example.com' % subdomain} environ_keys.update(kwargs) req = webob.Request.blank(url, environ_keys) - return req.get_response(api.API()) + return req.get_response(api.API('ec2')) def test_openstack(self): self.stubs.Set(api.openstack, 'API', APIStub) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 1b8c18974..52b392601 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -34,9 +34,6 @@ from nova.tests import fake_flags from nova.wsgi import Router -FLAGS = flags.FLAGS - - class Context(object): pass @@ -108,7 +105,6 @@ def stub_out_networking(stubs): def get_my_ip(): return '127.0.0.1' stubs.Set(nova.utils, 'get_my_ip', get_my_ip) - FLAGS.FAKE_subdomain = 'api' def stub_out_glance(stubs, initial_fixtures=[]): diff --git a/nova/tests/api/openstack/test_auth.py b/nova/tests/api/openstack/test_auth.py index b63da187f..29f4b8874 100644 --- a/nova/tests/api/openstack/test_auth.py +++ b/nova/tests/api/openstack/test_auth.py @@ -51,7 +51,7 @@ class Test(unittest.TestCase): req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'herp' req.headers['X-Auth-Key'] = 'derp' - result = req.get_response(nova.api.API()) + result = req.get_response(nova.api.API('os')) self.assertEqual(result.status, '204 No Content') self.assertEqual(len(result.headers['X-Auth-Token']), 40) self.assertEqual(result.headers['X-CDN-Management-Url'], @@ -65,7 +65,7 @@ class Test(unittest.TestCase): req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'herp' req.headers['X-Auth-Key'] = 'derp' - result = req.get_response(nova.api.API()) + result = req.get_response(nova.api.API('os')) self.assertEqual(result.status, '204 No Content') self.assertEqual(len(result.headers['X-Auth-Token']), 40) self.assertEqual(result.headers['X-Server-Management-Url'], @@ -79,7 +79,7 @@ class Test(unittest.TestCase): fakes.FakeRouter) req = webob.Request.blank('/v1.0/fake') req.headers['X-Auth-Token'] = token - result = req.get_response(nova.api.API()) + result = req.get_response(nova.api.API('os')) self.assertEqual(result.status, '200 OK') self.assertEqual(result.headers['X-Test-Success'], 'True') @@ -103,7 +103,7 @@ class Test(unittest.TestCase): req = webob.Request.blank('/v1.0/') req.headers['X-Auth-Token'] = 'bacon' - result = req.get_response(nova.api.API()) + result = req.get_response(nova.api.API('os')) self.assertEqual(result.status, '401 Unauthorized') self.assertEqual(self.destroy_called, True) @@ -111,18 +111,18 @@ class Test(unittest.TestCase): req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'herp' req.headers['X-Auth-Key'] = 'derp' - result = req.get_response(nova.api.API()) + result = req.get_response(nova.api.API('os')) self.assertEqual(result.status, '401 Unauthorized') def test_no_user(self): req = webob.Request.blank('/v1.0/') - result = req.get_response(nova.api.API()) + result = req.get_response(nova.api.API('os')) self.assertEqual(result.status, '401 Unauthorized') def test_bad_token(self): req = webob.Request.blank('/v1.0/') req.headers['X-Auth-Token'] = 'baconbaconbacon' - result = req.get_response(nova.api.API()) + result = req.get_response(nova.api.API('os')) self.assertEqual(result.status, '401 Unauthorized') @@ -146,7 +146,7 @@ class TestLimiter(unittest.TestCase): req = webob.Request.blank('/v1.0/') req.headers['X-Auth-User'] = 'herp' req.headers['X-Auth-Key'] = 'derp' - result = req.get_response(nova.api.API()) + result = req.get_response(nova.api.API('os')) self.assertEqual(len(result.headers['X-Auth-Token']), 40) token = result.headers['X-Auth-Token'] @@ -155,7 +155,7 @@ class TestLimiter(unittest.TestCase): req = webob.Request.blank('/v1.0/fake') req.method = 'POST' req.headers['X-Auth-Token'] = token - result = req.get_response(nova.api.API()) + result = req.get_response(nova.api.API('os')) self.assertEqual(result.status, '200 OK') self.assertEqual(result.headers['X-Test-Success'], 'True') diff --git a/nova/tests/api/openstack/test_flavors.py b/nova/tests/api/openstack/test_flavors.py index 8dd4d1f29..41018afdf 100644 --- a/nova/tests/api/openstack/test_flavors.py +++ b/nova/tests/api/openstack/test_flavors.py @@ -39,7 +39,7 @@ class FlavorsTest(unittest.TestCase): def test_get_flavor_list(self): req = webob.Request.blank('/v1.0/flavors') - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) def test_get_flavor_by_id(self): pass diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index d61c3a99b..0f3941c29 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -203,7 +203,7 @@ class ImageControllerWithGlanceServiceTest(unittest.TestCase): def test_get_image_index(self): req = webob.Request.blank('/v1.0/images') - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) res_dict = json.loads(res.body) fixture_index = [dict(id=f['id'], name=f['name']) for f @@ -215,7 +215,7 @@ class ImageControllerWithGlanceServiceTest(unittest.TestCase): def test_get_image_details(self): req = webob.Request.blank('/v1.0/images/detail') - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) res_dict = json.loads(res.body) for image in res_dict['images']: diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 785fb6f3a..8cfc6c45a 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -69,14 +69,14 @@ class ServersTest(unittest.TestCase): def test_get_server_by_id(self): req = webob.Request.blank('/v1.0/servers/1') - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) res_dict = json.loads(res.body) self.assertEqual(res_dict['server']['id'], 1) self.assertEqual(res_dict['server']['name'], 'server1') def test_get_server_list(self): req = webob.Request.blank('/v1.0/servers') - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) res_dict = json.loads(res.body) i = 0 @@ -119,14 +119,14 @@ class ServersTest(unittest.TestCase): req.method = 'POST' req.body = json.dumps(body) - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) self.assertEqual(res.status_int, 200) def test_update_no_body(self): req = webob.Request.blank('/v1.0/servers/1') req.method = 'PUT' - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) self.assertEqual(res.status_int, 422) def test_update_bad_params(self): @@ -145,7 +145,7 @@ class ServersTest(unittest.TestCase): req = webob.Request.blank('/v1.0/servers/1') req.method = 'PUT' req.body = self.body - req.get_response(nova.api.API()) + req.get_response(nova.api.API('os')) def test_update_server(self): inst_dict = dict(name='server_test', adminPass='bacon') @@ -161,28 +161,28 @@ class ServersTest(unittest.TestCase): req = webob.Request.blank('/v1.0/servers/1') req.method = 'PUT' req.body = self.body - req.get_response(nova.api.API()) + req.get_response(nova.api.API('os')) def test_create_backup_schedules(self): req = webob.Request.blank('/v1.0/servers/1/backup_schedules') req.method = 'POST' - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) self.assertEqual(res.status, '404 Not Found') def test_delete_backup_schedules(self): req = webob.Request.blank('/v1.0/servers/1/backup_schedules') req.method = 'DELETE' - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) self.assertEqual(res.status, '404 Not Found') def test_get_server_backup_schedules(self): req = webob.Request.blank('/v1.0/servers/1/backup_schedules') - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) self.assertEqual(res.status, '404 Not Found') def test_get_all_server_details(self): req = webob.Request.blank('/v1.0/servers/detail') - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) res_dict = json.loads(res.body) i = 0 @@ -200,7 +200,7 @@ class ServersTest(unittest.TestCase): req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) def test_server_rebuild(self): body = dict(server=dict( @@ -210,7 +210,7 @@ class ServersTest(unittest.TestCase): req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) def test_server_resize(self): body = dict(server=dict( @@ -220,7 +220,7 @@ class ServersTest(unittest.TestCase): req.method = 'POST' req.content_type = 'application/json' req.body = json.dumps(body) - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) def test_delete_server_instance(self): req = webob.Request.blank('/v1.0/servers/1') @@ -234,7 +234,7 @@ class ServersTest(unittest.TestCase): self.stubs.Set(nova.db.api, 'instance_destroy', instance_destroy_mock) - res = req.get_response(nova.api.API()) + res = req.get_response(nova.api.API('os')) self.assertEqual(res.status, '202 Accepted') self.assertEqual(self.server_delete_called, True) diff --git a/nova/tests/api_unittest.py b/nova/tests/api_unittest.py index 0a81c575b..33d4cb294 100644 --- a/nova/tests/api_unittest.py +++ b/nova/tests/api_unittest.py @@ -34,10 +34,6 @@ from nova.api.ec2 import apirequest from nova.auth import manager -FLAGS = flags.FLAGS -FLAGS.FAKE_subdomain = 'ec2' - - class FakeHttplibSocket(object): """a fake socket implementation for httplib.HTTPResponse, trivial""" def __init__(self, response_string): @@ -109,7 +105,7 @@ class ApiEc2TestCase(test.TrialTestCase): self.host = '127.0.0.1' - self.app = api.API() + self.app = api.API('ec2') def expect_http(self, host=None, is_secure=False): """Returns a new EC2 connection""" -- cgit From 67d2d35e944b10f6f1e2e6eeb0a8b33496d4d39b Mon Sep 17 00:00:00 2001 From: Eric Day Date: Tue, 2 Nov 2010 13:51:09 -0700 Subject: Fixed --help display for non-twisted bin/* commands. --- nova/flags.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'nova') diff --git a/nova/flags.py b/nova/flags.py index f3b0384ad..e51f286ad 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -138,6 +138,8 @@ class FlagValues(gflags.FlagValues): FLAGS = FlagValues() +gflags.FLAGS = FLAGS +gflags.DEFINE_flag(gflags.HelpFlag(), FLAGS) def _wrapper(func): -- cgit From e493e324eb9a9fe31e72551b34bab768b507bc1d Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 2 Nov 2010 18:05:47 -0400 Subject: Document final undocumented python modules. --- nova/image/service.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) (limited to 'nova') diff --git a/nova/image/service.py b/nova/image/service.py index 37cadddcc..52ddd4e0f 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -45,13 +45,9 @@ class BaseImageService(object): Returns a sequence of mappings of id and name information about images. - :retval a sequence of mappings with the following signature: - - [ - {'id': opaque id of image, - 'name': name of image - }, ... - ] + :rtype: array + :retval: a sequence of mappings with the following signature + {'id': opaque id of image, 'name': name of image} """ raise NotImplementedError @@ -60,19 +56,17 @@ class BaseImageService(object): """ Returns a sequence of mappings of detailed information about images. - :retval a sequence of mappings with the following signature: - - [ - {'id': opaque id of image, - 'name': name of image, - 'created_at': creation timestamp, - 'updated_at': modification timestamp, - 'deleted_at': deletion timestamp or None, - 'deleted': boolean indicating if image has been deleted, - 'status': string description of image status, - 'is_public': boolean indicating if image is public - }, ... - ] + :rtype: array + :retval: a sequence of mappings with the following signature + {'id': opaque id of image, + 'name': name of image, + 'created_at': creation timestamp, + 'updated_at': modification timestamp, + 'deleted_at': deletion timestamp or None, + 'deleted': boolean indicating if image has been deleted, + 'status': string description of image status, + 'is_public': boolean indicating if image is public + } If the service does not implement a method that provides a detailed set of information about images, then the method should raise -- cgit From 583d1b1c4d039f1f9751c8a2cc0cf59bb77551e0 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Tue, 2 Nov 2010 20:31:17 -0400 Subject: Fixes after trunk merge. --- nova/tests/fake_flags.py | 1 + 1 file changed, 1 insertion(+) (limited to 'nova') diff --git a/nova/tests/fake_flags.py b/nova/tests/fake_flags.py index 4bbef8832..bc377f0da 100644 --- a/nova/tests/fake_flags.py +++ b/nova/tests/fake_flags.py @@ -24,6 +24,7 @@ flags.DECLARE('volume_driver', 'nova.volume.manager') FLAGS.volume_driver = 'nova.volume.driver.FakeAOEDriver' FLAGS.connection_type = 'fake' FLAGS.fake_rabbit = True +flags.DECLARE('auth_driver', 'nova.auth.manager') FLAGS.auth_driver = 'nova.auth.dbdriver.DbDriver' flags.DECLARE('network_size', 'nova.network.manager') flags.DECLARE('num_networks', 'nova.network.manager') -- cgit From 2a16ae2f8479e469e413dfd036bac805163f3ac2 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Wed, 3 Nov 2010 15:05:10 +0100 Subject: Change socket type in nova.utils.get_my_ip() to SOCK_DGRAM. This way, we don't actually have to set up a connection. Also, change the destination host to an IP (chose one of Google's DNS's at random) rather than a hostname, so we avoid doing a DNS lookup. --- nova/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/utils.py b/nova/utils.py index bc495a691..e7892a212 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -154,8 +154,8 @@ def get_my_ip(): if getattr(FLAGS, 'fake_tests', None): return '127.0.0.1' try: - csock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - csock.connect(('www.google.com', 80)) + csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + csock.connect(('8.8.8.8', 80)) (addr, port) = csock.getsockname() csock.close() return addr -- cgit From 2cbef8ffd80546f1dcd850322621b04395591d69 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Wed, 3 Nov 2010 14:30:13 -0400 Subject: Doc cleanups. --- nova/network/manager.py | 116 +++++++++++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 46 deletions(-) (limited to 'nova') diff --git a/nova/network/manager.py b/nova/network/manager.py index 8a20cb491..b033bb0a4 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -17,7 +17,30 @@ # under the License. """ -Network Hosts are responsible for allocating ips and setting up network +Network Hosts are responsible for allocating ips and setting up network. + +There are multiple backend drivers that handle specific types of networking +topologies. All of the network commands are issued to a subclass of +:class:`NetworkManager`. + +**Related Flags** + +:network_driver: Driver to use for network creation +:flat_network_bridge: Bridge device for simple network instances +:flat_network_dns: Dns for simple network +:flat_network_dhcp_start: Dhcp start for FlatDhcp +:vlan_start: First VLAN for private networks +:vpn_ip: Public IP for the cloudpipe VPN servers +:vpn_start: First Vpn port for private networks +:cnt_vpn_clients: Number of addresses reserved for vpn clients +:network_size: Number of addresses in each private subnet +:floating_range: Floating IP address block +:fixed_range: Fixed IP address block +:date_dhcp_on_disassociate: Whether to update dhcp when fixed_ip + is disassociated +:fixed_ip_disassociate_timeout: Seconds after which a deallocated ip + is disassociated + """ import datetime @@ -63,15 +86,16 @@ flags.DEFINE_integer('fixed_ip_disassociate_timeout', 600, class AddressAlreadyAllocated(exception.Error): - """Address was already allocated""" + """Address was already allocated.""" pass class NetworkManager(manager.Manager): - """Implements common network manager functionality + """Implements common network manager functionality. - This class must be subclassed. + This class must be subclassed to support specific topologies. """ + def __init__(self, network_driver=None, *args, **kwargs): if not network_driver: network_driver = FLAGS.network_driver @@ -86,7 +110,7 @@ class NetworkManager(manager.Manager): self._on_set_network_host(ctxt, network['id']) def set_network_host(self, context, network_id): - """Safely sets the host of the network""" + """Safely sets the host of the network.""" logging.debug("setting network host") host = self.db.network_set_host(context, network_id, @@ -95,34 +119,34 @@ class NetworkManager(manager.Manager): return host def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): - """Gets a fixed ip from the pool""" + """Gets a fixed ip from the pool.""" raise NotImplementedError() def deallocate_fixed_ip(self, context, address, *args, **kwargs): - """Returns a fixed ip to the pool""" + """Returns a fixed ip to the pool.""" raise NotImplementedError() def setup_fixed_ip(self, context, address): - """Sets up rules for fixed ip""" + """Sets up rules for fixed ip.""" raise NotImplementedError() def _on_set_network_host(self, context, network_id): - """Called when this host becomes the host for a network""" + """Called when this host becomes the host for a network.""" raise NotImplementedError() def setup_compute_network(self, context, instance_id): - """Sets up matching network for compute hosts""" + """Sets up matching network for compute hosts.""" raise NotImplementedError() def allocate_floating_ip(self, context, project_id): - """Gets an floating ip from the pool""" + """Gets an floating ip from the pool.""" # TODO(vish): add floating ips through manage command return self.db.floating_ip_allocate_address(context, self.host, project_id) def associate_floating_ip(self, context, floating_address, fixed_address): - """Associates an floating ip to a fixed ip""" + """Associates an floating ip to a fixed ip.""" self.db.floating_ip_fixed_ip_associate(context, floating_address, fixed_address) @@ -130,18 +154,18 @@ class NetworkManager(manager.Manager): self.driver.ensure_floating_forward(floating_address, fixed_address) def disassociate_floating_ip(self, context, floating_address): - """Disassociates a floating ip""" + """Disassociates a floating ip.""" fixed_address = self.db.floating_ip_disassociate(context, floating_address) self.driver.unbind_floating_ip(floating_address) self.driver.remove_floating_forward(floating_address, fixed_address) def deallocate_floating_ip(self, context, floating_address): - """Returns an floating ip to the pool""" + """Returns an floating ip to the pool.""" self.db.floating_ip_deallocate(context, floating_address) def lease_fixed_ip(self, context, mac, address): - """Called by dhcp-bridge when ip is leased""" + """Called by dhcp-bridge when ip is leased.""" logging.debug("Leasing IP %s", address) fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) instance_ref = fixed_ip_ref['instance'] @@ -158,7 +182,7 @@ class NetworkManager(manager.Manager): logging.warn("IP %s leased that was already deallocated", address) def release_fixed_ip(self, context, mac, address): - """Called by dhcp-bridge when ip is released""" + """Called by dhcp-bridge when ip is released.""" logging.debug("Releasing IP %s", address) fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) instance_ref = fixed_ip_ref['instance'] @@ -183,26 +207,26 @@ class NetworkManager(manager.Manager): self.driver.update_dhcp(context, network_ref['id']) def get_network(self, context): - """Get the network for the current context""" + """Get the network for the current context.""" raise NotImplementedError() def create_networks(self, context, num_networks, network_size, *args, **kwargs): - """Create networks based on parameters""" + """Create networks based on parameters.""" raise NotImplementedError() @property def _bottom_reserved_ips(self): # pylint: disable-msg=R0201 - """Number of reserved ips at the bottom of the range""" + """Number of reserved ips at the bottom of the range.""" return 2 # network, gateway @property def _top_reserved_ips(self): # pylint: disable-msg=R0201 - """Number of reserved ips at the top of the range""" + """Number of reserved ips at the top of the range.""" return 1 # broadcast def _create_fixed_ips(self, context, network_id): - """Create all fixed ips for network""" + """Create all fixed ips for network.""" network_ref = self.db.network_get(context, network_id) # NOTE(vish): Should these be properties of the network as opposed # to properties of the manager class? @@ -222,10 +246,10 @@ class NetworkManager(manager.Manager): class FlatManager(NetworkManager): - """Basic network where no vlans are used""" + """Basic network where no vlans are used.""" def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): - """Gets a fixed ip from the pool""" + """Gets a fixed ip from the pool.""" # TODO(vish): when this is called by compute, we can associate compute # with a network, or a cluster of computes with a network # and use that network here with a method like @@ -239,21 +263,21 @@ class FlatManager(NetworkManager): return address def deallocate_fixed_ip(self, context, address, *args, **kwargs): - """Returns a fixed ip to the pool""" + """Returns a fixed ip to the pool.""" self.db.fixed_ip_update(context, address, {'allocated': False}) self.db.fixed_ip_disassociate(context.elevated(), address) def setup_compute_network(self, context, instance_id): - """Network is created manually""" + """Network is created manually.""" pass def setup_fixed_ip(self, context, address): - """Currently no setup""" + """Currently no setup.""" pass def create_networks(self, context, cidr, num_networks, network_size, *args, **kwargs): - """Create networks based on parameters""" + """Create networks based on parameters.""" fixed_net = IPy.IP(cidr) for index in range(num_networks): start = index * network_size @@ -271,7 +295,7 @@ class FlatManager(NetworkManager): self._create_fixed_ips(context, network_ref['id']) def get_network(self, context): - """Get the network for the current context""" + """Get the network for the current context.""" # NOTE(vish): To support mutilple network hosts, This could randomly # select from multiple networks instead of just # returning the one. It could also potentially be done @@ -280,7 +304,7 @@ class FlatManager(NetworkManager): FLAGS.flat_network_bridge) def _on_set_network_host(self, context, network_id): - """Called when this host becomes the host for a network""" + """Called when this host becomes the host for a network.""" net = {} net['injected'] = True net['bridge'] = FLAGS.flat_network_bridge @@ -289,19 +313,19 @@ class FlatManager(NetworkManager): class FlatDHCPManager(NetworkManager): - """Flat networking with dhcp""" + """Flat networking with dhcp.""" def setup_fixed_ip(self, context, address): - """Setup dhcp for this network""" + """Setup dhcp for this network.""" network_ref = db.fixed_ip_get_by_address(context, address) self.driver.update_dhcp(context, network_ref['id']) def deallocate_fixed_ip(self, context, address, *args, **kwargs): - """Returns a fixed ip to the pool""" + """Returns a fixed ip to the pool.""" self.db.fixed_ip_update(context, address, {'allocated': False}) def _on_set_network_host(self, context, network_id): - """Called when this host becomes the host for a project""" + """Called when this host becomes the host for a project.""" super(FlatDHCPManager, self)._on_set_network_host(context, network_id) network_ref = self.db.network_get(context, network_id) self.db.network_update(context, @@ -313,11 +337,11 @@ class FlatDHCPManager(NetworkManager): class VlanManager(NetworkManager): - """Vlan network with dhcp""" + """Vlan network with dhcp.""" @defer.inlineCallbacks def periodic_tasks(self, context=None): - """Tasks to be run at a periodic interval""" + """Tasks to be run at a periodic interval.""" yield super(VlanManager, self).periodic_tasks(context) now = datetime.datetime.utcnow() timeout = FLAGS.fixed_ip_disassociate_timeout @@ -330,13 +354,13 @@ class VlanManager(NetworkManager): def init_host(self): """Do any initialization that needs to be run if this is a - standalone service. + standalone service. """ super(VlanManager, self).init_host() self.driver.init_host() def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): - """Gets a fixed ip from the pool""" + """Gets a fixed ip from the pool.""" # TODO(vish): This should probably be getting project_id from # the instance, but it is another trip to the db. # Perhaps this method should take an instance_ref. @@ -356,11 +380,11 @@ class VlanManager(NetworkManager): return address def deallocate_fixed_ip(self, context, address, *args, **kwargs): - """Returns a fixed ip to the pool""" + """Returns a fixed ip to the pool.""" self.db.fixed_ip_update(context, address, {'allocated': False}) def setup_fixed_ip(self, context, address): - """Sets forwarding rules and dhcp for fixed ip""" + """Sets forwarding rules and dhcp for fixed ip.""" fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) network_ref = self.db.fixed_ip_get_network(context, address) if self.db.instance_is_vpn(context, fixed_ip_ref['instance_id']): @@ -370,19 +394,19 @@ class VlanManager(NetworkManager): self.driver.update_dhcp(context, network_ref['id']) def setup_compute_network(self, context, instance_id): - """Sets up matching network for compute hosts""" + """Sets up matching network for compute hosts.""" network_ref = db.network_get_by_instance(context, instance_id) self.driver.ensure_vlan_bridge(network_ref['vlan'], network_ref['bridge']) def restart_nets(self): - """Ensure the network for each user is enabled""" + """Ensure the network for each user is enabled.""" # TODO(vish): Implement this pass def create_networks(self, context, cidr, num_networks, network_size, vlan_start, vpn_start): - """Create networks based on parameters""" + """Create networks based on parameters.""" fixed_net = IPy.IP(cidr) for index in range(num_networks): vlan = vlan_start + index @@ -407,12 +431,12 @@ class VlanManager(NetworkManager): self._create_fixed_ips(context, network_ref['id']) def get_network(self, context): - """Get the network for the current context""" + """Get the network for the current context.""" return self.db.project_get_network(context.elevated(), context.project_id) def _on_set_network_host(self, context, network_id): - """Called when this host becomes the host for a network""" + """Called when this host becomes the host for a network.""" network_ref = self.db.network_get(context, network_id) net = {} net['vpn_public_address'] = FLAGS.vpn_ip @@ -424,11 +448,11 @@ class VlanManager(NetworkManager): @property def _bottom_reserved_ips(self): - """Number of reserved ips at the bottom of the range""" + """Number of reserved ips at the bottom of the range.""" return super(VlanManager, self)._bottom_reserved_ips + 1 # vpn server @property def _top_reserved_ips(self): - """Number of reserved ips at the top of the range""" + """Number of reserved ips at the top of the range.""" parent_reserved = super(VlanManager, self)._top_reserved_ips return parent_reserved + FLAGS.cnt_vpn_clients -- cgit From 179d980dffc03e1ee0449954eed40b1d5489f6e0 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 3 Nov 2010 14:59:35 -0700 Subject: make sure context keys are not unicode so they can be passed as kwargs --- nova/rpc.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'nova') diff --git a/nova/rpc.py b/nova/rpc.py index 895820cd0..05eaa0f99 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -262,6 +262,9 @@ def _unpack_context(msg): """Unpack context from msg.""" context_dict = {} for key in list(msg.keys()): + # NOTE(vish): Some versions of python don't like unicode keys + # in kwargs. + key = str(key) if key.startswith('_context_'): value = msg.pop(key) context_dict[key[9:]] = value -- cgit From 817690b03f2e498fb08eba3ca455719229f24640 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 3 Nov 2010 15:06:00 -0700 Subject: pep8 whitespace and line length fixes --- nova/adminclient.py | 4 ++-- nova/compute/manager.py | 3 ++- nova/db/api.py | 4 ++-- nova/db/sqlalchemy/models.py | 4 ++-- nova/volume/driver.py | 4 ++-- 5 files changed, 10 insertions(+), 9 deletions(-) (limited to 'nova') diff --git a/nova/adminclient.py b/nova/adminclient.py index 0227cddd7..af55197fc 100644 --- a/nova/adminclient.py +++ b/nova/adminclient.py @@ -273,10 +273,10 @@ class NovaAdminClient(object): def get_user_roles(self, user, project=None): """Returns a list of roles for the given user. - + Omitting project will return any global roles that the user has. Specifying project will return only project specific roles. - + """ params = {'User': user} if project: diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 3aed1e5a5..890d79fba 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -22,7 +22,8 @@ Handles all processes relating to instances (guest vms). The :py:class:`ComputeManager` class is a :py:class:`nova.manager.Manager` that handles RPC calls relating to creating instances. It is responsible for building a disk image, launching it via the underlying virtualization driver, -responding to calls to check it state, attaching persistent as well as termination. +responding to calls to check it state, attaching persistent as well as +termination. **Related Flags** diff --git a/nova/db/api.py b/nova/db/api.py index 6f1043d8f..8f9dc2443 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -395,7 +395,7 @@ def network_create_safe(context, values): The network is only returned if the create succeeds. If the create violates constraints because the network already exists, no exception is raised. - + """ return IMPL.network_create_safe(context, values) @@ -499,7 +499,7 @@ def export_device_create_safe(context, values): The device is not returned. If the create violates the unique constraints because the shelf_id and blade_id already exist, no exception is raised. - + """ return IMPL.export_device_create_safe(context, values) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index bc9a7480e..01b5cf350 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -411,10 +411,10 @@ class Network(BASE, NovaBase): class AuthToken(BASE, NovaBase): """Represents an authorization token for all API transactions. - + Fields are a string representing the actual token and a user id for mapping to the actual user - + """ __tablename__ = 'auth_tokens' token_hash = Column(String(255), primary_key=True) diff --git a/nova/volume/driver.py b/nova/volume/driver.py index 89e5c8d57..156aad2a0 100644 --- a/nova/volume/driver.py +++ b/nova/volume/driver.py @@ -114,7 +114,7 @@ class VolumeDriver(object): escaped_name)) def ensure_export(self, context, volume): - """Safely and synchronously recreates an export for a logical volume.""" + """Synchronously recreates an export for a logical volume.""" raise NotImplementedError() @defer.inlineCallbacks @@ -228,7 +228,7 @@ class ISCSIDriver(VolumeDriver): """Executes commands relating to ISCSI volumes.""" def ensure_export(self, context, volume): - """Safely and synchronously recreates an export for a logical volume.""" + """Synchronously recreates an export for a logical volume.""" iscsi_target = self.db.volume_get_iscsi_target_num(context, volume['id']) iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name']) -- cgit From 23463610cb180253697b500f11f01e686b20c1e8 Mon Sep 17 00:00:00 2001 From: Eric Day Date: Wed, 3 Nov 2010 15:50:24 -0700 Subject: Fix for bug #640400, enables the exclusive flag on the temporary queues. --- nova/rpc.py | 1 + 1 file changed, 1 insertion(+) (limited to 'nova') diff --git a/nova/rpc.py b/nova/rpc.py index 895820cd0..14fe010ac 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -206,6 +206,7 @@ class DirectConsumer(Consumer): self.routing_key = msg_id self.exchange = msg_id self.auto_delete = True + self.exclusive = True super(DirectConsumer, self).__init__(connection=connection) -- cgit From a8a61d61db0b00e0b397c807ac8ca89e39a26c5b Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Fri, 5 Nov 2010 19:52:12 -0400 Subject: Fix docstrings for wsigfied methods. --- nova/api/__init__.py | 3 +++ nova/utils.py | 8 ++++++++ 2 files changed, 11 insertions(+) (limited to 'nova') diff --git a/nova/api/__init__.py b/nova/api/__init__.py index f693225d9..1dabd3d21 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -30,6 +30,7 @@ import routes import webob.dec from nova import flags +from nova import utils from nova import wsgi from nova.api import cloudpipe from nova.api import ec2 @@ -83,6 +84,7 @@ class API(wsgi.Router): mapper.connect("/cloudpipe/{path_info:.*}", controller=cloudpipe.API()) super(API, self).__init__(mapper) + @utils.fix_wsgify_docstr @webob.dec.wsgify def osapi_versions(self, req): """Respond to a request for all OpenStack API versions.""" @@ -94,6 +96,7 @@ class API(wsgi.Router): "attributes": dict(version=["status", "id"])}} return wsgi.Serializer(req.environ, metadata).to_content_type(response) + @utils.fix_wsgify_docstr @webob.dec.wsgify def ec2api_versions(self, req): """Respond to a request for all EC2 versions.""" diff --git a/nova/utils.py b/nova/utils.py index e7892a212..d7ebe5b4c 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -21,6 +21,7 @@ System-level utilities and helper functions. """ import datetime +import functools import inspect import logging import os @@ -232,3 +233,10 @@ def utf8(value): return value.encode("utf-8") assert isinstance(value, str) return value + +def fix_wsgify_docstr(wsgified_func): + """A decorator to re-assign docstrings that webob.dec.wsgify clobbers.""" + @functools.wraps(wsgified_func.func) + def _f(*args, **kwargs): + wsgified_func(*args, **kwargs) + return _f -- cgit From 7ca2d7593e7faf36bab9d6cb1c8b0ba16494679f Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Mon, 8 Nov 2010 02:54:28 +0900 Subject: Normalization of Dev reference docs. --- nova/manager.py | 34 +++++++++++++++++++++++++++++++++- nova/service.py | 6 +++++- 2 files changed, 38 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/manager.py b/nova/manager.py index 4244b2db4..a6efb8732 100644 --- a/nova/manager.py +++ b/nova/manager.py @@ -15,8 +15,40 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + """ -Base class for managers of different parts of the system +Managers are responsible for a certain aspect of the sytem. It is a logical +grouping of code relating to a portion of the system. In general other +components should be using the manager to make changes to the components that +it is responsible for. + +For example, other components that need to deal with volumes in some way, +should do so by calling methods on the VolumeManager instead of directly +changing fields in the database. This allows us to keep all of the code +relating to volumes in the same place. + +We have adopted a basic strategy of Smart managers and dumb data, which means +rather than attaching methods to data objects, components should call manager +methods that act on the data. + +Methods on managers that can be executed locally should be called directly. If +a particular method must execute on a remote host, this should be done via rpc +to the service that wraps the manager + +Managers should be responsible for most of the db access, and +non-implementation specific data. Anything implementation specific that can't +be generalized should be done by the Driver. + +In general, we prefer to have one manager with multiple drivers for different +implementations, but sometimes it makes sense to have multiple managers. You +can think of it this way: Abstract different overall strategies at the manager +level(FlatNetwork vs VlanNetwork), and different implementations at the driver +level(LinuxNetDriver vs CiscoNetDriver). + +Managers will often provide methods for initial setup of a host or periodic +tasksto a wrapping service. + +This module provides Manager, a base class for managers. """ from nova import utils diff --git a/nova/service.py b/nova/service.py index d53d92b65..0eb3a2762 100644 --- a/nova/service.py +++ b/nova/service.py @@ -17,7 +17,11 @@ # under the License. """ -Generic Node baseclass for all workers that run on hosts +A service is a very thin wrapper around a Manager object. It exposes the +manager's public methods to other components of the system via rpc. It will +report state periodically to the database and is responsible for initiating any periodic tasts that need to be executed on a given host. + +This module contains Service, a generic baseclass for all workers. """ import inspect -- cgit From a999c8de9e51da2beda13fdbb66dfb0bad42f250 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Sun, 7 Nov 2010 14:46:17 -0500 Subject: back out stacked merge --- nova/manager.py | 34 +--------------------------------- nova/service.py | 6 +----- 2 files changed, 2 insertions(+), 38 deletions(-) (limited to 'nova') diff --git a/nova/manager.py b/nova/manager.py index a6efb8732..4244b2db4 100644 --- a/nova/manager.py +++ b/nova/manager.py @@ -15,40 +15,8 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - """ -Managers are responsible for a certain aspect of the sytem. It is a logical -grouping of code relating to a portion of the system. In general other -components should be using the manager to make changes to the components that -it is responsible for. - -For example, other components that need to deal with volumes in some way, -should do so by calling methods on the VolumeManager instead of directly -changing fields in the database. This allows us to keep all of the code -relating to volumes in the same place. - -We have adopted a basic strategy of Smart managers and dumb data, which means -rather than attaching methods to data objects, components should call manager -methods that act on the data. - -Methods on managers that can be executed locally should be called directly. If -a particular method must execute on a remote host, this should be done via rpc -to the service that wraps the manager - -Managers should be responsible for most of the db access, and -non-implementation specific data. Anything implementation specific that can't -be generalized should be done by the Driver. - -In general, we prefer to have one manager with multiple drivers for different -implementations, but sometimes it makes sense to have multiple managers. You -can think of it this way: Abstract different overall strategies at the manager -level(FlatNetwork vs VlanNetwork), and different implementations at the driver -level(LinuxNetDriver vs CiscoNetDriver). - -Managers will often provide methods for initial setup of a host or periodic -tasksto a wrapping service. - -This module provides Manager, a base class for managers. +Base class for managers of different parts of the system """ from nova import utils diff --git a/nova/service.py b/nova/service.py index 0eb3a2762..d53d92b65 100644 --- a/nova/service.py +++ b/nova/service.py @@ -17,11 +17,7 @@ # under the License. """ -A service is a very thin wrapper around a Manager object. It exposes the -manager's public methods to other components of the system via rpc. It will -report state periodically to the database and is responsible for initiating any periodic tasts that need to be executed on a given host. - -This module contains Service, a generic baseclass for all workers. +Generic Node baseclass for all workers that run on hosts """ import inspect -- cgit From 2c01c325719473fc764deec607a2b634ada5579a Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Sun, 7 Nov 2010 14:51:40 -0500 Subject: Merge lp:~termie/nova/trunkdoc (via patch, since bzr though it was already merged) --- nova/manager.py | 34 +++++++++++++++++++++++++++++++++- nova/service.py | 6 +++++- 2 files changed, 38 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/manager.py b/nova/manager.py index 4244b2db4..a6efb8732 100644 --- a/nova/manager.py +++ b/nova/manager.py @@ -15,8 +15,40 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + """ -Base class for managers of different parts of the system +Managers are responsible for a certain aspect of the sytem. It is a logical +grouping of code relating to a portion of the system. In general other +components should be using the manager to make changes to the components that +it is responsible for. + +For example, other components that need to deal with volumes in some way, +should do so by calling methods on the VolumeManager instead of directly +changing fields in the database. This allows us to keep all of the code +relating to volumes in the same place. + +We have adopted a basic strategy of Smart managers and dumb data, which means +rather than attaching methods to data objects, components should call manager +methods that act on the data. + +Methods on managers that can be executed locally should be called directly. If +a particular method must execute on a remote host, this should be done via rpc +to the service that wraps the manager + +Managers should be responsible for most of the db access, and +non-implementation specific data. Anything implementation specific that can't +be generalized should be done by the Driver. + +In general, we prefer to have one manager with multiple drivers for different +implementations, but sometimes it makes sense to have multiple managers. You +can think of it this way: Abstract different overall strategies at the manager +level(FlatNetwork vs VlanNetwork), and different implementations at the driver +level(LinuxNetDriver vs CiscoNetDriver). + +Managers will often provide methods for initial setup of a host or periodic +tasksto a wrapping service. + +This module provides Manager, a base class for managers. """ from nova import utils diff --git a/nova/service.py b/nova/service.py index d53d92b65..0eb3a2762 100644 --- a/nova/service.py +++ b/nova/service.py @@ -17,7 +17,11 @@ # under the License. """ -Generic Node baseclass for all workers that run on hosts +A service is a very thin wrapper around a Manager object. It exposes the +manager's public methods to other components of the system via rpc. It will +report state periodically to the database and is responsible for initiating any periodic tasts that need to be executed on a given host. + +This module contains Service, a generic baseclass for all workers. """ import inspect -- cgit From f2c84807600dd49458e7b342b70a4bb8f1bb2232 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Mon, 15 Nov 2010 14:43:50 -0500 Subject: pep8 --- nova/service.py | 3 ++- nova/utils.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/service.py b/nova/service.py index 0eb3a2762..9454d4049 100644 --- a/nova/service.py +++ b/nova/service.py @@ -19,7 +19,8 @@ """ A service is a very thin wrapper around a Manager object. It exposes the manager's public methods to other components of the system via rpc. It will -report state periodically to the database and is responsible for initiating any periodic tasts that need to be executed on a given host. +report state periodically to the database and is responsible for initiating +any periodic tasts that need to be executed on a given host. This module contains Service, a generic baseclass for all workers. """ diff --git a/nova/utils.py b/nova/utils.py index d7ebe5b4c..1207f52a4 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -234,6 +234,7 @@ def utf8(value): assert isinstance(value, str) return value + def fix_wsgify_docstr(wsgified_func): """A decorator to re-assign docstrings that webob.dec.wsgify clobbers.""" @functools.wraps(wsgified_func.func) -- cgit From f3744b0de85a1bc5be77f37a770144d3244bca86 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Mon, 15 Nov 2010 19:13:45 -0500 Subject: Change how wsgified doc wrapping happens to fix test. --- nova/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'nova') diff --git a/nova/utils.py b/nova/utils.py index 1207f52a4..d074cab75 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -237,7 +237,6 @@ def utf8(value): def fix_wsgify_docstr(wsgified_func): """A decorator to re-assign docstrings that webob.dec.wsgify clobbers.""" - @functools.wraps(wsgified_func.func) - def _f(*args, **kwargs): - wsgified_func(*args, **kwargs) - return _f + wsgified_func.__doc__ = wsgified_func.func.__doc__ + wsgified_func.func_name = wsgified_func.func.func_name + return wsgified_func -- cgit From ff3ec33010ce8ece87523f7cf3ef2e4a0a23006e Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Mon, 15 Nov 2010 21:08:08 -0500 Subject: The docs are just going to be wrong for now. I'll file a bug upstream. --- nova/api/__init__.py | 2 -- nova/utils.py | 7 ------- 2 files changed, 9 deletions(-) (limited to 'nova') diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 1dabd3d21..7e75445a8 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -84,7 +84,6 @@ class API(wsgi.Router): mapper.connect("/cloudpipe/{path_info:.*}", controller=cloudpipe.API()) super(API, self).__init__(mapper) - @utils.fix_wsgify_docstr @webob.dec.wsgify def osapi_versions(self, req): """Respond to a request for all OpenStack API versions.""" @@ -96,7 +95,6 @@ class API(wsgi.Router): "attributes": dict(version=["status", "id"])}} return wsgi.Serializer(req.environ, metadata).to_content_type(response) - @utils.fix_wsgify_docstr @webob.dec.wsgify def ec2api_versions(self, req): """Respond to a request for all EC2 versions.""" diff --git a/nova/utils.py b/nova/utils.py index d074cab75..2970b93bb 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -233,10 +233,3 @@ def utf8(value): return value.encode("utf-8") assert isinstance(value, str) return value - - -def fix_wsgify_docstr(wsgified_func): - """A decorator to re-assign docstrings that webob.dec.wsgify clobbers.""" - wsgified_func.__doc__ = wsgified_func.func.__doc__ - wsgified_func.func_name = wsgified_func.func.func_name - return wsgified_func -- cgit