Add node disks validation

Adds a validation to check node disks for root device hints and make
sure they have a sufficient size for the available flavors.

Closes-Bug: #1668972

Change-Id: I46c5b69601d37d2faa77d9da4bd1b3ff3373c086
This commit is contained in:
Florian Fuchs 2018-09-21 13:11:24 +00:00
parent 3b54eea329
commit 39f139cdad
4 changed files with 336 additions and 3 deletions

View File

@ -0,0 +1,142 @@
# -*- 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.
from tripleo_validations.tests import base
from validations.library.node_disks import _get_smallest_disk
from validations.library.node_disks import _has_root_device_hints
from validations.library.node_disks import validate_node_disks
# node_1: 2 disks, 1 larger than 4GB (50GB)
# node_2: 3 disks, 2 larger than 4GB (50GB, 10GB)
# node_2: 3 disks, 2 larger than 4GB (50GB, 10GB)
INTROSPECTION_DATA = {
'node_1': {
"inventory": {
"disks": [
{"name": "disk-1", "size": 53687091200},
{"name": "disk-2", "size": 4294967296},
]
}
},
'node_2': {
"inventory": {
"disks": [
{"name": "disk-1", "size": 53687091200},
{"name": "disk-2", "size": 10737418240},
{"name": "disk-3", "size": 4294967296},
]
}
},
'node_3': {
"inventory": {
"disks": [
{"name": "disk-1", "size": 53687091200},
{"name": "disk-2", "size": 10737418240},
{"name": "disk-3", "size": 4294967296},
]
}
},
}
# small: fits nodes with disks >= 10GB
# large: fits nodes with disks >= 50GB
FLAVOR_DATA = {
"small": {
"disk": 10,
"name": "small"
},
"large": {
"disk": 50,
"name": "large"
},
}
# node_1: no root device set, one disk > 4GB, fits both flavors
# node_2: root device set to name of disk-2 which fits both flavors
# node_3: no root device set, small disk only fits small flavor
NODE_DATA = {
"node_1": {"properties": {}},
"node_2": {
"properties": {
"root_device": {
"wwn": "0x4000cca77fc4dba1"
}
}
},
"node_3": {"properties": {}},
}
class TestGetSmallestDisk(base.TestCase):
def test_get_correct_disk(self):
introspection_data = INTROSPECTION_DATA['node_2']
smallest_disk = _get_smallest_disk(
introspection_data['inventory']['disks'])
self.assertEqual(smallest_disk['size'], 4294967296)
class TestHasRootDeviceHints(base.TestCase):
def test_detect_root_device_hints(self):
self.assertTrue(_has_root_device_hints('node_2', NODE_DATA))
def test_detect_no_root_device_hints(self):
self.assertFalse(_has_root_device_hints('node_1', NODE_DATA))
class TestValidateRootDeviceHints(base.TestCase):
def setUp(self):
super(TestValidateRootDeviceHints, self).setUp()
def test_node_1_no_warning(self):
introspection_data = {
'node_1': INTROSPECTION_DATA['node_1']
}
errors, warnings = validate_node_disks({},
FLAVOR_DATA,
introspection_data)
self.assertEqual([[], []], [errors, warnings])
def test_small_flavor_no_hints_warning(self):
introspection_data = {
'node_2': INTROSPECTION_DATA['node_2']
}
flavors = {
"small": FLAVOR_DATA['small']
}
expected_warnings = [
'node_2 has more than one disk available for deployment',
]
errors, warnings = validate_node_disks({},
flavors,
introspection_data)
self.assertEqual([[], expected_warnings], [errors, warnings])
def test_large_flavor_no_hints_error(self):
introspection_data = {
'node_3': INTROSPECTION_DATA['node_3']
}
expected_errors = [
'node_3 has more than one disk available for deployment and no '
'root device hints set. The disk that will be used is too small '
'for the flavor with the largest disk requirement ("large").',
]
errors, warnings = validate_node_disks({},
FLAVOR_DATA,
introspection_data)
self.assertEqual([expected_errors, []], [errors, warnings])

View File

