Fix Switch VLANs validation
The "Compare switch VLANs to VLANs in nic config" validation breaks when run through Mistral. This is because the custom module used in the validation imports functions from another custom module. Custom modules are recognized by Ansible, but they don't necessarily live on the Python path, so when run through Mistral the imported module cannot be found and the validation breaks. This patch moves the two imported functions to different locations: - `library.network_environment.get_network_config` is refactored into a generic utility function in `tripleo_validations.utils`. - `library.network_environment.open_network_environment_files` is copied to `library.network_environment.switch_vlans` (this is an ugly duplication, but the function is going to be removed from the original location in a different patch: https://review.openstack.org/#/c/547518/) Change-Id: If3bc62df730739640341386022c0cd99295b880b Closes-Bug: #1761717
This commit is contained in:
parent
008bf28bfa
commit
d0dc69c1eb
@ -49,99 +49,6 @@ class TestNicConfigs(base.TestCase):
|
|||||||
self.assertEqual("The 'config' property of 'foo' must be"
|
self.assertEqual("The 'config' property of 'foo' must be"
|
||||||
" a dictionary.", errors[0])
|
" a dictionary.", errors[0])
|
||||||
|
|
||||||
def test_get_network_config(self):
|
|
||||||
# Test config lookup using current format (t-h-t >= Ocata)
|
|
||||||
resources = {
|
|
||||||
'properties': {
|
|
||||||
'config': {
|
|
||||||
'str_replace': {
|
|
||||||
'params': {
|
|
||||||
'$network_config': {
|
|
||||||
'network_config': [
|
|
||||||
'current'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.assertEqual(
|
|
||||||
validation.get_network_config(resources, 'foo')[0],
|
|
||||||
'current')
|
|
||||||
|
|
||||||
def test_get_network_config_returns_none_if_not_found(self):
|
|
||||||
# get_network_config should return None if
|
|
||||||
# any of the keys cannot be found in the resources tree:
|
|
||||||
# `properties`, `config`, `network_config`
|
|
||||||
no_properties = {
|
|
||||||
'bar': {
|
|
||||||
'config': {
|
|
||||||
'str_replace': {
|
|
||||||
'params': {
|
|
||||||
'$network_config': {
|
|
||||||
'network_config': [
|
|
||||||
'current'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
no_config = {
|
|
||||||
'properties': {
|
|
||||||
'bar': {
|
|
||||||
'str_replace': {
|
|
||||||
'params': {
|
|
||||||
'$network_config': {
|
|
||||||
'network_config': [
|
|
||||||
'current'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
no_network_config = {
|
|
||||||
'properties': {
|
|
||||||
'config': {
|
|
||||||
'str_replace': {
|
|
||||||
'params': {
|
|
||||||
'$network_config': {
|
|
||||||
'bar': {
|
|
||||||
'some': 'val'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.assertEqual(
|
|
||||||
validation.get_network_config(no_properties, 'foo'), None)
|
|
||||||
self.assertEqual(validation.get_network_config(no_config, 'foo'), None)
|
|
||||||
self.assertEqual(
|
|
||||||
validation.get_network_config(no_network_config, 'foo'), None)
|
|
||||||
|
|
||||||
def test_get_network_config_old_format(self):
|
|
||||||
# Test config lookup using format used in t-h-t <= Newton
|
|
||||||
resources = {
|
|
||||||
'properties': {
|
|
||||||
'config': {
|
|
||||||
'os_net_config': {
|
|
||||||
'network_config': [
|
|
||||||
'old'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.assertEqual(
|
|
||||||
validation.get_network_config(resources, 'foo')[0],
|
|
||||||
'old')
|
|
||||||
|
|
||||||
def nic_data(self, bridges):
|
def nic_data(self, bridges):
|
||||||
return {
|
return {
|
||||||
'resources': {
|
'resources': {
|
||||||
|
120
tripleo_validations/tests/test_utils.py
Normal file
120
tripleo_validations/tests/test_utils.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# -*- 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.
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
|
from tripleo_validations.tests import base
|
||||||
|
from tripleo_validations import utils
|
||||||
|
|
||||||
|
PATH = [
|
||||||
|
('properties', collections.Mapping, 'dictionary'),
|
||||||
|
('config', collections.Mapping, 'dictionary'),
|
||||||
|
('network_config', collections.Iterable, 'list'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetNested(base.TestCase):
|
||||||
|
|
||||||
|
def test_get_nested(self):
|
||||||
|
# Test config lookup using current format (t-h-t >= Ocata)
|
||||||
|
resources = {
|
||||||
|
'properties': {
|
||||||
|
'config': {
|
||||||
|
'str_replace': {
|
||||||
|
'params': {
|
||||||
|
'$network_config': {
|
||||||
|
'network_config': [
|
||||||
|
'current'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.assertEqual(
|
||||||
|
utils.get_nested(resources, 'foo', PATH[:])[0],
|
||||||
|
'current')
|
||||||
|
|
||||||
|
def test_get_nested_returns_none_if_not_found(self):
|
||||||
|
# get_nested should return None if
|
||||||
|
# any of the keys cannot be found in the resources tree:
|
||||||
|
# `properties`, `config`, `network_config`
|
||||||
|
no_properties = {
|
||||||
|
'bar': {
|
||||||
|
'config': {
|
||||||
|
'str_replace': {
|
||||||
|
'params': {
|
||||||
|
'$network_config': {
|
||||||
|
'network_config': [
|
||||||
|
'current'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
no_config = {
|
||||||
|
'properties': {
|
||||||
|
'bar': {
|
||||||
|
'str_replace': {
|
||||||
|
'params': {
|
||||||
|
'$network_config': {
|
||||||
|
'network_config': [
|
||||||
|
'current'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
no_network_config = {
|
||||||
|
'properties': {
|
||||||
|
'config': {
|
||||||
|
'str_replace': {
|
||||||
|
'params': {
|
||||||
|
'$network_config': {
|
||||||
|
'bar': {
|
||||||
|
'some': 'val'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.assertEqual(
|
||||||
|
utils.get_nested(no_properties, 'foo', PATH[:]), None)
|
||||||
|
self.assertEqual(utils.get_nested(no_config, 'foo', PATH[:]), None)
|
||||||
|
self.assertEqual(
|
||||||
|
utils.get_nested(no_network_config, 'foo', PATH[:]), None)
|
||||||
|
|
||||||
|
def test_get_nested_old_format(self):
|
||||||
|
# Test config lookup using format used in t-h-t <= Newton
|
||||||
|
resources = {
|
||||||
|
'properties': {
|
||||||
|
'config': {
|
||||||
|
'os_net_config': {
|
||||||
|
'network_config': [
|
||||||
|
'old'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.assertEqual(
|
||||||
|
utils.get_nested(resources, 'foo', PATH[:])[0],
|
||||||
|
'old')
|
@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
from keystoneauth1.identity import generic as ks_id
|
from keystoneauth1.identity import generic as ks_id
|
||||||
from keystoneauth1 import session
|
from keystoneauth1 import session
|
||||||
from six import string_types
|
from six import string_types
|
||||||
@ -52,3 +54,33 @@ def filtered(obj):
|
|||||||
"""Only return properties of obj whose value can be properly serialized."""
|
"""Only return properties of obj whose value can be properly serialized."""
|
||||||
return {k: v for k, v in obj.__dict__.items()
|
return {k: v for k, v in obj.__dict__.items()
|
||||||
if isinstance(v, (string_types, int, list, dict, type(None)))}
|
if isinstance(v, (string_types, int, list, dict, type(None)))}
|
||||||
|
|
||||||
|
|
||||||
|
def get_nested(data, name, path):
|
||||||
|
# Finds and returns a property from a nested dictionary by
|
||||||
|
# following a path of a defined set of property names and types.
|
||||||
|
|
||||||
|
def deep_find_key(key_data, data, name):
|
||||||
|
key, instance_type, instance_name = key_data
|
||||||
|
if key in data.keys():
|
||||||
|
if not isinstance(data[key], instance_type):
|
||||||
|
raise ValueError("The '{}' property of '{}' must be a {}."
|
||||||
|
"".format(key, name, instance_name))
|
||||||
|
return data[key]
|
||||||
|
for item in data.values():
|
||||||
|
if isinstance(item, collections.Mapping):
|
||||||
|
return deep_find_key(key_data, item, name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(data, collections.Mapping):
|
||||||
|
raise ValueError(
|
||||||
|
"'{}' is not a valid resource.".format(name))
|
||||||
|
|
||||||
|
current_value = data
|
||||||
|
while len(path) > 0:
|
||||||
|
key_data = path.pop(0)
|
||||||
|
current_value = deep_find_key(key_data, current_value, name)
|
||||||
|
if current_value is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
return current_value
|
||||||
|
@ -26,6 +26,9 @@ import six
|
|||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
from os_net_config import validator
|
from os_net_config import validator
|
||||||
|
|
||||||
|
from tripleo_validations.utils import get_nested
|
||||||
|
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
DOCUMENTATION = '''
|
||||||
---
|
---
|
||||||
module: network_environment
|
module: network_environment
|
||||||
@ -135,42 +138,6 @@ def validate_network_environment(network_data, nic_configs):
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def get_network_config(resource, resource_name):
|
|
||||||
# Finds and returns `properties > config > network_config` inside
|
|
||||||
# a resources dictionary, with optional nesting levels in between.
|
|
||||||
|
|
||||||
def deep_find_key(key_data, resource, resource_name):
|
|
||||||
key, instance_type, instance_name = key_data
|
|
||||||
if key in resource.keys():
|
|
||||||
if not isinstance(resource[key], instance_type):
|
|
||||||
raise ValueError("The '{}' property of '{}' must be a {}."
|
|
||||||
"".format(key, resource_name, instance_name))
|
|
||||||
return resource[key]
|
|
||||||
for item in resource.values():
|
|
||||||
if isinstance(item, collections.Mapping):
|
|
||||||
return deep_find_key(key_data, item, resource_name)
|
|
||||||
return None
|
|
||||||
|
|
||||||
keys = [
|
|
||||||
('properties', collections.Mapping, 'dictionary'),
|
|
||||||
('config', collections.Mapping, 'dictionary'),
|
|
||||||
('network_config', collections.Iterable, 'list'),
|
|
||||||
]
|
|
||||||
current_value = resource
|
|
||||||
|
|
||||||
if not isinstance(resource, collections.Mapping):
|
|
||||||
raise ValueError(
|
|
||||||
"'{}' is not a valid resource.".format(resource_name))
|
|
||||||
|
|
||||||
while len(keys) > 0:
|
|
||||||
key_data = keys.pop(0)
|
|
||||||
current_value = deep_find_key(key_data, current_value, resource_name)
|
|
||||||
if current_value is None:
|
|
||||||
break
|
|
||||||
|
|
||||||
return current_value
|
|
||||||
|
|
||||||
|
|
||||||
def check_nic_configs(path, nic_data):
|
def check_nic_configs(path, nic_data):
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
@ -185,7 +152,12 @@ def check_nic_configs(path, nic_data):
|
|||||||
"a dictionary."]
|
"a dictionary."]
|
||||||
for name, resource in six.iteritems(resources):
|
for name, resource in six.iteritems(resources):
|
||||||
try:
|
try:
|
||||||
bridges = get_network_config(resource, name)
|
nested_path = [
|
||||||
|
('properties', collections.Mapping, 'dictionary'),
|
||||||
|
('config', collections.Mapping, 'dictionary'),
|
||||||
|
('network_config', collections.Iterable, 'list'),
|
||||||
|
]
|
||||||
|
bridges = get_nested(resource, name, nested_path)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
errors.append('{}'.format(e))
|
errors.append('{}'.format(e))
|
||||||
continue
|
continue
|
||||||
|
@ -15,12 +15,13 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
import os.path
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule # noqa
|
from ansible.module_utils.basic import AnsibleModule # noqa
|
||||||
import validations.library.network_environment as net_utils
|
from tripleo_validations import utils
|
||||||
|
|
||||||
DOCUMENTATION = '''
|
DOCUMENTATION = '''
|
||||||
---
|
---
|
||||||
@ -62,6 +63,32 @@ EXAMPLES = '''
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def open_network_environment_files(netenv_path, template_files):
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
network_data = yaml.safe_load(template_files[netenv_path])
|
||||||
|
except Exception as e:
|
||||||
|
return ({}, {}, ["Can't open network environment file '{}': {}"
|
||||||
|
.format(netenv_path, e)])
|
||||||
|
nic_configs = []
|
||||||
|
resource_registry = network_data.get('resource_registry', {})
|
||||||
|
for nic_name, relative_path in six.iteritems(resource_registry):
|
||||||
|
if nic_name.endswith("Net::SoftwareConfig"):
|
||||||
|
nic_config_path = os.path.normpath(
|
||||||
|
os.path.join(os.path.dirname(netenv_path), relative_path))
|
||||||
|
try:
|
||||||
|
nic_configs.append((
|
||||||
|
nic_name, nic_config_path,
|
||||||
|
yaml.safe_load(template_files[nic_config_path])))
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(
|
||||||
|
"Can't open the resource '{}' reference file '{}': {}"
|
||||||
|
.format(nic_name, nic_config_path, e))
|
||||||
|
|
||||||
|
return (network_data, nic_configs, errors)
|
||||||
|
|
||||||
|
|
||||||
def validate_switch_vlans(netenv_path, template_files, introspection_data):
|
def validate_switch_vlans(netenv_path, template_files, introspection_data):
|
||||||
"""Check if VLAN exists in introspection data for node
|
"""Check if VLAN exists in introspection data for node
|
||||||
|
|
||||||
@ -73,7 +100,7 @@ def validate_switch_vlans(netenv_path, template_files, introspection_data):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
network_data, nic_configs, errors =\
|
network_data, nic_configs, errors =\
|
||||||
net_utils.open_network_environment_files(netenv_path, template_files)
|
open_network_environment_files(netenv_path, template_files)
|
||||||
warnings = []
|
warnings = []
|
||||||
vlans_in_templates = False
|
vlans_in_templates = False
|
||||||
|
|
||||||
@ -95,7 +122,12 @@ def validate_switch_vlans(netenv_path, template_files, introspection_data):
|
|||||||
"and it must be a dictionary."]
|
"and it must be a dictionary."]
|
||||||
for name, resource in six.iteritems(resources):
|
for name, resource in six.iteritems(resources):
|
||||||
try:
|
try:
|
||||||
nw_config = net_utils.get_network_config(resource, name)
|
nested_path = [
|
||||||
|
('properties', collections.Mapping, 'dictionary'),
|
||||||
|
('config', collections.Mapping, 'dictionary'),
|
||||||
|
('network_config', collections.Iterable, 'list'),
|
||||||
|
]
|
||||||
|
nw_config = utils.get_nested(resource, name, nested_path)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
errors.append('{}'.format(e))
|
errors.append('{}'.format(e))
|
||||||
continue
|
continue
|
||||||
|
Loading…
Reference in New Issue
Block a user