XenAPI: Add support for XenServer VMs

Ironic should be able to control power of the VMs hosted on
a XenServer host.  This change adds such support to the ssh
driver.

Blueprint: xenserver-power-control

Change-Id: I30b4d0c3f2685170f8c4f6ec834e5395d6bb2499
This commit is contained in:
Bob Ball 2015-10-06 10:10:13 +01:00
parent 93f5a12a99
commit a31f3679b8
4 changed files with 176 additions and 2 deletions

View File

@ -118,8 +118,17 @@ CIMC driver
OneView driver
--------------
=======
.. toctree::
:maxdepth: 1
../drivers/oneview
XenServer ssh driver
--------------------
.. toctree::
:maxdepth: 1
../drivers/xenserver

View File

@ -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=<uuid> xenstore-data:vm-data="vm_data/disable_pf: 1"

View File

@ -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, ") %

View File

@ -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)