@ -0,0 +1,158 @@
#!/usr/bin/env python
# Copyright 2016 Red Hat, Inc.
# 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.
from ansible.module_utils.basic import AnsibleModule # noqa
DOCUMENTATION = '''
---
module: node_disks
short_description: Check disks, flavors and root device hints
description:
- Check if each node has a root device hint set if there is more
than one disk and compare flavors to disk sizes.
options:
nodes:
required: true
description:
- A list of nodes
type: list
flavors:
required: true
description:
- A list of flavors
type: list
introspection_data:
required: true
description:
- Introspection data for all nodes
type: list
author: "Florian Fuchs <flfuchs@redhat.com>"
'''
EXAMPLES = '''
- hosts: undercloud
tasks:
- name: Check node disks
node_disks:
nodes: "{{ lookup('ironic_nodes') }}"
flavors: "{{ lookup('nova_flavors') }}"
introspection_data: "{{ lookup('introspection_data',
auth_url=auth_url.value, password=password.value) }}"
'''
IGNORE_BYTE_MAX = 4294967296
ONE_DISK_TOO_SMALL_ERROR = """\
The node {} only has one disk and it's too small for the "{}" flavor"""
NO_RDH_SMALLEST_DISK_TOO_SMALL_ERROR = (
'{} has more than one disk available for deployment and no '
'root device hints set. The disk that will be used is too small '
'for the flavor with the largest disk requirement ("{}").')
def _get_minimum_disk_size(flavors):
min_gb = 0
name = 'n.a.'
for key, val in flavors.items():
disk_gb = val['disk']
if disk_gb > min_gb:
min_gb = disk_gb
name = key
# convert GB to bytes to compare to introspection data
return name, min_gb * 1073741824
def _get_smallest_disk(disks):
smallest = disks[0]
for disk in disks[1:]:
if disk['size'] < smallest['size']:
smallest = disk
return smallest
def _has_root_device_hints(node_name, node_data):
rdh = node_data.get(
node_name, {}).get('properties', {}).get('root_device')
return rdh is not None
def validate_node_disks(nodes, flavors, introspection_data):
"""Validate root device hints using introspection data.
:param nodes: Ironic nodes
:param introspection_data: Introspection data for all nodes
:returns warnings: List of warning messages
errors: List of error messages
"""
errors = []
warnings = []
# Get the name of the flavor with the largest disk requirement,
# which defines the minimum disk size.
max_disk_flavor, min_disk_size = _get_minimum_disk_size(flavors)
for node, content in introspection_data.items():
disks = content.get('inventory', {}).get('disks')
valid_disks = [disk for disk in disks
if disk['size'] > IGNORE_BYTE_MAX]
root_device_hints = _has_root_device_hints(node, nodes)
smallest_disk = _get_smallest_disk(valid_disks)
if len(valid_disks) == 1:
if smallest_disk.get('size', 0) < min_disk_size:
errors.append(ONE_DISK_TOO_SMALL_ERROR.format(
node, max_disk_flavor))
elif not root_device_hints and len(valid_disks) > 1:
if smallest_disk.get('size', 0) < min_disk_size:
errors.append(NO_RDH_SMALLEST_DISK_TOO_SMALL_ERROR.format(
node, max_disk_flavor))
else:
warnings.append('{} has more than one disk available for '
'deployment'.format(node))
return errors, warnings
def main():
module = AnsibleModule(argument_spec=dict(
nodes=dict(required=True, type='list'),
flavors=dict(required=True, type='dict'),
introspection_data=dict(required=True, type='list')
))
nodes = {node['name']: node for node in module.params.get('nodes')}
flavors = module.params.get('flavors')
introspection_data = {name: content for (name, content) in
module.params.get('introspection_data')}
errors, warnings = validate_node_disks(nodes,
flavors,
introspection_data)
if errors:
module.fail_json(msg="\n".join(errors))
elif warnings:
module.exit_json(warnings="\n".join(warnings))
else:
module.exit_json(msg="Root device hints are either set or not "
"necessary.")
if __name__ == '__main__':
main()

View File

@ -46,6 +46,14 @@ class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
"""Returns server information from nova."""
nova = utils.get_nova_client(variables)
flavors = nova.flavors.list()
return {f.name: {'name': f.name, 'keys': f.get_keys()}
for f in flavors}
return {f.name: {'name': f.name,
'id': f.id,
'disk': f.disk,
'ram': f.ram,
'vcpus': f.vcpus,
'ephemeral': f.ephemeral,
'swap': f.swap,
'is_public': f.is_public,
'rxtx_factor': f.rxtx_factor,
'keys': f.get_keys()}
for f in nova.flavors.list()}

View File

@ -0,0 +1,25 @@
---
- hosts: undercloud
vars:
metadata:
name: Check node disk configuration
description: >
Check node disk numbers and sizes and whether root device hints are set.
groups:
- pre-deployment
tasks:
- name: Get Ironic Inspector swift auth_url
become: true
ini: path=/var/lib/config-data/puppet-generated/ironic/etc/ironic/ironic.conf section=inspector key=auth_url
register: ironic_auth_url
- name: Get Ironic Inspector swift password
become: true
ini: path=/var/lib/config-data/puppet-generated/ironic/etc/ironic/ironic.conf section=inspector key=password
register: ironic_password
- name: Check node disks
node_disks:
nodes: "{{ lookup('ironic_nodes', wantlist=True) }}"
flavors: "{{ lookup('nova_flavors', wantlist=True) }}"
introspection_data: "{{ lookup('introspection_data',
auth_url=ironic_auth_url.value,
password=ironic_password.value) }}"