[ansible] Add basic root device hints support

This patch adds support for Ironic root hints to ansible deploy driver.

The advanced root hints based on oslo.utils operators are not supported yet,
only exact matches.

Change-Id: I6fd468cfa9a7792fa0e55aa58c337396e13d76ee
This commit is contained in:
Yuriy Zveryanskyy 2016-06-09 12:23:25 +03:00 committed by Pavlo Shchelokovskyy
parent 1d6b1b89d2
commit 84b1b0d34e
8 changed files with 278 additions and 2 deletions

View File

@ -82,8 +82,11 @@ and partition images, on both ``msdos`` and ``GPT`` labeled disks.
Root device hints
~~~~~~~~~~~~~~~~~
Root device hints are currently not supported (first device returned as
``ansible_devices`` fact is used), but support for them is planned.
Root device hints are currently supported in their basic form only
(with exact matches, without oslo.utils operators).
If no root device hint is provided for the node, first device returned as
part of ``ansible_devices`` fact is used as root device to create partitions
on or write the whole disk image to.
Node cleaning
~~~~~~~~~~~~~

View File

@ -302,6 +302,38 @@ def _parse_partitioning_info(node):
return {'partition_info': i_info}
def _parse_root_device_hints(node):
"""Convert string with hints to dict. """
root_device = node.properties.get('root_device')
if not root_device:
return {}
try:
parsed_hints = irlib_utils.parse_root_device_hints(root_device)
except ValueError as e:
raise exception.InvalidParameterValue(
_('Failed to validate the root device hints for node %(node)s. '
'Error: %(error)s') % {'node': node.uuid, 'error': e})
root_device_hints = {}
advanced = {}
for hint, value in parsed_hints.items():
if isinstance(value, six.string_types):
if value.startswith('== '):
root_device_hints[hint] = int(value[3:])
elif value.startswith('s== '):
root_device_hints[hint] = urlparse.unquote(value[4:])
else:
advanced[hint] = value
else:
root_device_hints[hint] = value
if advanced:
raise exception.InvalidParameterValue(
_('Ansible-deploy does not support advanced root device hints '
'based on oslo.utils operators. '
'Present advanced hints for node %(node)s are %(hints)s.') % {
'node': node.uuid, 'hints': advanced})
return root_device_hints
def _prepare_variables(task):
node = task.node
i_info = node.instance_info
@ -333,6 +365,11 @@ def _prepare_variables(task):
cfgdrv_type = 'file'
variables['configdrive'] = {'type': cfgdrv_type,
'location': cfgdrv_location}
root_device_hints = _parse_root_device_hints(node)
if root_device_hints:
variables['root_device_hints'] = root_device_hints
return variables
@ -441,6 +478,8 @@ class AnsibleDeploy(agent_base.HeartbeatMixin, base.DeployInterface):
error_msg = _('Node %s failed to validate deploy image info. Some '
'parameters were missing') % node.uuid
deploy_utils.check_for_missing_params(params, error_msg)
# validate root device hints, proper exceptions are raised from there
_parse_root_device_hints(node)
def _ansible_deploy(self, task, node_address):
"""Internal function for deployment to a node."""

View File

@ -0,0 +1,72 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# 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.
try:
import pyudev
HAS_PYUDEV = True
except ImportError:
HAS_PYUDEV = False
from ironic_staging_drivers.common.i18n import _LW
COLLECT_INFO = (('wwn', 'WWN'), ('serial', 'SERIAL_SHORT'),
('wwn_with_extension', 'WWN_WITH_EXTENSION'),
('wwn_vendor_extension', 'WWN_VENDOR_EXTENSION'))
def get_devices_wwn(devices):
if not HAS_PYUDEV:
LOG.warning(_LW('Can not collect "wwn", "wwn_with_extension", '
'"wwn_vendor_extension" and "serial" when using '
'root device hints because there\'s no UDEV python '
'binds installed'))
return
dev_dict = {}
context = pyudev.Context()
for device in devices:
name = '/dev/' + device
try:
udev = pyudev.Device.from_device_file(context, name)
except (ValueError, EnvironmentError, pyudev.DeviceNotFoundError) as e:
LOG.warning(_LW('Device %(dev)s is inaccessible, skipping... '
'Error: %(error)s'), {'dev': name, 'error': e})
continue
dev_dict[device] = {}
for key, udev_key in COLLECT_INFO:
dev_dict[device][key] = udev.get('ID_%s' % udev_key)
return {"ansible_facts": {"devices_wwn": dev_dict}}
def main():
module = AnsibleModule(
argument_spec=dict(
devices=dict(required=True, type='list'),
),
supports_check_mode=True,
)
devices = module.params['devices']
data = get_devices_wwn(devices)
module.exit_json(**data)
from ansible.module_utils.basic import * # noqa
if __name__ == '__main__':
main()

View File

