Merge "Add node disks validation"
This commit is contained in:
commit
fe57d705b2
142
tripleo_validations/tests/library/test_node_disks.py
Normal file
142
tripleo_validations/tests/library/test_node_disks.py
Normal 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])
|
158
validations/library/node_disks.py
Normal file
158
validations/library/node_disks.py
Normal 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()
|
@ -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()}
|
||||
|
25
validations/node-disks.yaml
Normal file
25
validations/node-disks.yaml
Normal 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) }}"
|
Loading…
x
Reference in New Issue
Block a user