diff options
-rw-r--r-- | nova/cmd/baremetal_deploy_helper.py | 2 | ||||
-rwxr-xr-x | nova/compute/manager.py | 13 | ||||
-rw-r--r-- | nova/db/sqlalchemy/models.py | 152 | ||||
-rw-r--r-- | nova/objects/instance.py | 54 | ||||
-rw-r--r-- | nova/tests/objects/test_instance.py | 114 | ||||
-rw-r--r-- | nova/tests/virt/libvirt/test_libvirt_config.py | 49 | ||||
-rw-r--r-- | nova/tests/virt/libvirt/test_libvirt_volume.py | 26 | ||||
-rw-r--r-- | nova/tests/virt/xenapi/test_vmops.py | 75 | ||||
-rw-r--r-- | nova/tests/virt/xenapi/test_xenapi.py | 11 | ||||
-rw-r--r-- | nova/virt/libvirt/config.py | 16 | ||||
-rw-r--r-- | nova/virt/libvirt/volume.py | 8 | ||||
-rw-r--r-- | nova/virt/xenapi/fake.py | 8 | ||||
-rw-r--r-- | nova/virt/xenapi/vmops.py | 26 | ||||
-rw-r--r-- | plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec | 1 | ||||
-rwxr-xr-x | plugins/xenserver/xenapi/etc/xapi.d/plugins/console | 80 |
15 files changed, 548 insertions, 87 deletions
diff --git a/nova/cmd/baremetal_deploy_helper.py b/nova/cmd/baremetal_deploy_helper.py index d63d06ee9..539d5b517 100644 --- a/nova/cmd/baremetal_deploy_helper.py +++ b/nova/cmd/baremetal_deploy_helper.py @@ -201,6 +201,7 @@ def deploy(address, port, iqn, lun, image_path, pxe_config_path, login_iscsi(address, port, iqn) try: root_uuid = work_on_disk(dev, root_mb, swap_mb, image_path) + switch_pxe_config(pxe_config_path, root_uuid) except processutils.ProcessExecutionError as err: # Log output if there was a error LOG.error("Cmd : %s" % err.cmd) @@ -208,7 +209,6 @@ def deploy(address, port, iqn, lun, image_path, pxe_config_path, LOG.error("StdErr : %s" % err.stderr) finally: logout_iscsi(address, port, iqn) - switch_pxe_config(pxe_config_path, root_uuid) # Ensure the node started netcat on the port after POST the request. time.sleep(3) notify(address, 10000) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 5c697993b..8364722e4 100755 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -394,8 +394,10 @@ class ComputeManager(manager.SchedulerDependentManager): instance_uuid=instance_uuid) def _get_instances_on_driver(self, context, filters=None): - """Return a list of instance records that match the instances found - on the hypervisor. + """Return a list of instance records for the instances found + on the hypervisor which satisfy the specified filters. If filters=None + return a list of instance records for all the instances found on the + hypervisor. """ if not filters: filters = {} @@ -404,10 +406,6 @@ class ComputeManager(manager.SchedulerDependentManager): filters['uuid'] = driver_uuids local_instances = self.conductor_api.instance_get_all_by_filters( context, filters, columns_to_join=[]) - local_instance_uuids = [inst['uuid'] for inst in local_instances] - for uuid in set(driver_uuids) - set(local_instance_uuids): - LOG.error(_('Instance %(uuid)s found in the hypervisor, but ' - 'not in the database'), locals()) return local_instances except NotImplementedError: pass @@ -422,9 +420,6 @@ class ComputeManager(manager.SchedulerDependentManager): for driver_instance in driver_instances: instance = name_map.get(driver_instance) if not instance: - LOG.error(_('Instance %(driver_instance)s found in the ' - 'hypervisor, but not in the database'), - locals()) continue local_instances.append(instance) return local_instances diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index d12e47246..99a68d2cf 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -21,7 +21,7 @@ SQLAlchemy models for nova data. """ -from sqlalchemy import Column, Integer, BigInteger, String, schema +from sqlalchemy import Column, Index, Integer, BigInteger, String, schema from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import ForeignKey, DateTime, Boolean, Text, Float from sqlalchemy.orm import relationship, backref, object_mapper @@ -60,8 +60,9 @@ class ComputeNode(BASE, NovaBase): """Represents a running compute service on a host.""" __tablename__ = 'compute_nodes' + __table_args__ = () id = Column(Integer, primary_key=True) - service_id = Column(Integer, ForeignKey('services.id'), nullable=True) + service_id = Column(Integer, ForeignKey('services.id'), nullable=False) service = relationship(Service, backref=backref('compute_node'), foreign_keys=service_id, @@ -69,23 +70,23 @@ class ComputeNode(BASE, NovaBase): 'ComputeNode.service_id == Service.id,' 'ComputeNode.deleted == 0)') - vcpus = Column(Integer) - memory_mb = Column(Integer) - local_gb = Column(Integer) - vcpus_used = Column(Integer) - memory_mb_used = Column(Integer) - local_gb_used = Column(Integer) - hypervisor_type = Column(Text) - hypervisor_version = Column(Integer) - hypervisor_hostname = Column(String(255)) + vcpus = Column(Integer, nullable=False) + memory_mb = Column(Integer, nullable=False) + local_gb = Column(Integer, nullable=False) + vcpus_used = Column(Integer, nullable=False) + memory_mb_used = Column(Integer, nullable=False) + local_gb_used = Column(Integer, nullable=False) + hypervisor_type = Column(Text, nullable=False) + hypervisor_version = Column(Integer, nullable=False) + hypervisor_hostname = Column(String(255), nullable=True) # Free Ram, amount of activity (resize, migration, boot, etc) and # the number of running VM's are a good starting point for what's # important when making scheduling decisions. - free_ram_mb = Column(Integer) - free_disk_gb = Column(Integer) - current_workload = Column(Integer) - running_vms = Column(Integer) + free_ram_mb = Column(Integer, nullable=True) + free_disk_gb = Column(Integer, nullable=True) + current_workload = Column(Integer, nullable=True) + running_vms = Column(Integer, nullable=True) # Note(masumotok): Expected Strings example: # @@ -98,17 +99,24 @@ class ComputeNode(BASE, NovaBase): # above, since it is copied from <cpu> tag of getCapabilities() # (See libvirt.virtConnection). cpu_info = Column(Text, nullable=True) - disk_available_least = Column(Integer) + disk_available_least = Column(Integer, nullable=True) class ComputeNodeStat(BASE, NovaBase): """Stats related to the current workload of a compute host that are intended to aid in making scheduler decisions.""" __tablename__ = 'compute_node_stats' + __table_args__ = ( + Index('ix_compute_node_stats_compute_node_id', 'compute_node_id'), + Index('compute_node_stats_node_id_and_deleted_idx', + 'compute_node_id', 'deleted') + ) + id = Column(Integer, primary_key=True) - key = Column(String(511)) - value = Column(String(255)) - compute_node_id = Column(Integer, ForeignKey('compute_nodes.id')) + key = Column(String(511), nullable=False) + value = Column(String(255), nullable=False) + compute_node_id = Column(Integer, ForeignKey('compute_nodes.id'), + nullable=False) primary_join = ('and_(ComputeNodeStat.compute_node_id == ' 'ComputeNode.id, ComputeNodeStat.deleted == 0)') @@ -122,16 +130,34 @@ class ComputeNodeStat(BASE, NovaBase): class Certificate(BASE, NovaBase): """Represents a x509 certificate.""" __tablename__ = 'certificates' + __table_args__ = ( + Index('certificates_project_id_deleted_idx', 'project_id', 'deleted'), + Index('certificates_user_id_deleted_idx', 'user_id', 'deleted') + ) id = Column(Integer, primary_key=True) - user_id = Column(String(255)) - project_id = Column(String(255)) - file_name = Column(String(255)) + user_id = Column(String(255), nullable=True) + project_id = Column(String(255), nullable=True) + file_name = Column(String(255), nullable=True) class Instance(BASE, NovaBase): """Represents a guest VM.""" __tablename__ = 'instances' + __table_args__ = ( + Index('instances_host_deleted_idx', + 'host', 'deleted'), + Index('instances_reservation_id_idx', + 'reservation_id'), + Index('instances_terminated_at_launched_at_idx', + 'terminated_at', 'launched_at'), + Index('instances_uuid_deleted_idx', + 'uuid', 'deleted'), + Index('instances_task_state_updated_at_idx', + 'task_state', 'updated_at'), + Index('instances_host_node_deleted_idx', + 'host', 'node', 'deleted') + ) injected_files = [] id = Column(Integer, primary_key=True, autoincrement=True) @@ -161,87 +187,87 @@ class Instance(BASE, NovaBase): def _extra_keys(self): return ['name'] - user_id = Column(String(255)) - project_id = Column(String(255)) + user_id = Column(String(255), nullable=True) + project_id = Column(String(255), nullable=True) - image_ref = Column(String(255)) - kernel_id = Column(String(255)) - ramdisk_id = Column(String(255)) - hostname = Column(String(255)) + image_ref = Column(String(255), nullable=True) + kernel_id = Column(String(255), nullable=True) + ramdisk_id = Column(String(255), nullable=True) + hostname = Column(String(255), nullable=True) - launch_index = Column(Integer) - key_name = Column(String(255)) + launch_index = Column(Integer, nullable=True) + key_name = Column(String(255), nullable=True) key_data = Column(Text) - power_state = Column(Integer) - vm_state = Column(String(255)) - task_state = Column(String(255)) + power_state = Column(Integer, nullable=True) + vm_state = Column(String(255), nullable=True) + task_state = Column(String(255), nullable=True) - memory_mb = Column(Integer) - vcpus = Column(Integer) - root_gb = Column(Integer) - ephemeral_gb = Column(Integer) + memory_mb = Column(Integer, nullable=True) + vcpus = Column(Integer, nullable=True) + root_gb = Column(Integer, nullable=True) + ephemeral_gb = Column(Integer, nullable=True) # This is not related to hostname, above. It refers # to the nova node. - host = Column(String(255)) # , ForeignKey('hosts.id')) + host = Column(String(255), nullable=True) # , ForeignKey('hosts.id')) # To identify the "ComputeNode" which the instance resides in. # This equals to ComputeNode.hypervisor_hostname. - node = Column(String(255)) + node = Column(String(255), nullable=True) # *not* flavor_id - instance_type_id = Column(Integer) + instance_type_id = Column(Integer, nullable=True) - user_data = Column(Text) + user_data = Column(Text, nullable=True) - reservation_id = Column(String(255)) + reservation_id = Column(String(255), nullable=True) - scheduled_at = Column(DateTime) - launched_at = Column(DateTime) - terminated_at = Column(DateTime) + scheduled_at = Column(DateTime, nullable=True) + launched_at = Column(DateTime, nullable=True) + terminated_at = Column(DateTime, nullable=True) - availability_zone = Column(String(255)) + availability_zone = Column(String(255), nullable=True) # User editable field for display in user-facing UIs - display_name = Column(String(255)) - display_description = Column(String(255)) + display_name = Column(String(255), nullable=True) + display_description = Column(String(255), nullable=True) # To remember on which host an instance booted. # An instance may have moved to another host by live migration. - launched_on = Column(Text) - locked = Column(Boolean) + launched_on = Column(Text, nullable=True) + locked = Column(Boolean, nullable=True) - os_type = Column(String(255)) - architecture = Column(String(255)) - vm_mode = Column(String(255)) - uuid = Column(String(36)) + os_type = Column(String(255), nullable=True) + architecture = Column(String(255), nullable=True) + vm_mode = Column(String(255), nullable=True) + uuid = Column(String(36), unique=True) - root_device_name = Column(String(255)) + root_device_name = Column(String(255), nullable=True) default_ephemeral_device = Column(String(255), nullable=True) default_swap_device = Column(String(255), nullable=True) - config_drive = Column(String(255)) + config_drive = Column(String(255), nullable=True) # User editable field meant to represent what ip should be used # to connect to the instance - access_ip_v4 = Column(types.IPAddress()) - access_ip_v6 = Column(types.IPAddress()) + access_ip_v4 = Column(types.IPAddress(), nullable=True) + access_ip_v6 = Column(types.IPAddress(), nullable=True) - auto_disk_config = Column(Boolean()) - progress = Column(Integer) + auto_disk_config = Column(Boolean(), nullable=True) + progress = Column(Integer, nullable=True) # EC2 instance_initiated_shutdown_terminate # True: -> 'terminate' # False: -> 'stop' # Note(maoy): currently Nova will always stop instead of terminate # no matter what the flag says. So we set the default to False. - shutdown_terminate = Column(Boolean(), default=False, nullable=False) + shutdown_terminate = Column(Boolean(), default=False, nullable=True) # EC2 disable_api_termination - disable_terminate = Column(Boolean(), default=False, nullable=False) + disable_terminate = Column(Boolean(), default=False, nullable=True) # OpenStack compute cell name. This will only be set at the top of # the cells tree and it'll be a full cell name such as 'api!hop1!hop2' - cell_name = Column(String(255)) + cell_name = Column(String(255), nullable=True) class InstanceInfoCache(BASE, NovaBase): diff --git a/nova/objects/instance.py b/nova/objects/instance.py index 0489f1374..58c581542 100644 --- a/nova/objects/instance.py +++ b/nova/objects/instance.py @@ -239,3 +239,57 @@ class Instance(base.NovaObject): uuid=self.uuid, expected_attrs=extra) self[attrname] = instance[attrname] + + +def _make_instance_list(context, inst_list, db_inst_list, expected_attrs): + inst_list.objects = [] + for db_inst in db_inst_list: + inst_obj = Instance._from_db_object(Instance(), db_inst, + expected_attrs=expected_attrs) + inst_obj._context = context + inst_list.objects.append(inst_obj) + inst_list.obj_reset_changes() + return inst_list + + +class InstanceList(base.ObjectListBase, base.NovaObject): + @base.remotable_classmethod + def get_by_filters(cls, context, filters, + sort_key=None, sort_dir=None, limit=None, marker=None, + expected_attrs=None): + db_inst_list = db.instance_get_all_by_filters( + context, filters, sort_key, sort_dir, limit, marker, + columns_to_join=expected_attrs) + + return _make_instance_list(context, cls(), db_inst_list, + expected_attrs) + + @base.remotable_classmethod + def get_by_host(cls, context, host, expected_attrs=None): + db_inst_list = db.instance_get_all_by_host( + context, host, columns_to_join=expected_attrs) + return _make_instance_list(context, cls(), db_inst_list, + expected_attrs) + + @base.remotable_classmethod + def get_by_host_and_node(cls, context, host, node, expected_attrs=None): + db_inst_list = db.instance_get_all_by_host_and_node( + context, host, node) + return _make_instance_list(context, cls(), db_inst_list, + expected_attrs) + + @base.remotable_classmethod + def get_by_host_and_not_type(cls, context, host, type_id=None, + expected_attrs=None): + db_inst_list = db.instance_get_all_by_host_and_not_type( + context, host, type_id=type_id) + return _make_instance_list(context, cls(), db_inst_list, + expected_attrs) + + @base.remotable_classmethod + def get_hung_in_rebooting(cls, context, reboot_window, + expected_attrs=None): + db_inst_list = db.instance_get_all_hung_in_rebooting(context, + reboot_window) + return _make_instance_list(context, cls(), db_inst_list, + expected_attrs) diff --git a/nova/tests/objects/test_instance.py b/nova/tests/objects/test_instance.py index 194f5f90c..54e010a8a 100644 --- a/nova/tests/objects/test_instance.py +++ b/nova/tests/objects/test_instance.py @@ -196,3 +196,117 @@ class TestInstanceObject(test_objects._LocalTest, class TestRemoteInstanceObject(test_objects._RemoteTest, _TestInstanceObject): pass + + +class _TestInstanceListObject(object): + def fake_instance(self, id, updates=None): + fake_instance = fakes.stub_instance(id=2, + access_ipv4='1.2.3.4', + access_ipv6='::1') + fake_instance['scheduled_at'] = None + fake_instance['terminated_at'] = None + fake_instance['deleted_at'] = None + fake_instance['created_at'] = None + fake_instance['updated_at'] = None + fake_instance['launched_at'] = ( + fake_instance['launched_at'].replace( + tzinfo=iso8601.iso8601.Utc(), microsecond=0)) + fake_instance['info_cache'] = {'network_info': 'foo', + 'instance_uuid': fake_instance['uuid']} + fake_instance['deleted'] = 0 + if updates: + fake_instance.update(updates) + return fake_instance + + def test_get_all_by_filters(self): + fakes = [self.fake_instance(1), self.fake_instance(2)] + ctxt = context.get_admin_context() + self.mox.StubOutWithMock(db, 'instance_get_all_by_filters') + db.instance_get_all_by_filters(ctxt, {'foo': 'bar'}, 'uuid', 'asc', + None, None, + columns_to_join=['metadata']).AndReturn( + fakes) + self.mox.ReplayAll() + inst_list = instance.InstanceList.get_by_filters( + ctxt, {'foo': 'bar'}, 'uuid', 'asc', expected_attrs=['metadata']) + + for i in range(0, len(fakes)): + self.assertTrue(isinstance(inst_list.objects[i], + instance.Instance)) + self.assertEqual(inst_list.objects[i].uuid, fakes[i]['uuid']) + self.assertRemotes() + + def test_get_by_host(self): + fakes = [self.fake_instance(1), + self.fake_instance(2)] + ctxt = context.get_admin_context() + self.mox.StubOutWithMock(db, 'instance_get_all_by_host') + db.instance_get_all_by_host(ctxt, 'foo', + columns_to_join=None).AndReturn(fakes) + self.mox.ReplayAll() + inst_list = instance.InstanceList.get_by_host(ctxt, 'foo') + for i in range(0, len(fakes)): + self.assertTrue(isinstance(inst_list.objects[i], + instance.Instance)) + self.assertEqual(inst_list.objects[i].uuid, fakes[i]['uuid']) + self.assertEqual(inst_list.obj_what_changed(), set()) + self.assertRemotes() + + def test_get_by_host_and_node(self): + fakes = [self.fake_instance(1), + self.fake_instance(2)] + ctxt = context.get_admin_context() + self.mox.StubOutWithMock(db, 'instance_get_all_by_host_and_node') + db.instance_get_all_by_host_and_node(ctxt, 'foo', 'bar').AndReturn( + fakes) + self.mox.ReplayAll() + inst_list = instance.InstanceList.get_by_host_and_node(ctxt, 'foo', + 'bar') + for i in range(0, len(fakes)): + self.assertTrue(isinstance(inst_list.objects[i], + instance.Instance)) + self.assertEqual(inst_list.objects[i].uuid, fakes[i]['uuid']) + self.assertRemotes() + + def test_get_by_host_and_not_type(self): + fakes = [self.fake_instance(1), + self.fake_instance(2)] + ctxt = context.get_admin_context() + self.mox.StubOutWithMock(db, 'instance_get_all_by_host_and_not_type') + db.instance_get_all_by_host_and_not_type(ctxt, 'foo', + type_id='bar').AndReturn( + fakes) + self.mox.ReplayAll() + inst_list = instance.InstanceList.get_by_host_and_not_type(ctxt, 'foo', + 'bar') + for i in range(0, len(fakes)): + self.assertTrue(isinstance(inst_list.objects[i], + instance.Instance)) + self.assertEqual(inst_list.objects[i].uuid, fakes[i]['uuid']) + self.assertRemotes() + + def test_get_hung_in_rebooting(self): + fakes = [self.fake_instance(1), + self.fake_instance(2)] + dt = timeutils.isotime() + ctxt = context.get_admin_context() + self.mox.StubOutWithMock(db, 'instance_get_all_hung_in_rebooting') + db.instance_get_all_hung_in_rebooting(ctxt, dt).AndReturn( + fakes) + self.mox.ReplayAll() + inst_list = instance.InstanceList.get_hung_in_rebooting(ctxt, dt) + for i in range(0, len(fakes)): + self.assertTrue(isinstance(inst_list.objects[i], + instance.Instance)) + self.assertEqual(inst_list.objects[i].uuid, fakes[i]['uuid']) + self.assertRemotes() + + +class TestInstanceListObject(test_objects._LocalTest, + _TestInstanceListObject): + pass + + +class TestRemoteInstanceListObject(test_objects._RemoteTest, + _TestInstanceListObject): + pass diff --git a/nova/tests/virt/libvirt/test_libvirt_config.py b/nova/tests/virt/libvirt/test_libvirt_config.py index 8eed7136e..2d9e52f3c 100644 --- a/nova/tests/virt/libvirt/test_libvirt_config.py +++ b/nova/tests/virt/libvirt/test_libvirt_config.py @@ -434,7 +434,7 @@ class LibvirtConfigGuestDiskTest(LibvirtConfigBaseTest): obj = config.LibvirtConfigGuestDisk() obj.source_type = "network" obj.source_protocol = "iscsi" - obj.source_host = "foo.bar.com" + obj.source_name = "foo.bar.com" obj.driver_name = "qemu" obj.driver_format = "qcow2" obj.target_dev = "/dev/hda" @@ -448,11 +448,56 @@ class LibvirtConfigGuestDiskTest(LibvirtConfigBaseTest): <target bus="ide" dev="/dev/hda"/> </disk>""") + def test_config_network_no_name(self): + obj = config.LibvirtConfigGuestDisk() + obj.source_type = 'network' + obj.source_protocol = 'nbd' + obj.source_hosts = ['foo.bar.com'] + obj.source_ports = [None] + obj.driver_name = 'qemu' + obj.driver_format = 'raw' + obj.target_dev = '/dev/vda' + obj.target_bus = 'virtio' + + xml = obj.to_xml() + self.assertXmlEqual(xml, """ + <disk type="network" device="disk"> + <driver name="qemu" type="raw"/> + <source protocol="nbd"> + <host name="foo.bar.com"/> + </source> + <target bus="virtio" dev="/dev/vda"/> + </disk>""") + + def test_config_network_multihost(self): + obj = config.LibvirtConfigGuestDisk() + obj.source_type = 'network' + obj.source_protocol = 'rbd' + obj.source_name = 'pool/image' + obj.source_hosts = ['foo.bar.com', '::1', '1.2.3.4'] + obj.source_ports = [None, '123', '456'] + obj.driver_name = 'qemu' + obj.driver_format = 'raw' + obj.target_dev = '/dev/vda' + obj.target_bus = 'virtio' + + xml = obj.to_xml() + self.assertXmlEqual(xml, """ + <disk type="network" device="disk"> + <driver name="qemu" type="raw"/> + <source name="pool/image" protocol="rbd"> + <host name="foo.bar.com"/> + <host name="::1" port="123"/> + <host name="1.2.3.4" port="456"/> + </source> + <target bus="virtio" dev="/dev/vda"/> + </disk>""") + def test_config_network_auth(self): obj = config.LibvirtConfigGuestDisk() obj.source_type = "network" obj.source_protocol = "rbd" - obj.source_host = "pool/image" + obj.source_name = "pool/image" obj.driver_name = "qemu" obj.driver_format = "raw" obj.target_dev = "/dev/vda" diff --git a/nova/tests/virt/libvirt/test_libvirt_volume.py b/nova/tests/virt/libvirt/test_libvirt_volume.py index 07a1a7b2f..a6dd9e7fe 100644 --- a/nova/tests/virt/libvirt/test_libvirt_volume.py +++ b/nova/tests/virt/libvirt/test_libvirt_volume.py @@ -218,6 +218,32 @@ class LibvirtVolumeTestCase(test.TestCase): self.assertEqual(tree.find('./source/auth'), None) libvirt_driver.disconnect_volume(connection_info, "vde") + def test_libvirt_rbd_driver_hosts(self): + libvirt_driver = volume.LibvirtNetVolumeDriver(self.fake_conn) + name = 'volume-00000001' + vol = {'id': 1, 'name': name} + connection_info = self.rbd_connection(vol) + disk_info = { + "bus": "virtio", + "dev": "vde", + "type": "disk", + } + hosts = ['example.com', '1.2.3.4', '::1'] + ports = [None, '6790', '6791'] + connection_info['data']['hosts'] = hosts + connection_info['data']['ports'] = ports + conf = libvirt_driver.connect_volume(connection_info, disk_info) + tree = conf.format_dom() + self.assertEqual(tree.get('type'), 'network') + self.assertEqual(tree.find('./source').get('protocol'), 'rbd') + rbd_name = '%s/%s' % ('rbd', name) + self.assertEqual(tree.find('./source').get('name'), rbd_name) + self.assertEqual(tree.find('./source/auth'), None) + found_hosts = tree.findall('./source/host') + self.assertEqual([host.get('name') for host in found_hosts], hosts) + self.assertEqual([host.get('port') for host in found_hosts], ports) + libvirt_driver.disconnect_volume(connection_info, "vde") + def test_libvirt_rbd_driver_auth_enabled(self): libvirt_driver = volume.LibvirtNetVolumeDriver(self.fake_conn) name = 'volume-00000001' diff --git a/nova/tests/virt/xenapi/test_vmops.py b/nova/tests/virt/xenapi/test_vmops.py index 18a444f41..674d84882 100644 --- a/nova/tests/virt/xenapi/test_vmops.py +++ b/nova/tests/virt/xenapi/test_vmops.py @@ -18,8 +18,12 @@ from nova.compute import task_states from nova.compute import vm_mode +from nova import exception from nova import test +from nova.tests.virt.xenapi import stubs from nova.virt import fake +from nova.virt.xenapi import driver as xenapi_conn +from nova.virt.xenapi import fake as xenapi_fake from nova.virt.xenapi import vm_utils from nova.virt.xenapi import vmops @@ -166,3 +170,74 @@ class VMOpsTestCase(test.TestCase): self.assertTrue(self._vmops._is_xsm_sr_check_relaxed()) self.assertEqual(self.make_plugin_call_count, 1) + + +class GetConsoleOutputTestCase(stubs.XenAPITestBase): + def setUp(self): + super(GetConsoleOutputTestCase, self).setUp() + stubs.stubout_session(self.stubs, xenapi_fake.SessionBase) + self._session = xenapi_conn.XenAPISession('test_url', 'root', + 'test_pass', fake.FakeVirtAPI()) + self.vmops = vmops.VMOps(self._session, fake.FakeVirtAPI()) + self.vms = [] + + def tearDown(self): + super(GetConsoleOutputTestCase, self).tearDown() + for vm in self.vms: + xenapi_fake.destroy_vm(vm) + + def _create_vm(self, name, state): + vm = xenapi_fake.create_vm(name, state) + self.vms.append(vm) + return vm + + def test_get_console_output_works(self): + self.mox.StubOutWithMock(self.vmops, '_get_dom_id') + + instance = {"name": "dummy"} + self.vmops._get_dom_id(instance, check_rescue=True).AndReturn(42) + self.mox.ReplayAll() + + self.assertEqual("dom_id: 42", self.vmops.get_console_output(instance)) + + def test_get_console_output_throws_nova_exception(self): + self.mox.StubOutWithMock(self.vmops, '_get_dom_id') + + instance = {"name": "dummy"} + # dom_id=0 used to trigger exception in fake XenAPI + self.vmops._get_dom_id(instance, check_rescue=True).AndReturn(0) + self.mox.ReplayAll() + + self.assertRaises(exception.NovaException, + self.vmops.get_console_output, instance) + + def test_get_dom_id_works(self): + instance = {"name": "dummy"} + vm_ref = self._create_vm("dummy", "Running") + vm_rec = xenapi_fake.get_record("VM", vm_ref) + + self.assertEqual(vm_rec["domid"], self.vmops._get_dom_id(instance)) + + def test_get_dom_id_works_with_rescue_vm(self): + instance = {"name": "dummy"} + vm_ref = self._create_vm("dummy-rescue", "Running") + vm_rec = xenapi_fake.get_record("VM", vm_ref) + + self.assertEqual(vm_rec["domid"], + self.vmops._get_dom_id(instance, check_rescue=True)) + + def test_get_dom_id_raises_not_found(self): + instance = {"name": "dummy"} + vm_ref = self._create_vm("notdummy", "Running") + vm_rec = xenapi_fake.get_record("VM", vm_ref) + + self.assertRaises(exception.NotFound, + self.vmops._get_dom_id, instance) + + def test_get_dom_id_works_with_vmref(self): + instance = {"name": "dummy"} + vm_ref = self._create_vm("dummy", "Running") + vm_rec = xenapi_fake.get_record("VM", vm_ref) + + self.assertEqual(vm_rec["domid"], + self.vmops._get_dom_id(vm_ref=vm_ref)) diff --git a/nova/tests/virt/xenapi/test_xenapi.py b/nova/tests/virt/xenapi/test_xenapi.py index 7dcb1cec8..d99fdcb8e 100644 --- a/nova/tests/virt/xenapi/test_xenapi.py +++ b/nova/tests/virt/xenapi/test_xenapi.py @@ -1121,6 +1121,17 @@ class XenAPIVMTestCase(stubs.XenAPITestBase): conn.reboot(self.context, instance, None, "SOFT") + def test_get_console_output_succeeds(self): + + def fake_get_console_output(instance): + self.assertEqual("instance", instance) + return "console_log" + self.stubs.Set(self.conn._vmops, 'get_console_output', + fake_get_console_output) + + self.assertEqual(self.conn.get_console_output("instance"), + "console_log") + def _test_maintenance_mode(self, find_host, find_aggregate): real_call_xenapi = self.conn._session.call_xenapi instance = self._create_instance(spawn=True) diff --git a/nova/virt/libvirt/config.py b/nova/virt/libvirt/config.py index 08a0566dd..e5319c514 100644 --- a/nova/virt/libvirt/config.py +++ b/nova/virt/libvirt/config.py @@ -460,7 +460,9 @@ class LibvirtConfigGuestDisk(LibvirtConfigGuestDevice): self.driver_cache = None self.source_path = None self.source_protocol = None - self.source_host = None + self.source_name = None + self.source_hosts = [] + self.source_ports = [] self.target_dev = None self.target_path = None self.target_bus = None @@ -499,8 +501,16 @@ class LibvirtConfigGuestDisk(LibvirtConfigGuestDevice): elif self.source_type == "mount": dev.append(etree.Element("source", dir=self.source_path)) elif self.source_type == "network": - dev.append(etree.Element("source", protocol=self.source_protocol, - name=self.source_host)) + source = etree.Element("source", protocol=self.source_protocol) + if self.source_name is not None: + source.set('name', self.source_name) + hosts_info = zip(self.source_hosts, self.source_ports) + for name, port in hosts_info: + host = etree.Element('host', name=name) + if port is not None: + host.set('port', port) + source.append(host) + dev.append(source) if self.auth_secret_type is not None: auth = etree.Element("auth") diff --git a/nova/virt/libvirt/volume.py b/nova/virt/libvirt/volume.py index 4a11e2704..712e108de 100644 --- a/nova/virt/libvirt/volume.py +++ b/nova/virt/libvirt/volume.py @@ -131,7 +131,7 @@ class LibvirtFakeVolumeDriver(LibvirtBaseVolumeDriver): disk_info) conf.source_type = "network" conf.source_protocol = "fake" - conf.source_host = "fake" + conf.source_name = "fake" return conf @@ -145,10 +145,12 @@ class LibvirtNetVolumeDriver(LibvirtBaseVolumeDriver): conf = super(LibvirtNetVolumeDriver, self).connect_volume(connection_info, disk_info) + netdisk_properties = connection_info['data'] conf.source_type = "network" conf.source_protocol = connection_info['driver_volume_type'] - conf.source_host = connection_info['data']['name'] - netdisk_properties = connection_info['data'] + conf.source_name = netdisk_properties.get('name') + conf.source_hosts = netdisk_properties.get('hosts', []) + conf.source_ports = netdisk_properties.get('ports', []) auth_enabled = netdisk_properties.get('auth_enabled') if (conf.source_protocol == 'rbd' and CONF.rbd_secret_uuid): diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py index 69b0c2010..379671370 100644 --- a/nova/virt/xenapi/fake.py +++ b/nova/virt/xenapi/fake.py @@ -50,10 +50,12 @@ A fake XenAPI SDK. """ +import base64 import pickle import random import uuid from xml.sax import saxutils +import zlib import pprint @@ -608,6 +610,12 @@ class SessionBase(object): def _plugin_xenhost_host_uptime(self, method, args): return jsonutils.dumps({"uptime": "fake uptime"}) + def _plugin_console_get_console_log(self, method, args): + dom_id = args["dom_id"] + if dom_id == 0: + raise Failure('Guest does not have a console') + return base64.b64encode(zlib.compress("dom_id: %s" % dom_id)) + def host_call_plugin(self, _1, _2, plugin, method, args): func = getattr(self, '_plugin_%s_%s' % (plugin, method), None) if not func: diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 56a4b18d8..bf8aa3e52 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -19,9 +19,11 @@ Management class for VM-related functions (spawn, reboot, etc). """ +import base64 import functools import itertools import time +import zlib from eventlet import greenthread import netaddr @@ -1421,9 +1423,18 @@ class VMOps(object): return bw def get_console_output(self, instance): - """Return snapshot of console.""" - # TODO(armando-migliaccio): implement this to fix pylint! - return 'FAKE CONSOLE OUTPUT of instance' + """Return last few lines of instance console.""" + dom_id = self._get_dom_id(instance, check_rescue=True) + + try: + raw_console_data = self._session.call_plugin('console', + 'get_console_log', {'dom_id': dom_id}) + except self._session.XenAPI.Failure as exc: + LOG.exception(exc) + msg = _("Guest does not have a console available") + raise exception.NovaException(msg) + + return zlib.decompress(base64.b64decode(raw_console_data)) def get_vnc_console(self, instance): """Return connection info for a vnc console.""" @@ -1607,9 +1618,7 @@ class VMOps(object): """ args = {} if instance or vm_ref: - vm_ref = vm_ref or self._get_vm_opaque_ref(instance) - vm_rec = self._session.call_xenapi("VM.get_record", vm_ref) - args['dom_id'] = vm_rec['domid'] + args['dom_id'] = self._get_dom_id(instance, vm_ref) args.update(addl_args) try: return self._session.call_plugin(plugin, method, args) @@ -1630,6 +1639,11 @@ class VMOps(object): return {'returncode': 'error', 'message': err_msg} return None + def _get_dom_id(self, instance=None, vm_ref=None, check_rescue=False): + vm_ref = vm_ref or self._get_vm_opaque_ref(instance, check_rescue) + vm_rec = self._session.call_xenapi("VM.get_record", vm_ref) + return vm_rec['domid'] + def _add_to_param_xenstore(self, vm_ref, key, val): """ Takes a key/value pair and adds it to the xenstore parameter diff --git a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec index b93c7b071..85c2d1c05 100644 --- a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec +++ b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec @@ -32,6 +32,7 @@ rm -rf $RPM_BUILD_ROOT /etc/xapi.d/plugins/bandwidth /etc/xapi.d/plugins/bittorrent /etc/xapi.d/plugins/config_file +/etc/xapi.d/plugins/console /etc/xapi.d/plugins/glance /etc/xapi.d/plugins/kernel /etc/xapi.d/plugins/migration diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/console b/plugins/xenserver/xenapi/etc/xapi.d/plugins/console new file mode 100755 index 000000000..afcb783f7 --- /dev/null +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/console @@ -0,0 +1,80 @@ +#!/usr/bin/python +# 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. + +""" +To configure this plugin, you must set the following xenstore key: +/local/logconsole/@ = "/var/log/xen/guest/console.%d" + +This can be done by running: +xenstore-write /local/logconsole/@ "/var/log/xen/guest/console.%d" + +WARNING: +You should ensure appropriate log rotation to ensure +guests are not able to consume too much Dom0 disk space, +and equally should not be able to stop other guests from logging. +Adding and removing the following xenstore key will reopen the log, +as will be required after a log rotate: +/local/logconsole/<dom_id> +""" + +import base64 +import logging +import os +import zlib + +import XenAPIPlugin + +import pluginlib_nova +pluginlib_nova.configure_logging("console") + +CONSOLE_LOG_DIR = '/var/log/xen/guest' +CONSOLE_LOG_FILE_PATTERN = CONSOLE_LOG_DIR + '/console.%d' + +MAX_CONSOLE_BYTES = 102400 +SEEK_SET = 0 +SEEK_END = 2 + + +def _last_bytes(file_like_object): + try: + file_like_object.seek(-MAX_CONSOLE_BYTES, SEEK_END) + except IOError, e: + if e.errno == 22: + file_like_object.seek(0, SEEK_SET) + else: + raise + return file_like_object.read() + + +def get_console_log(session, arg_dict): + try: + raw_dom_id = arg_dict['dom_id'] + except KeyError: + raise pluginlib_nova.PluginError("Missing dom_id") + try: + dom_id = int(raw_dom_id) + except ValueError: + raise pluginlib_nova.PluginError("Invalid dom_id") + + logfile = CONSOLE_LOG_FILE_PATTERN % dom_id + try: + log_content = pluginlib_nova.with_file(logfile, 'rb', _last_bytes) + except IOError, e: + msg = "Error reading console: %s" % e + logging.debug(msg) + raise pluginlib_nova.PluginError(msg) + return base64.b64encode(zlib.compress(log_content)) + + +if __name__ == "__main__": + XenAPIPlugin.dispatch({"get_console_log": get_console_log}) |