diff --git a/doc/source/deploy/drivers.rst b/doc/source/deploy/drivers.rst index c3a9147ff5..1f317bcf07 100644 --- a/doc/source/deploy/drivers.rst +++ b/doc/source/deploy/drivers.rst @@ -118,8 +118,17 @@ CIMC driver OneView driver -------------- - +======= .. toctree:: :maxdepth: 1 ../drivers/oneview + + +XenServer ssh driver +-------------------- + +.. toctree:: + :maxdepth: 1 + + ../drivers/xenserver diff --git a/doc/source/drivers/xenserver.rst b/doc/source/drivers/xenserver.rst new file mode 100644 index 0000000000..e866059d34 --- /dev/null +++ b/doc/source/drivers/xenserver.rst @@ -0,0 +1,41 @@ +.. _xenserver: +.. _bug 1498576: https://bugs.launchpad.net/diskimage-builder/+bug/1498576 + +================= +XenServer drivers +================= + +Overview +======== + +XenServer drivers can be used to deploy hosts with Ironic by using XenServer +VMs to simulate bare metal nodes. + +Ironic provides support via the ``pxe_ssh`` and ``agent_ssh`` drivers for +using a XenServer VM as a bare metal target and do provisioning on it. It +works by connecting via SSH into the XenServer host and running commands using +the 'xe' command. + +This is particularly useful for deploying overclouds that use XenServer for VM +hosting as the Compute node must be run as a virtual machine on the XenServer +host it will be controlling. In this case, one VM per hypervisor needs to be +installed. + +This support has been tested with XenServer 6.5. + +Usage +===== + +* Install the VMs using the "Other Install Media" template, which will ensure + that they are HVM guests + +* Set the HVM guests to boot from network first + +* If your generated initramfs does not have the fix for `bug 1498576`_, + disable the Xen PV drivers as a work around + +:: + + xe vm-param-set uuid= xenstore-data:vm-data="vm_data/disable_pf: 1" + + diff --git a/ironic/drivers/modules/ssh.py b/ironic/drivers/modules/ssh.py index 7277c8618b..49eff35060 100644 --- a/ironic/drivers/modules/ssh.py +++ b/ironic/drivers/modules/ssh.py @@ -25,6 +25,7 @@ Currently supported environments are: Virsh (virsh) VMware (vmware) Parallels (parallels) + XenServer (xenserver) """ import os @@ -71,7 +72,7 @@ REQUIRED_PROPERTIES = { "Required."), 'ssh_username': _("username to authenticate as. Required."), 'ssh_virt_type': _("virtualization software to use; one of vbox, virsh, " - "vmware, parallels. Required.") + "vmware, parallels, xenserver. Required.") } OTHER_PROPERTIES = { 'ssh_key_contents': _("private key(s). One of this, ssh_key_filename, " @@ -107,6 +108,12 @@ def _get_boot_device_map(virt_type): boot_devices.PXE: 'net', boot_devices.CDROM: 'dvd', } + elif virt_type == 'xenserver': + return { + boot_devices.DISK: 'c', + boot_devices.PXE: 'n', + boot_devices.CDROM: 'd', + } elif virt_type == 'parallels': return { boot_devices.DISK: 'hdd0', @@ -228,6 +235,32 @@ def _get_command_sets(virt_type): "{_BaseCmd_} list -i {_NodeName_} | " "awk '/^Boot order:/ {print $3}'"), } + elif virt_type == 'xenserver': + return { + 'base_cmd': 'LC_ALL=C /opt/xensource/bin/xe', + # Note(bobba): XenServer appears to have a condition where + # vm-start can return before the power-state + # has been updated to 'running'. Ironic + # expects the power-state to be updated + # immediately, so may find that power-state + # is still 'halted' and attempt to start the + # VM a second time. Sleep to avoid the race. + 'start_cmd': 'vm-start uuid={_NodeName_} && sleep 10s', + 'stop_cmd': 'vm-shutdown uuid={_NodeName_} force=true', + 'list_all': "vm-list --minimal | tr ',' '\n'", + 'list_running': ( + "vm-list power-state=running --minimal |" + " tr ',' '\n'"), + 'get_node_macs': ( + "vif-list vm-uuid={_NodeName_}" + " params=MAC --minimal | tr ',' '\n'"), + 'set_boot_device': ( + "{_BaseCmd_} vm-param-set uuid={_NodeName_}" + " HVM-boot-params:order='{_BootDevice_}'"), + 'get_boot_device': ( + "{_BaseCmd_} vm-param-get uuid={_NodeName_}" + " --param-name=HVM-boot-params param-key=order | cut -b 1"), + } else: raise exception.InvalidParameterValue(_( "SSHPowerDriver '%(virt_type)s' is not a valid virt_type, ") % diff --git a/ironic/tests/unit/drivers/modules/test_ssh.py b/ironic/tests/unit/drivers/modules/test_ssh.py index 4cb501ae83..ba75e67f31 100644 --- a/ironic/tests/unit/drivers/modules/test_ssh.py +++ b/ironic/tests/unit/drivers/modules/test_ssh.py @@ -185,6 +185,10 @@ class SSHValidateParametersTestCase(db_base.DbTestCase): boot_map = ssh._get_boot_device_map('vbox') self.assertEqual('net', boot_map[boot_devices.PXE]) + def test__get_boot_device_map_xenserver(self): + boot_map = ssh._get_boot_device_map('xenserver') + self.assertEqual('n', boot_map[boot_devices.PXE]) + def test__get_boot_device_map_exception(self): self.assertRaises(exception.InvalidParameterValue, ssh._get_boot_device_map, @@ -859,6 +863,23 @@ class SSHDriverTestCase(db_base.DbTestCase): 'edit %s') % fake_name mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + @mock.patch.object(ssh, '_ssh_execute', autospec=True) + def test_management_interface_set_boot_device_xenserver_ok(self, + mock_exc, + mock_h, + mock_get_conn): + fake_name = 'fake-name' + mock_h.return_value = fake_name + mock_get_conn.return_value = self.sshclient + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node['driver_info']['ssh_virt_type'] = 'xenserver' + self.driver.management.set_boot_device(task, boot_devices.PXE) + expected_cmd = ("LC_ALL=C /opt/xensource/bin/xe vm-param-set uuid=%s " + "HVM-boot-params:order='n'") % fake_name + mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + def test_set_boot_device_bad_device(self): with task_manager.acquire(self.context, self.node.uuid) as task: self.assertRaises(exception.InvalidParameterValue, @@ -941,6 +962,25 @@ class SSHDriverTestCase(db_base.DbTestCase): 'print; }\' Q="\'" RS="[<>]" | head -1') % fake_name mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + @mock.patch.object(ssh, '_ssh_execute', autospec=True) + def test_management_interface_get_boot_device_xenserver(self, mock_exc, + mock_h, + mock_get_conn): + fake_name = 'fake-name' + mock_h.return_value = fake_name + mock_exc.return_value = ('n', '') + mock_get_conn.return_value = self.sshclient + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node['driver_info']['ssh_virt_type'] = 'xenserver' + result = self.driver.management.get_boot_device(task) + self.assertEqual(boot_devices.PXE, result['boot_device']) + expected_cmd = ('LC_ALL=C /opt/xensource/bin/xe vm-param-get ' + 'uuid=%s --param-name=HVM-boot-params ' + 'param-key=order | cut -b 1') % fake_name + mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + @mock.patch.object(ssh, '_get_connection', autospec=True) @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) def test_get_boot_device_not_supported(self, mock_h, mock_get_conn): @@ -972,6 +1012,57 @@ class SSHDriverTestCase(db_base.DbTestCase): "echo '\"%(node)s\"' || true") % {'node': nodename} mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + @mock.patch.object(ssh, '_ssh_execute', autospec=True) + def test_get_power_state_xenserver(self, mock_exc, mock_h, mock_get_conn): + # To see replacing {_NodeName_} in xenserver's list_running + nodename = 'fakevm' + mock_h.return_value = nodename + mock_get_conn.return_value = self.sshclient + mock_exc.return_value = (nodename, '') + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node['driver_info']['ssh_virt_type'] = 'xenserver' + power_state = self.driver.power.get_power_state(task) + self.assertEqual(states.POWER_ON, power_state) + expected_cmd = ("LC_ALL=C /opt/xensource/bin/xe " + "vm-list power-state=running --minimal | tr ',' '\n'") + mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + @mock.patch.object(ssh, '_ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_power_status', autospec=True) + def test_start_command_xenserver(self, mock_power, mock_exc, mock_h, + mock_get_conn): + mock_power.side_effect = [states.POWER_OFF, states.POWER_ON] + nodename = 'fakevm' + mock_h.return_value = nodename + mock_get_conn.return_value = self.sshclient + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node['driver_info']['ssh_virt_type'] = 'xenserver' + self.driver.power.set_power_state(task, states.POWER_ON) + expected_cmd = ("LC_ALL=C /opt/xensource/bin/xe " + "vm-start uuid=fakevm && sleep 10s") + mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + + @mock.patch.object(ssh, '_get_connection', autospec=True) + @mock.patch.object(ssh, '_get_hosts_name_for_node', autospec=True) + @mock.patch.object(ssh, '_ssh_execute', autospec=True) + @mock.patch.object(ssh, '_get_power_status', autospec=True) + def test_stop_command_xenserver(self, mock_power, mock_exc, mock_h, + mock_get_conn): + mock_power.side_effect = [states.POWER_ON, states.POWER_OFF] + nodename = 'fakevm' + mock_h.return_value = nodename + mock_get_conn.return_value = self.sshclient + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node['driver_info']['ssh_virt_type'] = 'xenserver' + self.driver.power.set_power_state(task, states.POWER_OFF) + expected_cmd = ("LC_ALL=C /opt/xensource/bin/xe " + "vm-shutdown uuid=fakevm force=true") + mock_exc.assert_called_once_with(mock.ANY, expected_cmd) + def test_management_interface_validate_good(self): with task_manager.acquire(self.context, self.node.uuid) as task: task.driver.management.validate(task)