@ -0,0 +1,98 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# 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.
GIB = 1 << 30
EXTRA_PARAMS = set(['wwn', 'serial', 'wwn_with_extension',
'wwn_vendor_extension'])
# NOTE: ansible calculates device size as float with 2-digits precision,
# Ironic requires size in GiB, if we will use ansible size parameter
# a bug is possible for devices > 1 TB
def size_gib(device_info):
sectors = device_info.get('sectors')
sectorsize = device_info.get('sectorsize')
if sectors is None or sectorsize is None:
return '0'
return str((int(sectors) * int(sectorsize)) // GIB)
def merge_devices_info(devices, devices_wwn):
merged_info = devices.copy()
for device in merged_info:
if device in devices_wwn:
merged_info[device].update(devices_wwn[device])
# replace size
merged_info[device]['size'] = size_gib(merged_info[device])
return merged_info
def root_hint(hints, devices):
hint = None
name = hints.pop('name', None)
for device in devices:
for key in hints:
if hints[key] != devices[device].get(key):
break
else:
# If multiple hints are specified, a device must satisfy all
# the hints
dev_name = '/dev/' + device
if name is None or name == dev_name:
hint = dev_name
break
return hint
def main():
module = AnsibleModule(
argument_spec=dict(
root_device_hints=dict(required=True, type='dict'),
ansible_devices=dict(required=True, type='dict'),
ansible_devices_wwn=dict(required=True, type='dict')
),
supports_check_mode=True)
hints = module.params['root_device_hints']
devices = module.params['ansible_devices']
devices_wwn = module.params['ansible_devices_wwn']
if devices_wwn is None:
extra = set(hints) & EXTRA_PARAMS
if extra:
module.fail_json(msg='Extra hints (supported by additional ansible'
' module) are set but this information can not be'
' collected. Extra hints: %s' % ', '.join(extra))
devices_info = merge_devices_info(devices, devices_wwn or {})
hint = root_hint(hints, devices_info)
if hint is None:
module.fail_json(msg='Root device hints are set, but none of the '
'devices satisfy them. Collected devices info: %s'
% devices_info)
ret_data = {'ansible_facts': {'ironic_root_device': hint}}
module.exit_json(**ret_data)
from ansible.module_utils.basic import * # noqa
if __name__ == '__main__':
main()

View File

@ -1,3 +1,6 @@
- include: roothints.yaml
when: ironic.root_device_hints is defined
- set_fact:
ironic_root_device: /dev/{{ ansible_devices.keys()[0] }}
when: ironic_root_device is undefined

View File

@ -0,0 +1,8 @@
- name: get devices wwn facts
facts_wwn:
devices: "{{ ansible_devices.keys() }}"
- name: calculate root hint
root_hints:
root_device_hints: "{{ ironic.root_device_hints }}"
ansible_devices: "{{ ansible_devices }}"
ansible_devices_wwn: "{{ devices_wwn | default(None) }}"

View File

@ -296,6 +296,37 @@ class TestAnsibleMethods(db_base.DbTestCase):
"foo": "bar"},
ansible_deploy._prepare_extra_vars(host_list, ansible_vars))
def test__parse_root_device_hints(self):
hints = {"wwn": "fake wwn", "size": "12345", "rotational": True}
expected = {"wwn": "fake wwn", "size": 12345, "rotational": True}
props = self.node.properties
props['root_device'] = hints
self.node.properties = props
self.node.save()
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertEqual(
expected, ansible_deploy._parse_root_device_hints(task.node))
def test__parse_root_device_hints_fail_advanced(self):
hints = {"wwn": "s!= fake wwn",
"size": ">= 12345",
"name": "<or> spam <or> ham",
"rotational": True}
expected = {"wwn": "s!= fake%20wwn",
"name": "<or> spam <or> ham",
"size": ">= 12345"}
props = self.node.properties
props['root_device'] = hints
self.node.properties = props
self.node.save()
with task_manager.acquire(self.context, self.node.uuid) as task:
exc = self.assertRaises(
exception.InvalidParameterValue,
ansible_deploy._parse_root_device_hints, task.node)
for key, value in expected.items():
self.assertIn(six.text_type(key), six.text_type(exc))
self.assertIn(six.text_type(value), six.text_type(exc))
@mock.patch.object(ansible_deploy, '_calculate_memory_req', autospec=True,
return_value=2000)
def test__prepare_variables(self, mem_req_mock):
@ -308,6 +339,23 @@ class TestAnsibleMethods(db_base.DbTestCase):
self.assertEqual(expected,
ansible_deploy._prepare_variables(task))
@mock.patch.object(ansible_deploy, '_calculate_memory_req', autospec=True,
return_value=2000)
def test__prepare_variables_root_device_hints(self, mem_req_mock):
props = self.node.properties
props['root_device'] = {"wwn": "fake-wwn"}
self.node.properties = props
self.node.save()
expected = {"image": {"url": "http://image",
"source": "fake-image",
"mem_req": 2000,
"disk_format": "qcow2",
"checksum": "md5:checksum"},
"root_device_hints": {"wwn": "fake-wwn"}}
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertEqual(expected,
ansible_deploy._prepare_variables(task))
@mock.patch.object(ansible_deploy, '_calculate_memory_req', autospec=True,
return_value=2000)
def test__prepare_variables_noglance(self, mem_req_mock):

View File

@ -0,0 +1,5 @@
---
features:
- Support for basic root device hints added to ansible deploy driver.
Only exact matches are supported for now, advanced hints based on
oslo.utils operators are not supported yet.