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 dfe98891b46c4f02f13ea2686979ca7ff4547bd3 Mon Sep 17 00:00:00 2001 From: Ryan Lane Date: Mon, 25 Oct 2010 23:10:51 +0000 Subject: Making net injection create /etc/network if non-existant --- nova/compute/disk.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/compute/disk.py b/nova/compute/disk.py index e362b4507..f2e5f8570 100644 --- a/nova/compute/disk.py +++ b/nova/compute/disk.py @@ -171,6 +171,9 @@ def _inject_key_into_fs(key, fs, execute=None): @defer.inlineCallbacks def _inject_net_into_fs(net, fs, execute=None): - netfile = os.path.join(os.path.join(os.path.join( - fs, 'etc'), 'network'), 'interfaces') + netdir = os.path.join(os.path.join(fs, 'etc'), 'network') + yield execute('sudo mkdir -p %s' % netdir) # existing dir doesn't matter + yield execute('sudo chown root:root %s' % netdir) + yield execute('sudo chmod 755 %s' % netdir) + netfile = os.path.join(netdir, 'interfaces') yield execute('sudo tee %s' % netfile, net) -- 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 60c82177da9c4ebbb89e5534959d0d5a52bfa49a Mon Sep 17 00:00:00 2001 From: Eric Day Date: Wed, 3 Nov 2010 12:38:15 -0700 Subject: Fix for bug#613264, allowing hosts to be specified for nova-api and objectstore listeners. --- nova/objectstore/handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index b26906001..aaf207db4 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -438,6 +438,7 @@ def get_application(): # Disabled because of lack of proper introspection in Twisted # or possibly different versions of twisted? # pylint: disable-msg=E1101 - objectStoreService = internet.TCPServer(FLAGS.s3_port, factory) + objectStoreService = internet.TCPServer(FLAGS.s3_port, factory, + interface=FLAGS.s3_host) objectStoreService.setServiceParent(application) return application -- cgit From d65c35bcadc6cc4e4d1fc61502d43fd001ce2f0e Mon Sep 17 00:00:00 2001 From: Eric Day Date: Wed, 3 Nov 2010 13:13:59 -0700 Subject: Added an extra argument to the objectstore listen to separate out the listening host from the connecting host. --- nova/objectstore/handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index aaf207db4..c8920b00c 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -61,6 +61,7 @@ from nova.objectstore import image FLAGS = flags.FLAGS +flags.DEFINE_string('s3_listen_host', '', 'Host to listen on.') def render_xml(request, value): @@ -439,6 +440,6 @@ def get_application(): # or possibly different versions of twisted? # pylint: disable-msg=E1101 objectStoreService = internet.TCPServer(FLAGS.s3_port, factory, - interface=FLAGS.s3_host) + interface=FLAGS.s3_listen_host) objectStoreService.setServiceParent(application) return application -- 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 3b695e11da34247123ea919e71096e53393f227b Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Thu, 11 Nov 2010 19:52:36 -0600 Subject: Added a .mailmap that maps addresses in bzr to people's real, preferred e-mail addresses. (I made a few guesses along the way, feel free to adjust according to what is actually the preferred e-mail) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added a couple of methods to nova.utils to parse said .mailmap and do the appropriate (though highly naïve) replacement. Apply mailmap replacement in changelog generation in setup.py. Add a unit test that checks everyone is properly listed in Authors. Add sleepsonthefloor to Authors. If anyone knows the real name, please add it. --- nova/tests/misc_unittest.py | 48 +++++++++++++++++++++++++++++++++++++++++++++ nova/utils.py | 16 +++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 nova/tests/misc_unittest.py (limited to 'nova') diff --git a/nova/tests/misc_unittest.py b/nova/tests/misc_unittest.py new file mode 100644 index 000000000..856060afa --- /dev/null +++ b/nova/tests/misc_unittest.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import subprocess + +from nova import test +from nova.utils import parse_mailmap, str_dict_replace + + +class ProjectTestCase(test.TrialTestCase): + def test_authors_up_to_date(self): + if os.path.exists('../.bzr'): + log_cmd = subprocess.Popen(["bzr", "log", "-n0"], + stdout=subprocess.PIPE) + changelog = log_cmd.communicate()[0] + mailmap = parse_mailmap('../.mailmap') + + contributors = set() + for l in changelog.split('\n'): + l = l.strip() + if (l.startswith('author:') or l.startswith('committer:') + and not l == 'committer: Tarmac'): + email = l.split(' ')[-1] + contributors.add(str_dict_replace(email, mailmap)) + + authors_file = open('../Authors', 'r').read() + + missing = set() + for contributor in contributors: + if not contributor in authors_file: + missing.add(contributor) + + self.assertTrue(len(missing) == 0, + '%r not listed in Authors' % missing) diff --git a/nova/utils.py b/nova/utils.py index e7892a212..b63237c10 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -173,6 +173,22 @@ def isotime(at=None): def parse_isotime(timestr): return datetime.datetime.strptime(timestr, TIME_FORMAT) +def parse_mailmap(mailmap='.mailmap'): + mapping = {} + if os.path.exists(mailmap): + fp = open(mailmap, 'r') + for l in fp: + l = l.strip() + if not l.startswith('#') and ' ' in l: + canonical_email, alias = l.split(' ') + mapping[alias] = canonical_email + return mapping + +def str_dict_replace(s, mapping): + for s1, s2 in mapping.iteritems(): + s = s.replace(s1, s2) + return s + class LazyPluggable(object): """A pluggable backend loaded lazily based on some value.""" -- 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 From 777663e9673310880e0aaf47093ceedd153eedeb Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 16 Nov 2010 13:26:59 -0500 Subject: pep8 --- nova/compute/disk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/compute/disk.py b/nova/compute/disk.py index 7f1e2b25f..925c20eee 100644 --- a/nova/compute/disk.py +++ b/nova/compute/disk.py @@ -176,7 +176,7 @@ def _inject_key_into_fs(key, fs, execute=None): @defer.inlineCallbacks def _inject_net_into_fs(net, fs, execute=None): netdir = os.path.join(os.path.join(fs, 'etc'), 'network') - yield execute('sudo mkdir -p %s' % netdir) # existing dir doesn't matter + yield execute('sudo mkdir -p %s' % netdir) # existing dir doesn't matter yield execute('sudo chown root:root %s' % netdir) yield execute('sudo chmod 755 %s' % netdir) netfile = os.path.join(netdir, 'interfaces') -- cgit From 11bca7a4f07c2e7037c8b08b2383a7c6e296b15a Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 16 Nov 2010 13:32:16 -0500 Subject: Add docstrings to any methods I touch --- nova/compute/disk.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'nova') diff --git a/nova/compute/disk.py b/nova/compute/disk.py index 925c20eee..61c9c077f 100644 --- a/nova/compute/disk.py +++ b/nova/compute/disk.py @@ -165,6 +165,11 @@ def inject_data(image, key=None, net=None, partition=None, execute=None): @defer.inlineCallbacks def _inject_key_into_fs(key, fs, execute=None): + """Add the given public ssh key to root's authorized_keys. + + key is an ssh key string. + fs is the path to the base of the filesystem into which to inject the key. + """ sshdir = os.path.join(os.path.join(fs, 'root'), '.ssh') yield execute('sudo mkdir -p %s' % sshdir) # existing dir doesn't matter yield execute('sudo chown root %s' % sshdir) @@ -175,6 +180,10 @@ def _inject_key_into_fs(key, fs, execute=None): @defer.inlineCallbacks def _inject_net_into_fs(net, fs, execute=None): + """Inject /etc/network/interfaces into the filesystem rooted at fs. + + net is the contents of /etc/network/interfaces. + """ netdir = os.path.join(os.path.join(fs, 'etc'), 'network') yield execute('sudo mkdir -p %s' % netdir) # existing dir doesn't matter yield execute('sudo chown root:root %s' % netdir) -- cgit From e72ae82362fb8a93d599c0c4473aa41c96837cf5 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 16 Nov 2010 13:49:18 -0500 Subject: pep8 --- nova/compute/disk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/compute/disk.py b/nova/compute/disk.py index 61c9c077f..4338d39f0 100644 --- a/nova/compute/disk.py +++ b/nova/compute/disk.py @@ -166,7 +166,7 @@ def inject_data(image, key=None, net=None, partition=None, execute=None): @defer.inlineCallbacks def _inject_key_into_fs(key, fs, execute=None): """Add the given public ssh key to root's authorized_keys. - + key is an ssh key string. fs is the path to the base of the filesystem into which to inject the key. """ -- cgit From 7b4733b36d5351a2ba42c82b4d2b821a3b1d12cd Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Tue, 16 Nov 2010 23:38:37 +0000 Subject: fixes errors in describe address and associate address. Adds test cases --- nova/api/ec2/cloud.py | 6 +++--- nova/tests/cloud_unittest.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) (limited to 'nova') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index fbe4caa48..e2eaa7c5c 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -679,7 +679,7 @@ class CloudController(object): context.project_id) for floating_ip_ref in iterator: address = floating_ip_ref['address'] - instance_id = None + ec2_id = None if (floating_ip_ref['fixed_ip'] and floating_ip_ref['fixed_ip']['instance']): internal_id = floating_ip_ref['fixed_ip']['instance']['ec2_id'] @@ -717,8 +717,8 @@ class CloudController(object): "args": {"floating_address": floating_ip_ref['address']}}) return {'releaseResponse': ["Address released."]} - def associate_address(self, context, ec2_id, public_ip, **kwargs): - internal_id = ec2_id_to_internal_id(ec2_id) + def associate_address(self, context, instance_id, public_ip, **kwargs): + internal_id = ec2_id_to_internal_id(instance_id) instance_ref = db.instance_get_by_internal_id(context, internal_id) fixed_address = db.instance_get_fixed_address(context, instance_ref['id']) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 2d61d2675..938210fa7 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -91,6 +91,35 @@ class CloudTestCase(test.TrialTestCase): # NOTE(vish): create depends on pool, so just call helper directly return cloud._gen_key(self.context, self.context.user.id, name) + def test_describe_addresses(self): + """Makes sure describe addresses runs without raising an exception""" + address = "10.10.10.10" + db.floating_ip_create(context.get_admin_context(), + {'address': address, + 'host': FLAGS.host}) + self.cloud.allocate_address(self.context) + self.cloud.describe_addresses(self.context) + self.cloud.release_address(self.context, + public_ip=address) + + def test_associate_disassociate_address(self): + """Verifies associate runs cleanly without raising an exception""" + address = "10.10.10.10" + db.floating_ip_create(context.get_admin_context(), + {'address': address, + 'host': FLAGS.host}) + self.cloud.allocate_address(self.context) + inst = db.instance_create(self.context, {}) + ec2_id = cloud.internal_id_to_ec2_id(inst['internal_id']) + self.cloud.associate_address(self.context, + instance_id=ec2_id, + public_ip=address) + self.cloud.disassociate_address(self.context, + public_ip=address) + self.cloud.release_address(self.context, + public_ip=address) + db.instance_destroy(self.context, inst['id']) + def test_console_output(self): image_id = FLAGS.default_image instance_type = FLAGS.default_instance_type -- cgit From 5bd44c890be3fa5c632897c106409ff556e6a19a Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 17 Nov 2010 02:23:20 +0000 Subject: fix leaking floating ip from network unittests and use of fakeldap driver --- nova/tests/network_unittest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index b7caed4fd..6f4705719 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -41,7 +41,6 @@ class NetworkTestCase(test.TrialTestCase): # flags in the corresponding section in nova-dhcpbridge self.flags(connection_type='fake', fake_network=True, - auth_driver='nova.auth.ldapdriver.FakeLdapDriver', network_size=16, num_networks=5) logging.getLogger().setLevel(logging.DEBUG) @@ -127,6 +126,7 @@ class NetworkTestCase(test.TrialTestCase): self.network.deallocate_floating_ip(self.context, float_addr) self.network.deallocate_fixed_ip(self.context, fix_addr) release_ip(fix_addr) + db.floating_ip_destroy(context.get_admin_context(), float_addr) def test_allocate_deallocate_fixed_ip(self): """Makes sure that we can allocate and deallocate a fixed ip""" -- cgit From db8c0a153df467c645df82b2ed6c2b282eae6850 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 17 Nov 2010 02:41:04 +0000 Subject: delete floating ips after tests --- nova/tests/cloud_unittest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 938210fa7..2c6d9959b 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -94,18 +94,19 @@ class CloudTestCase(test.TrialTestCase): def test_describe_addresses(self): """Makes sure describe addresses runs without raising an exception""" address = "10.10.10.10" - db.floating_ip_create(context.get_admin_context(), + db.floating_ip_create(self.context, {'address': address, 'host': FLAGS.host}) self.cloud.allocate_address(self.context) self.cloud.describe_addresses(self.context) self.cloud.release_address(self.context, public_ip=address) + db.floating_ip_destroy(self.context, address) def test_associate_disassociate_address(self): """Verifies associate runs cleanly without raising an exception""" address = "10.10.10.10" - db.floating_ip_create(context.get_admin_context(), + db.floating_ip_create(self.context, {'address': address, 'host': FLAGS.host}) self.cloud.allocate_address(self.context) @@ -119,6 +120,7 @@ class CloudTestCase(test.TrialTestCase): self.cloud.release_address(self.context, public_ip=address) db.instance_destroy(self.context, inst['id']) + db.floating_ip_destroy(self.context, address) def test_console_output(self): image_id = FLAGS.default_image -- cgit From 973b2d6892dfacb3d9a2f7e87514f6e18faa37e4 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Wed, 17 Nov 2010 13:56:42 -0600 Subject: Improved Pylint Score --- nova/db/sqlalchemy/api.py | 9 ++++----- nova/image/services/glance/__init__.py | 1 + nova/network/linux_net.py | 2 +- nova/objectstore/bucket.py | 4 ++-- nova/objectstore/image.py | 4 ++-- nova/virt/libvirt_conn.py | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) (limited to 'nova') diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index b8f999af4..2a5442d9b 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -44,7 +44,6 @@ def is_admin_context(context): warnings.warn('Use of empty request context is deprecated', DeprecationWarning) raise Exception('die') - return True return context.is_admin @@ -502,14 +501,14 @@ def fixed_ip_get_by_address(context, address, session=None): @require_context def fixed_ip_get_instance(context, address): - fixed_ip_ref = fixed_ip_get_by_address(context, address) - return fixed_ip_ref.instance + fixed_ip_ref = fixed_ip_get_by_address(context, address) + return fixed_ip_ref.instance @require_admin_context def fixed_ip_get_network(context, address): - fixed_ip_ref = fixed_ip_get_by_address(context, address) - return fixed_ip_ref.network + fixed_ip_ref = fixed_ip_get_by_address(context, address) + return fixed_ip_ref.network @require_context diff --git a/nova/image/services/glance/__init__.py b/nova/image/services/glance/__init__.py index f1d05f0bc..9617ab7a3 100644 --- a/nova/image/services/glance/__init__.py +++ b/nova/image/services/glance/__init__.py @@ -19,6 +19,7 @@ import httplib import json +import logging import urlparse import webob.exc diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 7b323efa1..4ea24cda6 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -244,7 +244,7 @@ def _confirm_rule(chain, cmd): def _remove_rule(chain, cmd): """Remove iptables rule""" if FLAGS.use_nova_chains: - chain = "%S" % chain.lower() + chain = "%s" % chain.lower() _execute("sudo iptables --delete %s %s" % (chain, cmd)) diff --git a/nova/objectstore/bucket.py b/nova/objectstore/bucket.py index 0ba4934d1..697982538 100644 --- a/nova/objectstore/bucket.py +++ b/nova/objectstore/bucket.py @@ -78,8 +78,8 @@ class Bucket(object): path = os.path.abspath(os.path.join( FLAGS.buckets_path, bucket_name)) if not path.startswith(os.path.abspath(FLAGS.buckets_path)) or \ - os.path.exists(path): - raise exception.NotAuthorized() + os.path.exists(path): + raise exception.NotAuthorized() os.makedirs(path) diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py index b7b2ec6ab..4554444fa 100644 --- a/nova/objectstore/image.py +++ b/nova/objectstore/image.py @@ -48,8 +48,8 @@ class Image(object): self.image_id = image_id self.path = os.path.abspath(os.path.join(FLAGS.images_path, image_id)) if not self.path.startswith(os.path.abspath(FLAGS.images_path)) or \ - not os.path.isdir(self.path): - raise exception.NotFound + not os.path.isdir(self.path): + raise exception.NotFound @property def image_path(self): diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 535a3b53e..18085089f 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -420,7 +420,7 @@ class LibvirtConnection(object): @defer.inlineCallbacks def _create_image(self, inst, libvirt_xml, prefix='', disk_images=None): # syntactic nicety - basepath = lambda fname='', prefix=prefix: os.path.join( + basepath = lambda fname = '', prefix = prefix: os.path.join( FLAGS.instances_path, inst['name'], prefix + fname) @@ -457,7 +457,7 @@ class LibvirtConnection(object): yield images.fetch(inst.ramdisk_id, basepath('ramdisk'), user, project) - execute = lambda cmd, process_input=None, check_exit_code=True: \ + execute = lambda cmd, process_input = None, check_exit_code = True: \ process.simple_execute(cmd=cmd, process_input=process_input, check_exit_code=check_exit_code) -- cgit From 1dfdd7477647e45b96b5781789b846d5c9066663 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 17 Nov 2010 21:23:12 +0000 Subject: fix greenthread race conditions in trunk and floating ip leakage --- nova/db/sqlalchemy/api.py | 2 +- nova/tests/cloud_unittest.py | 4 ++++ nova/tests/quota_unittest.py | 8 +++----- 3 files changed, 8 insertions(+), 6 deletions(-) (limited to 'nova') diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index b8f999af4..dfa4efc8f 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -390,7 +390,7 @@ def floating_ip_get_by_address(context, address, session=None): filter_by(deleted=can_read_deleted(context)).\ first() if not result: - raise exception.NotFound('No fixed ip for address %s' % address) + raise exception.NotFound('No floating ip for address %s' % address) return result diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 2c6d9959b..9886a2449 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -101,6 +101,7 @@ class CloudTestCase(test.TrialTestCase): self.cloud.describe_addresses(self.context) self.cloud.release_address(self.context, public_ip=address) + greenthread.sleep(0.3) db.floating_ip_destroy(self.context, address) def test_associate_disassociate_address(self): @@ -111,6 +112,7 @@ class CloudTestCase(test.TrialTestCase): 'host': FLAGS.host}) self.cloud.allocate_address(self.context) inst = db.instance_create(self.context, {}) + fixed = self.network.allocate_fixed_ip(self.context, inst['id']) ec2_id = cloud.internal_id_to_ec2_id(inst['internal_id']) self.cloud.associate_address(self.context, instance_id=ec2_id, @@ -119,6 +121,8 @@ class CloudTestCase(test.TrialTestCase): public_ip=address) self.cloud.release_address(self.context, public_ip=address) + greenthread.sleep(0.3) + self.network.deallocate_fixed_ip(self.context, fixed) db.instance_destroy(self.context, inst['id']) db.floating_ip_destroy(self.context, address) diff --git a/nova/tests/quota_unittest.py b/nova/tests/quota_unittest.py index 9e3afbf4e..b7c1d2acc 100644 --- a/nova/tests/quota_unittest.py +++ b/nova/tests/quota_unittest.py @@ -138,11 +138,8 @@ class QuotaTestCase(test.TrialTestCase): def test_too_many_addresses(self): address = '192.168.0.100' - try: - db.floating_ip_get_by_address(context.get_admin_context(), address) - except exception.NotFound: - db.floating_ip_create(context.get_admin_context(), - {'address': address, 'host': FLAGS.host}) + db.floating_ip_create(context.get_admin_context(), + {'address': address, 'host': FLAGS.host}) float_addr = self.network.allocate_floating_ip(self.context, self.project.id) # NOTE(vish): This assert never fails. When cloud attempts to @@ -151,3 +148,4 @@ class QuotaTestCase(test.TrialTestCase): # that is breaking. self.assertRaises(cloud.QuotaError, self.cloud.allocate_address, self.context) + db.floating_ip_destroy(context.get_admin_context(), address) -- cgit From 70e10503472d37f08f9f0880c87e10afc3abc851 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 18 Nov 2010 10:52:54 -0800 Subject: Make sure that the response body is a string and not unicode --- nova/api/ec2/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index b7664ec71..a6ee16c33 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -244,8 +244,8 @@ class Executor(wsgi.Application): resp = webob.Response() resp.status = 400 resp.headers['Content-Type'] = 'text/xml' - resp.body = ('\n' + resp.body = str('\n' '%s' '%s' - '?') % (code, message) + '?' % (code, message)) return resp -- cgit From 8e1b88cc228f9ed55c3b6e4fdd790a572d63e6fe Mon Sep 17 00:00:00 2001 From: Eric Day Date: Thu, 18 Nov 2010 13:27:52 -0800 Subject: First step to getting the image APIs consolidated. The EC2 API was using a one-off S3 image service wrapper, but this should be moved into the nova.image space and use the same interface as the others. There are still some mismatches between the various image service implementations, but this patch was getting large and wanted to keep it within a resonable size. --- nova/api/ec2/cloud.py | 24 ++-- nova/api/ec2/images.py | 123 ----------------- nova/api/openstack/images.py | 13 +- nova/flags.py | 2 +- nova/image/glance.py | 227 ++++++++++++++++++++++++++++++++ nova/image/local.py | 88 +++++++++++++ nova/image/s3.py | 108 +++++++++++++++ nova/image/service.py | 97 +------------- nova/image/services/__init__.py | 0 nova/image/services/glance/__init__.py | 216 ------------------------------ nova/tests/api/openstack/fakes.py | 20 ++- nova/tests/api/openstack/test_images.py | 40 +++--- 12 files changed, 481 insertions(+), 477 deletions(-) delete mode 100644 nova/api/ec2/images.py create mode 100644 nova/image/glance.py create mode 100644 nova/image/local.py create mode 100644 nova/image/s3.py delete mode 100644 nova/image/services/__init__.py delete mode 100644 nova/image/services/glance/__init__.py (limited to 'nova') diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index e2eaa7c5c..9327bf0d4 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -41,7 +41,7 @@ from nova import rpc from nova import utils from nova.compute.instance_types import INSTANCE_TYPES from nova.api import cloud -from nova.api.ec2 import images +from nova.image.s3 import S3ImageService FLAGS = flags.FLAGS @@ -100,6 +100,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.image_service = S3ImageService() self.setup() def __str__(self): @@ -785,7 +786,7 @@ class CloudController(object): vpn = kwargs['image_id'] == FLAGS.vpn_image_id if not vpn: - image = images.get(context, kwargs['image_id']) + image = self.image_service.show(context, kwargs['image_id']) # FIXME(ja): if image is vpn, this breaks # get defaults from imagestore @@ -798,8 +799,8 @@ class CloudController(object): ramdisk_id = kwargs.get('ramdisk_id', ramdisk_id) # make sure we have access to kernel and ramdisk - images.get(context, kernel_id) - images.get(context, ramdisk_id) + self.image_service.show(context, kernel_id) + self.image_service.show(context, ramdisk_id) logging.debug("Going to run %s instances...", num_instances) launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) @@ -993,20 +994,17 @@ class CloudController(object): return True def describe_images(self, context, image_id=None, **kwargs): - # The objectstore does its own authorization for describe - imageSet = images.list(context, image_id) + imageSet = self.image_service.index(context, image_id) return {'imagesSet': imageSet} def deregister_image(self, context, image_id, **kwargs): - # FIXME: should the objectstore be doing these authorization checks? - images.deregister(context, image_id) + self.image_service.deregister(context, image_id) return {'imageId': image_id} def register_image(self, context, image_location=None, **kwargs): - # FIXME: should the objectstore be doing these authorization checks? if image_location is None and 'name' in kwargs: image_location = kwargs['name'] - image_id = images.register(context, image_location) + image_id = self.image_service.register(context, image_location) logging.debug("Registered %s as %s" % (image_location, image_id)) return {'imageId': image_id} @@ -1014,7 +1012,7 @@ class CloudController(object): if attribute != 'launchPermission': raise exception.ApiError('attribute not supported: %s' % attribute) try: - image = images.list(context, image_id)[0] + image = self.image_service.show(context, image_id) except IndexError: raise exception.ApiError('invalid id: %s' % image_id) result = {'image_id': image_id, 'launchPermission': []} @@ -1033,8 +1031,8 @@ class CloudController(object): raise exception.ApiError('only group "all" is supported') if not operation_type in ['add', 'remove']: raise exception.ApiError('operation_type must be add or remove') - return images.modify(context, image_id, operation_type) + return self.image_service.modify(context, image_id, operation_type) def update_image(self, context, image_id, **kwargs): - result = images.update(context, image_id, dict(kwargs)) + result = self.image_service.update(context, image_id, dict(kwargs)) return result diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py deleted file mode 100644 index 60f9008e9..000000000 --- a/nova/api/ec2/images.py +++ /dev/null @@ -1,123 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Proxy AMI-related calls from the cloud controller, to the running -objectstore service. -""" - -import json -import urllib - -import boto.s3.connection - -from nova import exception -from nova import flags -from nova import utils -from nova.auth import manager - - -FLAGS = flags.FLAGS - - -def modify(context, image_id, operation): - conn(context).make_request( - method='POST', - bucket='_images', - query_args=qs({'image_id': image_id, 'operation': operation})) - - return True - - -def update(context, image_id, attributes): - """update an image's attributes / info.json""" - attributes.update({"image_id": image_id}) - conn(context).make_request( - method='POST', - bucket='_images', - query_args=qs(attributes)) - return True - - -def register(context, image_location): - """ rpc call to register a new image based from a manifest """ - - image_id = utils.generate_uid('ami') - conn(context).make_request( - method='PUT', - bucket='_images', - query_args=qs({'image_location': image_location, - 'image_id': image_id})) - - return image_id - - -def list(context, filter_list=[]): - """ return a list of all images that a user can see - - optionally filtered by a list of image_id """ - - if FLAGS.connection_type == 'fake': - return [{'imageId': 'bar'}] - - # FIXME: send along the list of only_images to check for - response = conn(context).make_request( - method='GET', - bucket='_images') - - result = json.loads(response.read()) - if not filter_list is None: - return [i for i in result if i['imageId'] in filter_list] - return result - - -def get(context, image_id): - """return a image object if the context has permissions""" - result = list(context, [image_id]) - if not result: - raise exception.NotFound('Image %s could not be found' % image_id) - image = result[0] - return image - - -def deregister(context, image_id): - """ unregister an image """ - conn(context).make_request( - method='DELETE', - bucket='_images', - query_args=qs({'image_id': image_id})) - - -def conn(context): - access = manager.AuthManager().get_access_key(context.user, - context.project) - secret = str(context.user.secret) - calling = boto.s3.connection.OrdinaryCallingFormat() - return boto.s3.connection.S3Connection(aws_access_key_id=access, - aws_secret_access_key=secret, - is_secure=False, - calling_format=calling, - port=FLAGS.s3_port, - host=FLAGS.s3_host) - - -def qs(params): - pairs = [] - for key in params.keys(): - pairs.append(key + '=' + urllib.quote(params[key])) - return '&'.join(pairs) diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 5bc915e63..cdbdc9bdd 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -17,6 +17,7 @@ from webob import exc +from nova import context from nova import flags from nova import utils from nova import wsgi @@ -46,19 +47,23 @@ class Controller(wsgi.Controller): def detail(self, req): """Return all public images in detail.""" + user_id = req.environ['nova.context']['user']['id'] + ctxt = context.RequestContext(user_id, user_id) try: - images = self._service.detail() + images = self._service.detail(ctxt) images = nova.api.openstack.limited(images, req) except NotImplementedError: # Emulate detail() using repeated calls to show() - images = self._service.index() + images = self._service.index(ctxt) images = nova.api.openstack.limited(images, req) - images = [self._service.show(i['id']) for i in images] + images = [self._service.show(ctxt, i['id']) for i in images] return dict(images=images) def show(self, req, id): """Return data about the given image id.""" - return dict(image=self._service.show(id)) + user_id = req.environ['nova.context']['user']['id'] + ctxt = context.RequestContext(user_id, user_id) + return dict(image=self._service.show(ctxt, id)) def delete(self, req, id): # Only public images are supported for now. diff --git a/nova/flags.py b/nova/flags.py index 4ae86d9b2..07b469bca 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -232,7 +232,7 @@ DEFINE_string('scheduler_manager', 'nova.scheduler.manager.SchedulerManager', 'Manager for scheduler') # The service to use for image search and retrieval -DEFINE_string('image_service', 'nova.image.service.LocalImageService', +DEFINE_string('image_service', 'nova.image.local.LocalImageService', 'The service to use for retrieving and searching for images.') DEFINE_string('host', socket.gethostname(), diff --git a/nova/image/glance.py b/nova/image/glance.py new file mode 100644 index 000000000..547381a4a --- /dev/null +++ b/nova/image/glance.py @@ -0,0 +1,227 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Implementation of an image service that uses Glance as the backend""" + +import httplib +import json +import urlparse + +import webob.exc + +from nova import utils +from nova import flags +from nova import exception +import nova.image.service + +FLAGS = flags.FLAGS + + +flags.DEFINE_string('glance_teller_address', 'http://127.0.0.1', + 'IP address or URL where Glance\'s Teller service resides') +flags.DEFINE_string('glance_teller_port', '9191', + 'Port for Glance\'s Teller service') +flags.DEFINE_string('glance_parallax_address', 'http://127.0.0.1', + 'IP address or URL where Glance\'s Parallax service ' + 'resides') +flags.DEFINE_string('glance_parallax_port', '9292', + 'Port for Glance\'s Parallax service') + + +class TellerClient(object): + + def __init__(self): + self.address = FLAGS.glance_teller_address + self.port = FLAGS.glance_teller_port + url = urlparse.urlparse(self.address) + self.netloc = url.netloc + self.connection_type = {'http': httplib.HTTPConnection, + 'https': httplib.HTTPSConnection}[url.scheme] + + +class ParallaxClient(object): + + def __init__(self): + self.address = FLAGS.glance_parallax_address + self.port = FLAGS.glance_parallax_port + url = urlparse.urlparse(self.address) + self.netloc = url.netloc + self.connection_type = {'http': httplib.HTTPConnection, + 'https': httplib.HTTPSConnection}[url.scheme] + + def get_image_index(self): + """ + Returns a list of image id/name mappings from Parallax + """ + try: + c = self.connection_type(self.netloc, self.port) + c.request("GET", "images") + res = c.getresponse() + if res.status == 200: + # Parallax returns a JSONified dict(images=image_list) + data = json.loads(res.read())['images'] + return data + else: + logging.warn("Parallax returned HTTP error %d from " + "request for /images", res.status_int) + return [] + finally: + c.close() + + def get_image_details(self): + """ + Returns a list of detailed image data mappings from Parallax + """ + try: + c = self.connection_type(self.netloc, self.port) + c.request("GET", "images/detail") + res = c.getresponse() + if res.status == 200: + # Parallax returns a JSONified dict(images=image_list) + data = json.loads(res.read())['images'] + return data + else: + logging.warn("Parallax returned HTTP error %d from " + "request for /images/detail", res.status_int) + return [] + finally: + c.close() + + def get_image_metadata(self, image_id): + """ + Returns a mapping of image metadata from Parallax + """ + try: + c = self.connection_type(self.netloc, self.port) + c.request("GET", "images/%s" % image_id) + res = c.getresponse() + if res.status == 200: + # Parallax returns a JSONified dict(image=image_info) + data = json.loads(res.read())['image'] + return data + else: + # TODO(jaypipes): log the error? + return None + finally: + c.close() + + def add_image_metadata(self, image_metadata): + """ + Tells parallax about an image's metadata + """ + try: + c = self.connection_type(self.netloc, self.port) + body = json.dumps(image_metadata) + c.request("POST", "images", body) + res = c.getresponse() + if res.status == 200: + # Parallax returns a JSONified dict(image=image_info) + data = json.loads(res.read())['image'] + return data['id'] + else: + # TODO(jaypipes): log the error? + return None + finally: + c.close() + + def update_image_metadata(self, image_id, image_metadata): + """ + Updates Parallax's information about an image + """ + try: + c = self.connection_type(self.netloc, self.port) + body = json.dumps(image_metadata) + c.request("PUT", "images/%s" % image_id, body) + res = c.getresponse() + return res.status == 200 + finally: + c.close() + + def delete_image_metadata(self, image_id): + """ + Deletes Parallax's information about an image + """ + try: + c = self.connection_type(self.netloc, self.port) + c.request("DELETE", "images/%s" % image_id) + res = c.getresponse() + return res.status == 200 + finally: + c.close() + + +class GlanceImageService(nova.image.service.BaseImageService): + """Provides storage and retrieval of disk image objects within Glance.""" + + def __init__(self): + self.teller = TellerClient() + self.parallax = ParallaxClient() + + def index(self, context): + """ + Calls out to Parallax for a list of images available + """ + images = self.parallax.get_image_index() + return images + + def detail(self, context): + """ + Calls out to Parallax for a list of detailed image information + """ + images = self.parallax.get_image_details() + return images + + def show(self, context, id): + """ + Returns a dict containing image data for the given opaque image id. + """ + image = self.parallax.get_image_metadata(id) + if image: + return image + raise exception.NotFound + + def create(self, context, data): + """ + Store the image data and return the new image id. + + :raises AlreadyExists if the image already exist. + + """ + return self.parallax.add_image_metadata(data) + + def update(self, context, image_id, data): + """Replace the contents of the given image with the new data. + + :raises NotFound if the image does not exist. + + """ + self.parallax.update_image_metadata(image_id, data) + + def delete(self, context, image_id): + """ + Delete the given image. + + :raises NotFound if the image does not exist. + + """ + self.parallax.delete_image_metadata(image_id) + + def delete_all(self): + """ + Clears out all images + """ + pass diff --git a/nova/image/local.py b/nova/image/local.py new file mode 100644 index 000000000..9b0cdcc50 --- /dev/null +++ b/nova/image/local.py @@ -0,0 +1,88 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import cPickle as pickle +import os.path +import random + +from nova import exception +from nova.image import service + + +class LocalImageService(service.BaseImageService): + + """Image service storing images to local disk. + + It assumes that image_ids are integers.""" + + def __init__(self): + self._path = "/tmp/nova/images" + try: + os.makedirs(self._path) + except OSError: # Exists + pass + + def _path_to(self, image_id): + return os.path.join(self._path, str(image_id)) + + def _ids(self): + """The list of all image ids.""" + return [int(i) for i in os.listdir(self._path)] + + def index(self, context): + return [dict(id=i['id'], name=i['name']) for i in self.detail(context)] + + def detail(self, context): + return [self.show(context, id) for id in self._ids()] + + def show(self, context, id): + try: + return pickle.load(open(self._path_to(id))) + except IOError: + raise exception.NotFound + + def create(self, context, data): + """ + Store the image data and return the new image id. + """ + id = random.randint(0, 2 ** 32 - 1) + data['id'] = id + self.update(context, id, data) + return id + + def update(self, context, image_id, data): + """Replace the contents of the given image with the new data.""" + try: + pickle.dump(data, open(self._path_to(image_id), 'w')) + except IOError: + raise exception.NotFound + + def delete(self, context, image_id): + """ + Delete the given image. Raises OSError if the image does not exist. + """ + try: + os.unlink(self._path_to(image_id)) + except IOError: + raise exception.NotFound + + def delete_all(self): + """ + Clears out all images in local directory + """ + for id in self._ids(): + os.unlink(self._path_to(id)) diff --git a/nova/image/s3.py b/nova/image/s3.py new file mode 100644 index 000000000..ba1086aca --- /dev/null +++ b/nova/image/s3.py @@ -0,0 +1,108 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Proxy AMI-related calls from the cloud controller, to the running +objectstore service. +""" + +import json +import urllib + +import boto.s3.connection + +from nova import exception +from nova import flags +from nova import utils +from nova.auth import manager +from nova.image import service + + +FLAGS = flags.FLAGS + + +class S3ImageService(service.BaseImageService): + + def modify(self, context, image_id, operation): + self._conn(context).make_request( + method='POST', + bucket='_images', + query_args=self._qs({'image_id': image_id, 'operation': operation})) + return True + + def update(self, context, image_id, attributes): + """update an image's attributes / info.json""" + attributes.update({"image_id": image_id}) + self._conn(context).make_request( + method='POST', + bucket='_images', + query_args=self._qs(attributes)) + return True + + def register(self, context, image_location): + """ rpc call to register a new image based from a manifest """ + image_id = utils.generate_uid('ami') + self._conn(context).make_request( + method='PUT', + bucket='_images', + query_args=self._qs({'image_location': image_location, + 'image_id': image_id})) + return image_id + + def index(self, context): + """Return a list of all images that a user can see.""" + response = self._conn(context).make_request( + method='GET', + bucket='_images') + return json.loads(response.read()) + + def show(self, context, image_id): + """return a image object if the context has permissions""" + if FLAGS.connection_type == 'fake': + return {'imageId': 'bar'} + result = self.index(context) + result = [i for i in result if i['imageId'] == image_id] + if not result: + raise exception.NotFound('Image %s could not be found' % image_id) + image = result[0] + return image + + def deregister(self, context, image_id): + """ unregister an image """ + self._conn(context).make_request( + method='DELETE', + bucket='_images', + query_args=self._qs({'image_id': image_id})) + + def _conn(self, context): + access = manager.AuthManager().get_access_key(context.user, + context.project) + secret = str(context.user.secret) + calling = boto.s3.connection.OrdinaryCallingFormat() + return boto.s3.connection.S3Connection(aws_access_key_id=access, + aws_secret_access_key=secret, + is_secure=False, + calling_format=calling, + port=FLAGS.s3_port, + host=FLAGS.s3_host) + + def _qs(self, params): + pairs = [] + for key in params.keys(): + pairs.append(key + '=' + urllib.quote(params[key])) + return '&'.join(pairs) diff --git a/nova/image/service.py b/nova/image/service.py index 52ddd4e0f..ebee2228d 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -15,32 +15,12 @@ # License for the specific language governing permissions and limitations # under the License. -import cPickle as pickle -import os.path -import random - -from nova import flags -from nova import exception - -FLAGS = flags.FLAGS - - -flags.DEFINE_string('glance_teller_address', 'http://127.0.0.1', - 'IP address or URL where Glance\'s Teller service resides') -flags.DEFINE_string('glance_teller_port', '9191', - 'Port for Glance\'s Teller service') -flags.DEFINE_string('glance_parallax_address', 'http://127.0.0.1', - 'IP address or URL where Glance\'s Parallax service ' - 'resides') -flags.DEFINE_string('glance_parallax_port', '9292', - 'Port for Glance\'s Parallax service') - class BaseImageService(object): """Base class for providing image search and retrieval services""" - def index(self): + def index(self, context): """ Returns a sequence of mappings of id and name information about images. @@ -52,7 +32,7 @@ class BaseImageService(object): """ raise NotImplementedError - def detail(self): + def detail(self, context): """ Returns a sequence of mappings of detailed information about images. @@ -76,7 +56,7 @@ class BaseImageService(object): """ raise NotImplementedError - def show(self, id): + def show(self, context, id): """ Returns a dict containing image data for the given opaque image id. @@ -96,7 +76,7 @@ class BaseImageService(object): """ raise NotImplementedError - def create(self, data): + def create(self, context, data): """ Store the image data and return the new image id. @@ -105,7 +85,7 @@ class BaseImageService(object): """ raise NotImplementedError - def update(self, image_id, data): + def update(self, context, image_id, data): """Replace the contents of the given image with the new data. :raises NotFound if the image does not exist. @@ -113,7 +93,7 @@ class BaseImageService(object): """ raise NotImplementedError - def delete(self, image_id): + def delete(self, context, image_id): """ Delete the given image. @@ -121,68 +101,3 @@ class BaseImageService(object): """ raise NotImplementedError - - -class LocalImageService(BaseImageService): - - """Image service storing images to local disk. - - It assumes that image_ids are integers.""" - - def __init__(self): - self._path = "/tmp/nova/images" - try: - os.makedirs(self._path) - except OSError: # Exists - pass - - def _path_to(self, image_id): - return os.path.join(self._path, str(image_id)) - - def _ids(self): - """The list of all image ids.""" - return [int(i) for i in os.listdir(self._path)] - - def index(self): - return [dict(id=i['id'], name=i['name']) for i in self.detail()] - - def detail(self): - return [self.show(id) for id in self._ids()] - - def show(self, id): - try: - return pickle.load(open(self._path_to(id))) - except IOError: - raise exception.NotFound - - def create(self, data): - """ - Store the image data and return the new image id. - """ - id = random.randint(0, 2 ** 32 - 1) - data['id'] = id - self.update(id, data) - return id - - def update(self, image_id, data): - """Replace the contents of the given image with the new data.""" - try: - pickle.dump(data, open(self._path_to(image_id), 'w')) - except IOError: - raise exception.NotFound - - def delete(self, image_id): - """ - Delete the given image. Raises OSError if the image does not exist. - """ - try: - os.unlink(self._path_to(image_id)) - except IOError: - raise exception.NotFound - - def delete_all(self): - """ - Clears out all images in local directory - """ - for id in self._ids(): - os.unlink(self._path_to(id)) diff --git a/nova/image/services/__init__.py b/nova/image/services/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nova/image/services/glance/__init__.py b/nova/image/services/glance/__init__.py deleted file mode 100644 index f1d05f0bc..000000000 --- a/nova/image/services/glance/__init__.py +++ /dev/null @@ -1,216 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Implementation of an image service that uses Glance as the backend""" - -import httplib -import json -import urlparse - -import webob.exc - -from nova import utils -from nova import flags -from nova import exception -import nova.image.service - -FLAGS = flags.FLAGS - - -class TellerClient(object): - - def __init__(self): - self.address = FLAGS.glance_teller_address - self.port = FLAGS.glance_teller_port - url = urlparse.urlparse(self.address) - self.netloc = url.netloc - self.connection_type = {'http': httplib.HTTPConnection, - 'https': httplib.HTTPSConnection}[url.scheme] - - -class ParallaxClient(object): - - def __init__(self): - self.address = FLAGS.glance_parallax_address - self.port = FLAGS.glance_parallax_port - url = urlparse.urlparse(self.address) - self.netloc = url.netloc - self.connection_type = {'http': httplib.HTTPConnection, - 'https': httplib.HTTPSConnection}[url.scheme] - - def get_image_index(self): - """ - Returns a list of image id/name mappings from Parallax - """ - try: - c = self.connection_type(self.netloc, self.port) - c.request("GET", "images") - res = c.getresponse() - if res.status == 200: - # Parallax returns a JSONified dict(images=image_list) - data = json.loads(res.read())['images'] - return data - else: - logging.warn("Parallax returned HTTP error %d from " - "request for /images", res.status_int) - return [] - finally: - c.close() - - def get_image_details(self): - """ - Returns a list of detailed image data mappings from Parallax - """ - try: - c = self.connection_type(self.netloc, self.port) - c.request("GET", "images/detail") - res = c.getresponse() - if res.status == 200: - # Parallax returns a JSONified dict(images=image_list) - data = json.loads(res.read())['images'] - return data - else: - logging.warn("Parallax returned HTTP error %d from " - "request for /images/detail", res.status_int) - return [] - finally: - c.close() - - def get_image_metadata(self, image_id): - """ - Returns a mapping of image metadata from Parallax - """ - try: - c = self.connection_type(self.netloc, self.port) - c.request("GET", "images/%s" % image_id) - res = c.getresponse() - if res.status == 200: - # Parallax returns a JSONified dict(image=image_info) - data = json.loads(res.read())['image'] - return data - else: - # TODO(jaypipes): log the error? - return None - finally: - c.close() - - def add_image_metadata(self, image_metadata): - """ - Tells parallax about an image's metadata - """ - try: - c = self.connection_type(self.netloc, self.port) - body = json.dumps(image_metadata) - c.request("POST", "images", body) - res = c.getresponse() - if res.status == 200: - # Parallax returns a JSONified dict(image=image_info) - data = json.loads(res.read())['image'] - return data['id'] - else: - # TODO(jaypipes): log the error? - return None - finally: - c.close() - - def update_image_metadata(self, image_id, image_metadata): - """ - Updates Parallax's information about an image - """ - try: - c = self.connection_type(self.netloc, self.port) - body = json.dumps(image_metadata) - c.request("PUT", "images/%s" % image_id, body) - res = c.getresponse() - return res.status == 200 - finally: - c.close() - - def delete_image_metadata(self, image_id): - """ - Deletes Parallax's information about an image - """ - try: - c = self.connection_type(self.netloc, self.port) - c.request("DELETE", "images/%s" % image_id) - res = c.getresponse() - return res.status == 200 - finally: - c.close() - - -class GlanceImageService(nova.image.service.BaseImageService): - """Provides storage and retrieval of disk image objects within Glance.""" - - def __init__(self): - self.teller = TellerClient() - self.parallax = ParallaxClient() - - def index(self): - """ - Calls out to Parallax for a list of images available - """ - images = self.parallax.get_image_index() - return images - - def detail(self): - """ - Calls out to Parallax for a list of detailed image information - """ - images = self.parallax.get_image_details() - return images - - def show(self, id): - """ - Returns a dict containing image data for the given opaque image id. - """ - image = self.parallax.get_image_metadata(id) - if image: - return image - raise exception.NotFound - - def create(self, data): - """ - Store the image data and return the new image id. - - :raises AlreadyExists if the image already exist. - - """ - return self.parallax.add_image_metadata(data) - - def update(self, image_id, data): - """Replace the contents of the given image with the new data. - - :raises NotFound if the image does not exist. - - """ - self.parallax.update_image_metadata(image_id, data) - - def delete(self, image_id): - """ - Delete the given image. - - :raises NotFound if the image does not exist. - - """ - self.parallax.delete_image_metadata(image_id) - - def delete_all(self): - """ - Clears out all images - """ - pass diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 52b392601..639a2ebe4 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -29,7 +29,7 @@ from nova import flags from nova import exception as exc import nova.api.openstack.auth from nova.image import service -from nova.image.services import glance +from nova.image import glance from nova.tests import fake_flags from nova.wsgi import Router @@ -76,7 +76,7 @@ def stub_out_image_service(stubs): def fake_image_show(meh, id): return dict(kernelId=1, ramdiskId=1) - stubs.Set(nova.image.service.LocalImageService, 'show', fake_image_show) + stubs.Set(nova.image.local.LocalImageService, 'show', fake_image_show) def stub_out_auth(stubs): @@ -151,21 +151,19 @@ def stub_out_glance(stubs, initial_fixtures=[]): self.fixtures = [] fake_parallax_client = FakeParallaxClient(initial_fixtures) - stubs.Set(nova.image.services.glance.ParallaxClient, 'get_image_index', + stubs.Set(nova.image.glance.ParallaxClient, 'get_image_index', fake_parallax_client.fake_get_image_index) - stubs.Set(nova.image.services.glance.ParallaxClient, 'get_image_details', + stubs.Set(nova.image.glance.ParallaxClient, 'get_image_details', fake_parallax_client.fake_get_image_details) - stubs.Set(nova.image.services.glance.ParallaxClient, 'get_image_metadata', + stubs.Set(nova.image.glance.ParallaxClient, 'get_image_metadata', fake_parallax_client.fake_get_image_metadata) - stubs.Set(nova.image.services.glance.ParallaxClient, 'add_image_metadata', + stubs.Set(nova.image.glance.ParallaxClient, 'add_image_metadata', fake_parallax_client.fake_add_image_metadata) - stubs.Set(nova.image.services.glance.ParallaxClient, - 'update_image_metadata', + stubs.Set(nova.image.glance.ParallaxClient, 'update_image_metadata', fake_parallax_client.fake_update_image_metadata) - stubs.Set(nova.image.services.glance.ParallaxClient, - 'delete_image_metadata', + stubs.Set(nova.image.glance.ParallaxClient, 'delete_image_metadata', fake_parallax_client.fake_delete_image_metadata) - stubs.Set(nova.image.services.glance.GlanceImageService, 'delete_all', + stubs.Set(nova.image.glance.GlanceImageService, 'delete_all', fake_parallax_client.fake_delete_all) diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 0f3941c29..207947b3c 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -28,6 +28,7 @@ import unittest import stubout import webob +from nova import context from nova import exception from nova import flags from nova import utils @@ -52,12 +53,12 @@ class BaseImageServiceTests(object): 'serverId': None, 'progress': None} - num_images = len(self.service.index()) + num_images = len(self.service.index(self.context)) - id = self.service.create(fixture) + id = self.service.create(self.context, fixture) self.assertNotEquals(None, id) - self.assertEquals(num_images + 1, len(self.service.index())) + self.assertEquals(num_images + 1, len(self.service.index(self.context))) def test_create_and_show_non_existing_image(self): @@ -68,14 +69,15 @@ class BaseImageServiceTests(object): 'serverId': None, 'progress': None} - num_images = len(self.service.index()) + num_images = len(self.service.index(self.context)) - id = self.service.create(fixture) + id = self.service.create(self.context, fixture) self.assertNotEquals(None, id) self.assertRaises(exception.NotFound, self.service.show, + self.context, 'bad image id') def test_update(self): @@ -87,12 +89,12 @@ class BaseImageServiceTests(object): 'serverId': None, 'progress': None} - id = self.service.create(fixture) + id = self.service.create(self.context, fixture) fixture['status'] = 'in progress' - self.service.update(id, fixture) - new_image_data = self.service.show(id) + self.service.update(self.context, id, fixture) + new_image_data = self.service.show(self.context, id) self.assertEquals('in progress', new_image_data['status']) def test_delete(self): @@ -111,20 +113,20 @@ class BaseImageServiceTests(object): 'serverId': None, 'progress': None}] - num_images = len(self.service.index()) - self.assertEquals(0, num_images, str(self.service.index())) + num_images = len(self.service.index(self.context)) + self.assertEquals(0, num_images, str(self.service.index(self.context))) ids = [] for fixture in fixtures: - new_id = self.service.create(fixture) + new_id = self.service.create(self.context, fixture) ids.append(new_id) - num_images = len(self.service.index()) - self.assertEquals(2, num_images, str(self.service.index())) + num_images = len(self.service.index(self.context)) + self.assertEquals(2, num_images, str(self.service.index(self.context))) - self.service.delete(ids[0]) + self.service.delete(self.context, ids[0]) - num_images = len(self.service.index()) + num_images = len(self.service.index(self.context)) self.assertEquals(1, num_images) @@ -135,8 +137,9 @@ class LocalImageServiceTest(unittest.TestCase, def setUp(self): self.stubs = stubout.StubOutForTesting() - service_class = 'nova.image.service.LocalImageService' + service_class = 'nova.image.local.LocalImageService' self.service = utils.import_object(service_class) + self.context = context.RequestContext(None, None) def tearDown(self): self.service.delete_all() @@ -151,8 +154,9 @@ class GlanceImageServiceTest(unittest.TestCase, def setUp(self): self.stubs = stubout.StubOutForTesting() fakes.stub_out_glance(self.stubs) - service_class = 'nova.image.services.glance.GlanceImageService' + service_class = 'nova.image.glance.GlanceImageService' self.service = utils.import_object(service_class) + self.context = context.RequestContext(None, None) self.service.delete_all() def tearDown(self): @@ -187,7 +191,7 @@ class ImageControllerWithGlanceServiceTest(unittest.TestCase): def setUp(self): self.orig_image_service = FLAGS.image_service - FLAGS.image_service = 'nova.image.services.glance.GlanceImageService' + FLAGS.image_service = 'nova.image.glance.GlanceImageService' self.stubs = stubout.StubOutForTesting() fakes.FakeAuthManager.auth_data = {} fakes.FakeAuthDatabase.data = {} -- cgit From 9c7ddf24acfdbdb220bcc56d8e4d6421cd46e1d7 Mon Sep 17 00:00:00 2001 From: Eric Day Date: Thu, 18 Nov 2010 21:27:00 -0800 Subject: PEP8 fixes, 2 lines were too long. --- nova/image/s3.py | 3 ++- nova/tests/api/openstack/test_images.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/image/s3.py b/nova/image/s3.py index ba1086aca..0a25161de 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -42,7 +42,8 @@ class S3ImageService(service.BaseImageService): self._conn(context).make_request( method='POST', bucket='_images', - query_args=self._qs({'image_id': image_id, 'operation': operation})) + query_args=self._qs({'image_id': image_id, + 'operation': operation})) return True def update(self, context, image_id, attributes): diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 207947b3c..f610cbf9c 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -58,7 +58,8 @@ class BaseImageServiceTests(object): id = self.service.create(self.context, fixture) self.assertNotEquals(None, id) - self.assertEquals(num_images + 1, len(self.service.index(self.context))) + self.assertEquals(num_images + 1, + len(self.service.index(self.context))) def test_create_and_show_non_existing_image(self): -- cgit From aa433547ff797678cd2aad17d70c1c0b569d1e37 Mon Sep 17 00:00:00 2001 From: Armando Migliaccio Date: Fri, 19 Nov 2010 17:32:38 +0000 Subject: first cut of fixes for bug #676128 --- nova/virt/xenapi.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'nova') diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py index 0f563aa41..2f115c14b 100644 --- a/nova/virt/xenapi.py +++ b/nova/virt/xenapi.py @@ -286,11 +286,21 @@ class XenAPIConnection(object): # Don't complain, just return. This lets us clean up instances # that have already disappeared from the underlying platform. defer.returnValue(None) + # Get the VDIs related to the VM + vdis = yield self._lookup_vm_vdis(vm) try: task = yield self._call_xenapi('Async.VM.hard_shutdown', vm) yield self._wait_for_task(task) except Exception, exc: logging.warn(exc) + # Disk clean-up + if vdis: + for vdi in vdis: + try: + task = yield self._call_xenapi('Async.VDI.destroy', vdi) + yield self._wait_for_task(task) + except Exception, exc: + logging.warn(exc) try: task = yield self._call_xenapi('Async.VM.destroy', vm) yield self._wait_for_task(task) @@ -325,6 +335,24 @@ class XenAPIConnection(object): else: return vms[0] + @utils.deferredToThread + def _lookup_vm_vdis(self, vm): + return self._lookup_vm_vdis_blocking(vm) + + def _lookup_vm_vdis_blocking(self, vm): + # Firstly we get the VBDs, then the VDIs. + # TODO: do we leave the read-only devices? + vbds = self._conn.xenapi.VM.get_VBDs(vm) + vdis = [] + if vbds: + for vbd in vbds: + vdis.append(self._conn.xenapi.VBD.get_VDI(vbd)) + if len(vdis) > 0: + return vdis + else: + return None + + def _wait_for_task(self, task): """Return a Deferred that will give the result of the given task. The task is polled until it completes.""" -- cgit From 300e6e3517d0e50c7fd6775eff10f1b0d677f25a Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 19 Nov 2010 15:47:24 -0600 Subject: Check for running AMQP instances --- nova/rpc.py | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) (limited to 'nova') diff --git a/nova/rpc.py b/nova/rpc.py index ea36d69f4..0c79c7f76 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -24,6 +24,7 @@ No fan-out support yet. import json import logging import sys +import time import uuid from carrot import connection as carrot_connection @@ -82,31 +83,35 @@ class Consumer(messaging.Consumer): Contains methods for connecting the fetch method to async loops """ def __init__(self, *args, **kwargs): - self.failed_connection = False - super(Consumer, self).__init__(*args, **kwargs) + while True: + try: + super(Consumer, self).__init__(*args, **kwargs) + break + except: + logging.warning("AMQP server on %s:%d is unreachable. " \ + "Trying again in 30 seconds." % ( + FLAGS.rabbit_host, + FLAGS.rabbit_port + )) + time.sleep(30) + continue def fetch(self, no_ack=None, auto_ack=None, enable_callbacks=False): """Wraps the parent fetch with some logic for failed connections""" - # TODO(vish): the logic for failed connections and logging should be - # refactored into some sort of connection manager object try: - if self.failed_connection: - # NOTE(vish): conn is defined in the parent class, we can - # recreate it as long as we create the backend too - # pylint: disable-msg=W0201 - self.conn = Connection.recreate() - self.backend = self.conn.create_backend() super(Consumer, self).fetch(no_ack, auto_ack, enable_callbacks) - if self.failed_connection: - logging.error("Reconnected to queue") - self.failed_connection = False - # NOTE(vish): This is catching all errors because we really don't - # exceptions to be logged 10 times a second if some - # persistent failure occurs. - except Exception: # pylint: disable-msg=W0703 - if not self.failed_connection: - logging.exception("Failed to fetch message from queue") - self.failed_connection = True + except: + try: + self.connection = Connection.recreate() + self.backend = self.connection.create_backend() + self.declare() + except: + logging.warning("AMQP server on %s:%d is unreachable. " \ + "Trying again in 30 seconds." % ( + FLAGS.rabbit_host, + FLAGS.rabbit_port + )) + time.sleep(30) def attach_to_eventlet(self): """Only needed for unit tests!""" -- cgit From 281008b1ce3f92507622d856e0a310690ea37ab3 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 19 Nov 2010 16:01:55 -0600 Subject: Added some comments --- nova/rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/rpc.py b/nova/rpc.py index 0c79c7f76..d9097afaa 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -100,12 +100,12 @@ class Consumer(messaging.Consumer): """Wraps the parent fetch with some logic for failed connections""" try: super(Consumer, self).fetch(no_ack, auto_ack, enable_callbacks) - except: + except: # Catching all because carrot sucks try: self.connection = Connection.recreate() self.backend = self.connection.create_backend() self.declare() - except: + except: # Catching all because carrot sucks logging.warning("AMQP server on %s:%d is unreachable. " \ "Trying again in 30 seconds." % ( FLAGS.rabbit_host, -- cgit From aa9fe71ed82889f1f3ca306c5836414966d08539 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 19 Nov 2010 16:13:04 -0600 Subject: Added some comments --- nova/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/rpc.py b/nova/rpc.py index d9097afaa..cf3889a47 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -87,7 +87,7 @@ class Consumer(messaging.Consumer): try: super(Consumer, self).__init__(*args, **kwargs) break - except: + except: # Catching all because carrot sucks logging.warning("AMQP server on %s:%d is unreachable. " \ "Trying again in 30 seconds." % ( FLAGS.rabbit_host, -- cgit From dec8aab1001d85ff7845f7521faf1f610700bd09 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Fri, 19 Nov 2010 16:42:37 -0600 Subject: Reverted some changes --- nova/rpc.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) (limited to 'nova') diff --git a/nova/rpc.py b/nova/rpc.py index cf3889a47..c7ff9ce6b 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -83,35 +83,43 @@ class Consumer(messaging.Consumer): Contains methods for connecting the fetch method to async loops """ def __init__(self, *args, **kwargs): + self.failed_connection = False + while True: try: super(Consumer, self).__init__(*args, **kwargs) break - except: # Catching all because carrot sucks + except: # Catching all because carrot sucks logging.warning("AMQP server on %s:%d is unreachable. " \ "Trying again in 30 seconds." % ( - FLAGS.rabbit_host, - FLAGS.rabbit_port - )) + FLAGS.rabbit_host, + FLAGS.rabbit_port)) time.sleep(30) continue def fetch(self, no_ack=None, auto_ack=None, enable_callbacks=False): """Wraps the parent fetch with some logic for failed connections""" + # TODO(vish): the logic for failed connections and logging should be + # refactored into some sort of connection manager object try: - super(Consumer, self).fetch(no_ack, auto_ack, enable_callbacks) - except: # Catching all because carrot sucks - try: + if self.failed_connection: + # NOTE(vish): conn is defined in the parent class, we can + # recreate it as long as we create the backend too + # pylint: disable-msg=W0201 self.connection = Connection.recreate() self.backend = self.connection.create_backend() self.declare() - except: # Catching all because carrot sucks - logging.warning("AMQP server on %s:%d is unreachable. " \ - "Trying again in 30 seconds." % ( - FLAGS.rabbit_host, - FLAGS.rabbit_port - )) - time.sleep(30) + super(Consumer, self).fetch(no_ack, auto_ack, enable_callbacks) + if self.failed_connection: + logging.error("Reconnected to queue") + self.failed_connection = False + # NOTE(vish): This is catching all errors because we really don't + # exceptions to be logged 10 times a second if some + # persistent failure occurs. + except Exception: # pylint: disable-msg=W0703 + if not self.failed_connection: + logging.exception("Failed to fetch message from queue") + self.failed_connection = True def attach_to_eventlet(self): """Only needed for unit tests!""" -- cgit From 874edc5103e1ebbe1def1639ef056574dcd406e9 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Sat, 20 Nov 2010 12:16:54 -0600 Subject: Use logging.exception instead --- nova/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/rpc.py b/nova/rpc.py index c7ff9ce6b..9938b0838 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -90,7 +90,7 @@ class Consumer(messaging.Consumer): super(Consumer, self).__init__(*args, **kwargs) break except: # Catching all because carrot sucks - logging.warning("AMQP server on %s:%d is unreachable. " \ + logging.exception("AMQP server on %s:%d is unreachable. " \ "Trying again in 30 seconds." % ( FLAGS.rabbit_host, FLAGS.rabbit_port)) -- cgit From 958591ab2996443ffb6d2f92f928eaad277aa2db Mon Sep 17 00:00:00 2001 From: Armando Migliaccio Date: Mon, 22 Nov 2010 13:11:00 +0000 Subject: pep8 violations fix --- nova/virt/xenapi.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) (limited to 'nova') diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py index c3a0c2c7a..3169562a5 100644 --- a/nova/virt/xenapi.py +++ b/nova/virt/xenapi.py @@ -338,28 +338,27 @@ class XenAPIConnection(object): @utils.deferredToThread def _lookup_vm_vdis(self, vm): return self._lookup_vm_vdis_blocking(vm) - + def _lookup_vm_vdis_blocking(self, vm): # Firstly we get the VBDs, then the VDIs. # TODO: do we leave the read-only devices? vbds = self._conn.xenapi.VM.get_VBDs(vm) vdis = [] if vbds: - for vbd in vbds: - try: - vdi = self._conn.xenapi.VBD.get_VDI(vbd) - # Test valid VDI - record = self._conn.xenapi.VDI.get_record(vdi) - except Exception, exc: - logging.warn(exc) - else: - vdis.append(vdi) - if len(vdis) > 0: - return vdis - else: - return None - - + for vbd in vbds: + try: + vdi = self._conn.xenapi.VBD.get_VDI(vbd) + # Test valid VDI + record = self._conn.xenapi.VDI.get_record(vdi) + except Exception, exc: + logging.warn(exc) + else: + vdis.append(vdi) + if len(vdis) > 0: + return vdis + else: + return None + def _wait_for_task(self, task): """Return a Deferred that will give the result of the given task. The task is polled until it completes.""" -- cgit From 51be7159574d3e0cba8a81b8ea3e9706ce74ac3a Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Mon, 22 Nov 2010 14:45:05 -0600 Subject: Set and use AMQP retry interval and max retry constants --- nova/rpc.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) (limited to 'nova') diff --git a/nova/rpc.py b/nova/rpc.py index 9938b0838..c2d18cb61 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -38,8 +38,11 @@ from nova import fakerabbit from nova import flags from nova import context + FLAGS = flags.FLAGS +AMQP_RETRY_INT = 10 +AMQP_MAX_RETRIES = 12 LOG = logging.getLogger('amqplib') LOG.setLevel(logging.DEBUG) @@ -85,17 +88,23 @@ class Consumer(messaging.Consumer): def __init__(self, *args, **kwargs): self.failed_connection = False - while True: + for i in range(AMQP_MAX_RETRIES): try: super(Consumer, self).__init__(*args, **kwargs) break except: # Catching all because carrot sucks - logging.exception("AMQP server on %s:%d is unreachable. " \ - "Trying again in 30 seconds." % ( - FLAGS.rabbit_host, - FLAGS.rabbit_port)) - time.sleep(30) - continue + if i + 1 == AMQP_MAX_RETRIES: + logging.exception("Unable to connect to AMQP server" \ + " after %d tries. Shutting down." % AMQP_MAX_RETRIES) + sys.exit(1) + else: + logging.exception("AMQP server on %s:%d is unreachable." \ + " Trying again in %d seconds." % ( + FLAGS.rabbit_host, + FLAGS.rabbit_port, + AMQP_RETRY_INT)) + time.sleep(AMQP_RETRY_INT) + continue def fetch(self, no_ack=None, auto_ack=None, enable_callbacks=False): """Wraps the parent fetch with some logic for failed connections""" @@ -103,7 +112,7 @@ class Consumer(messaging.Consumer): # refactored into some sort of connection manager object try: if self.failed_connection: - # NOTE(vish): conn is defined in the parent class, we can + # NOTE(vish): connection is defined in the parent class, we can # recreate it as long as we create the backend too # pylint: disable-msg=W0201 self.connection = Connection.recreate() -- cgit From 14e4ba7f0e10fc3c2f532b445c1f656f53c8aa95 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Mon, 22 Nov 2010 15:22:02 -0600 Subject: Refactor AMQP retry loop --- nova/rpc.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) (limited to 'nova') diff --git a/nova/rpc.py b/nova/rpc.py index c2d18cb61..961b56de6 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -86,25 +86,25 @@ class Consumer(messaging.Consumer): Contains methods for connecting the fetch method to async loops """ def __init__(self, *args, **kwargs): - self.failed_connection = False - - for i in range(AMQP_MAX_RETRIES): + for i in xrange(AMQP_MAX_RETRIES): + if i > 0: + time.sleep(AMQP_RETRY_INT) try: super(Consumer, self).__init__(*args, **kwargs) + self.failed_connection = False break except: # Catching all because carrot sucks - if i + 1 == AMQP_MAX_RETRIES: - logging.exception("Unable to connect to AMQP server" \ - " after %d tries. Shutting down." % AMQP_MAX_RETRIES) - sys.exit(1) - else: - logging.exception("AMQP server on %s:%d is unreachable." \ - " Trying again in %d seconds." % ( - FLAGS.rabbit_host, - FLAGS.rabbit_port, - AMQP_RETRY_INT)) - time.sleep(AMQP_RETRY_INT) - continue + logging.exception("AMQP server on %s:%d is unreachable." \ + " Trying again in %d seconds." % ( + FLAGS.rabbit_host, + FLAGS.rabbit_port, + AMQP_RETRY_INT)) + self.failed_connection = True + continue + if self.failed_connection: + logging.exception("Unable to connect to AMQP server" \ + " after %d tries. Shutting down." % AMQP_MAX_RETRIES) + sys.exit(1) def fetch(self, no_ack=None, auto_ack=None, enable_callbacks=False): """Wraps the parent fetch with some logic for failed connections""" -- cgit From a8497abaf24436a92a85129d9771a12f046f2f42 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Mon, 22 Nov 2010 16:04:21 -0600 Subject: Removed unnecessary continue --- nova/rpc.py | 1 - 1 file changed, 1 deletion(-) (limited to 'nova') diff --git a/nova/rpc.py b/nova/rpc.py index 961b56de6..57e522ad3 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -100,7 +100,6 @@ class Consumer(messaging.Consumer): FLAGS.rabbit_port, AMQP_RETRY_INT)) self.failed_connection = True - continue if self.failed_connection: logging.exception("Unable to connect to AMQP server" \ " after %d tries. Shutting down." % AMQP_MAX_RETRIES) -- cgit From deac609ceb1cd6e081445bfc4d8f8c3222b97774 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Mon, 22 Nov 2010 16:28:28 -0600 Subject: Make time.sleep() non-blocking --- nova/wsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/wsgi.py b/nova/wsgi.py index b04b487ea..c7ee9ed14 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -28,7 +28,7 @@ from xml.dom import minidom import eventlet import eventlet.wsgi -eventlet.patcher.monkey_patch(all=False, socket=True) +eventlet.patcher.monkey_patch(all=False, socket=True, time=True) import routes import routes.middleware import webob -- cgit From f0f990495428c028401ba9a4740e6b7a0441213c Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Mon, 22 Nov 2010 16:48:44 -0600 Subject: Use FLAGS instead of constants --- nova/flags.py | 2 ++ nova/rpc.py | 11 ++++------- 2 files changed, 6 insertions(+), 7 deletions(-) (limited to 'nova') diff --git a/nova/flags.py b/nova/flags.py index 4ae86d9b2..a39f22273 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -196,6 +196,8 @@ DEFINE_integer('rabbit_port', 5672, 'rabbit port') DEFINE_string('rabbit_userid', 'guest', 'rabbit userid') DEFINE_string('rabbit_password', 'guest', 'rabbit password') DEFINE_string('rabbit_virtual_host', '/', 'rabbit virtual host') +DEFINE_integer('rabbit_retry_interval', 10, 'rabbit connection retry interval') +DEFINE_integer('rabbit_max_retries', 12, 'rabbit connection attempts') DEFINE_string('control_exchange', 'nova', 'the main exchange to connect to') DEFINE_string('cc_host', '127.0.0.1', 'ip of api server') DEFINE_integer('cc_port', 8773, 'cloud controller port') diff --git a/nova/rpc.py b/nova/rpc.py index 57e522ad3..86a29574f 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -41,9 +41,6 @@ from nova import context FLAGS = flags.FLAGS -AMQP_RETRY_INT = 10 -AMQP_MAX_RETRIES = 12 - LOG = logging.getLogger('amqplib') LOG.setLevel(logging.DEBUG) @@ -86,9 +83,9 @@ class Consumer(messaging.Consumer): Contains methods for connecting the fetch method to async loops """ def __init__(self, *args, **kwargs): - for i in xrange(AMQP_MAX_RETRIES): + for i in xrange(FLAGS.rabbit_max_retries): if i > 0: - time.sleep(AMQP_RETRY_INT) + time.sleep(FLAGS.rabbit_retry_interval) try: super(Consumer, self).__init__(*args, **kwargs) self.failed_connection = False @@ -98,11 +95,11 @@ class Consumer(messaging.Consumer): " Trying again in %d seconds." % ( FLAGS.rabbit_host, FLAGS.rabbit_port, - AMQP_RETRY_INT)) + FLAGS.rabbit_retry_interval)) self.failed_connection = True if self.failed_connection: logging.exception("Unable to connect to AMQP server" \ - " after %d tries. Shutting down." % AMQP_MAX_RETRIES) + " after %d tries. Shutting down." % FLAGS.rabbit_max_retries) sys.exit(1) def fetch(self, no_ack=None, auto_ack=None, enable_callbacks=False): -- cgit From 89f56207de1ffe2f1f9d5c3cad3ab71ba324d133 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Tue, 23 Nov 2010 13:48:57 +0100 Subject: Add a --logdir flag that will be prepended to the logfile setting. This makes it easier to share a flagfile between multiple workers while still having separate log files. --- nova/server.py | 3 +++ nova/twistd.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'nova') diff --git a/nova/server.py b/nova/server.py index cb424caa1..4d0f6e4da 100644 --- a/nova/server.py +++ b/nova/server.py @@ -42,6 +42,7 @@ flags.DEFINE_bool('daemonize', False, 'daemonize this process') # clutter. flags.DEFINE_bool('use_syslog', True, 'output to syslog when daemonizing') flags.DEFINE_string('logfile', None, 'log file to output to') +flags.DEFINE_string('logdir', None, 'directory to keep log files in (will be prepended to $logfile)') flags.DEFINE_string('pidfile', None, 'pid file to output to') flags.DEFINE_string('working_directory', './', 'working directory...') flags.DEFINE_integer('uid', os.getuid(), 'uid under which to run') @@ -119,6 +120,8 @@ def daemonize(args, name, main): else: if not FLAGS.logfile: FLAGS.logfile = '%s.log' % name + if FLAGS.logdir: + FLAGS.logfile = os.path.join(FLAGS.logdir, FLAGS.logfile) logfile = logging.FileHandler(FLAGS.logfile) logfile.setFormatter(formatter) logger.addHandler(logfile) diff --git a/nova/twistd.py b/nova/twistd.py index 3ec0ff61e..6c6cb955f 100644 --- a/nova/twistd.py +++ b/nova/twistd.py @@ -43,7 +43,7 @@ else: FLAGS = flags.FLAGS - +flags.DEFINE_string('logdir', None, 'directory to keep log files in (will be prepended to $logfile)') class TwistdServerOptions(ServerOptions): def parseArgs(self, *args): @@ -246,6 +246,8 @@ def serve(filename): FLAGS.logfile = '%s.log' % name elif FLAGS.logfile.endswith('twistd.log'): FLAGS.logfile = FLAGS.logfile.replace('twistd.log', '%s.log' % name) + if FLAGS.logdir: + FLAGS.logfile = os.path.join(FLAGS.logdir, FLAGS.logfile) if not FLAGS.prefix: FLAGS.prefix = name elif FLAGS.prefix.endswith('twisted'): -- cgit From 513e4eb76a8d21108484bbc08e3ff755190cb2d9 Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Tue, 23 Nov 2010 12:04:34 -0600 Subject: Make aws_access_key_id and aws_secret_access_key configurable --- nova/adminclient.py | 16 +++++++++++----- nova/compute/monitor.py | 4 ++-- nova/flags.py | 2 ++ 3 files changed, 15 insertions(+), 7 deletions(-) (limited to 'nova') diff --git a/nova/adminclient.py b/nova/adminclient.py index af55197fc..5a62cce7d 100644 --- a/nova/adminclient.py +++ b/nova/adminclient.py @@ -22,13 +22,15 @@ Nova User API client library. import base64 import boto import httplib + +from nova import flags from boto.ec2.regioninfo import RegionInfo +FLAGS = flags.FLAGS + DEFAULT_CLC_URL = 'http://127.0.0.1:8773' DEFAULT_REGION = 'nova' -DEFAULT_ACCESS_KEY = 'admin' -DEFAULT_SECRET_KEY = 'admin' class UserInfo(object): @@ -192,9 +194,13 @@ class HostInfo(object): class NovaAdminClient(object): - def __init__(self, clc_url=DEFAULT_CLC_URL, region=DEFAULT_REGION, - access_key=DEFAULT_ACCESS_KEY, secret_key=DEFAULT_SECRET_KEY, - **kwargs): + def __init__( + self, + clc_url=DEFAULT_CLC_URL, + region=DEFAULT_REGION, + access_key=FLAGS.aws_access_key_id, + secret_key=FLAGS.aws_secret_access_key, + **kwargs): parts = self.split_clc_url(clc_url) self.clc_url = clc_url diff --git a/nova/compute/monitor.py b/nova/compute/monitor.py index d0154600f..ce45b14f6 100644 --- a/nova/compute/monitor.py +++ b/nova/compute/monitor.py @@ -211,8 +211,8 @@ def store_graph(instance_id, filename): # the response we can make our own client that does the actual # request and hands it off to the response parser. s3 = boto.s3.connection.S3Connection( - aws_access_key_id='admin', - aws_secret_access_key='admin', + aws_access_key_id=FLAGS.aws_access_key_id, + aws_secret_access_key=FLAGS.aws_secret_access_key, is_secure=False, calling_format=boto.s3.connection.OrdinaryCallingFormat(), port=FLAGS.s3_port, diff --git a/nova/flags.py b/nova/flags.py index 121b9ca25..f7ae26050 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -179,6 +179,8 @@ DEFINE_list('region_list', [], 'list of region=url pairs separated by commas') DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') +DEFINE_string('aws_access_key_id', 'admin', 'AWS Access ID') +DEFINE_string('aws_secret_access_key', 'admin', 'AWS Access Key') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', '127.0.0.1', 's3 host') DEFINE_string('compute_topic', 'compute', 'the topic compute nodes listen on') -- cgit From 9c57e5ce37c1f2405fcf7a1ba322946e6d84efeb Mon Sep 17 00:00:00 2001 From: Josh Kearney Date: Tue, 23 Nov 2010 12:46:07 -0600 Subject: Remove FAKE_subdomain from docs --- nova/api/__init__.py | 1 - 1 file changed, 1 deletion(-) (limited to 'nova') diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 7e75445a8..80f9f2109 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -22,7 +22,6 @@ Root WSGI middleware for all API controllers. :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 """ -- cgit From edd7e3ed3bee6c11156569ab13b4eb5b3a1f7152 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Tue, 23 Nov 2010 21:52:00 +0100 Subject: Address PEP8 complaints. --- nova/server.py | 3 ++- nova/twistd.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'nova') diff --git a/nova/server.py b/nova/server.py index 4d0f6e4da..a0ee54681 100644 --- a/nova/server.py +++ b/nova/server.py @@ -42,7 +42,8 @@ flags.DEFINE_bool('daemonize', False, 'daemonize this process') # clutter. flags.DEFINE_bool('use_syslog', True, 'output to syslog when daemonizing') flags.DEFINE_string('logfile', None, 'log file to output to') -flags.DEFINE_string('logdir', None, 'directory to keep log files in (will be prepended to $logfile)') +flags.DEFINE_string('logdir', None, 'directory to keep log files in ' + '(will be prepended to $logfile)') flags.DEFINE_string('pidfile', None, 'pid file to output to') flags.DEFINE_string('working_directory', './', 'working directory...') flags.DEFINE_integer('uid', os.getuid(), 'uid under which to run') diff --git a/nova/twistd.py b/nova/twistd.py index 6c6cb955f..cb5648ce6 100644 --- a/nova/twistd.py +++ b/nova/twistd.py @@ -43,7 +43,9 @@ else: FLAGS = flags.FLAGS -flags.DEFINE_string('logdir', None, 'directory to keep log files in (will be prepended to $logfile)') +flags.DEFINE_string('logdir', None, 'directory to keep log files in ' + '(will be prepended to $logfile)') + class TwistdServerOptions(ServerOptions): def parseArgs(self, *args): -- cgit From 3df7b85265b123080387f1a844e067026410a9bc Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Tue, 23 Nov 2010 21:58:46 +0100 Subject: Address pep8 complaints. --- nova/utils.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'nova') diff --git a/nova/utils.py b/nova/utils.py index 820d5bb8a..142584df8 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -174,6 +174,7 @@ def isotime(at=None): def parse_isotime(timestr): return datetime.datetime.strptime(timestr, TIME_FORMAT) + def parse_mailmap(mailmap='.mailmap'): mapping = {} if os.path.exists(mailmap): @@ -185,6 +186,7 @@ def parse_mailmap(mailmap='.mailmap'): mapping[alias] = canonical_email return mapping + def str_dict_replace(s, mapping): for s1, s2 in mapping.iteritems(): s = s.replace(s1, s2) -- cgit