Implement intermediary file validation

Implements JSON schema validation for intermediary YAML files

Adds tests for intermediary validation

Change-Id: Iaa385d265b027426f8e5f2376462ffb4c0d1d3fa
This commit is contained in:
Ian H Pittwood 2019-08-26 14:31:33 -05:00
parent 7f1ed8bcf9
commit 0bed580daa
9 changed files with 688 additions and 88 deletions

46
Pipfile.lock generated
View File

@ -128,7 +128,7 @@
}, },
"spyglass-plugin-xls": { "spyglass-plugin-xls": {
"git": "https://opendev.org/airship/spyglass-plugin-xls.git", "git": "https://opendev.org/airship/spyglass-plugin-xls.git",
"ref": "269ce7154f88a51e5dd9b883e2f7f15d5326a905" "ref": "3b794d05ffd4731e1b7c4f23bc73a3d73f5ba1c1"
} }
}, },
"develop": { "develop": {
@ -198,13 +198,6 @@
], ],
"version": "==4.5.4" "version": "==4.5.4"
}, },
"ddt": {
"hashes": [
"sha256:474546b4020ce8a2f9550ba8899c28aa2c284c7bbf175bddede98be949d1ca7c",
"sha256:d13e6af8f36238e89d00f4ebccf2bda4f6d1878be560a6600689e42077e164e3"
],
"version": "==1.2.1"
},
"execnet": { "execnet": {
"hashes": [ "hashes": [
"sha256:0dd40ad3b960aae93bdad7fe1c3f049bbcc8fba47094655a4301f5b33e906816", "sha256:0dd40ad3b960aae93bdad7fe1c3f049bbcc8fba47094655a4301f5b33e906816",
@ -226,12 +219,6 @@
], ],
"version": "==2.6.2" "version": "==2.6.2"
}, },
"gitdb": {
"hashes": [
"sha256:a3ebbc27be035a2e874ed904df516e35f4a29a778a764385de09de9e0f139658"
],
"version": "==0.6.4"
},
"gitdb2": { "gitdb2": {
"hashes": [ "hashes": [
"sha256:83361131a1836661a155172932a13c08bda2db3674e4caa32368aa6eb02f38c2", "sha256:83361131a1836661a155172932a13c08bda2db3674e4caa32368aa6eb02f38c2",
@ -241,10 +228,10 @@
}, },
"gitpython": { "gitpython": {
"hashes": [ "hashes": [
"sha256:556b64796c5e268b35e3b431a429e813ad54aa178e1baaec2a9ba82e8575a89e", "sha256:947cc75913e7b6da108458136607e2ee0e40c20be1e12d4284e7c6c12956c276",
"sha256:629867ebf609cef21bb9d849039e281e25963fb7d714a2f6bacc1ecce4800293" "sha256:d2f4945f8260f6981d724f5957bc076398ada55cb5d25aaee10108bcdc894100"
], ],
"version": "==3.0.0" "version": "==3.0.2"
}, },
"hacking": { "hacking": {
"hashes": [ "hashes": [
@ -259,6 +246,7 @@
"sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", "sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8",
"sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3" "sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3"
], ],
"markers": "python_version < '3.8'",
"version": "==0.19" "version": "==0.19"
}, },
"mccabe": { "mccabe": {
@ -327,11 +315,11 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:6ef6d06de77ce2961156013e9dff62f1b2688aa04d0dc244299fe7d67e09370d", "sha256:95b1f6db806e5b1b5b443efeb58984c24945508f93a866c1719e1a507a957d7c",
"sha256:a736fed91c12681a7b34617c8fcefe39ea04599ca72c608751c31d89579a3f77" "sha256:c3d5020755f70c82eceda3feaf556af9a341334414a8eca521a18f463bcead88"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.0.1" "version": "==5.1.1"
}, },
"pytest-cov": { "pytest-cov": {
"hashes": [ "hashes": [
@ -380,12 +368,6 @@
], ],
"version": "==1.12.0" "version": "==1.12.0"
}, },
"smmap": {
"hashes": [
"sha256:0e2b62b497bd5f0afebc002eda4d90df9d209c30ef257e8673c90a6b5c119d62"
],
"version": "==0.9.0"
},
"smmap2": { "smmap2": {
"hashes": [ "hashes": [
"sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde",
@ -417,10 +399,10 @@
}, },
"virtualenv": { "virtualenv": {
"hashes": [ "hashes": [
"sha256:6cb2e4c18d22dbbe283d0a0c31bb7d90771a606b2cb3415323eea008eaee6a9d", "sha256:94a6898293d07f84a98add34c4df900f8ec64a570292279f6d91c781d37fd305",
"sha256:909fe0d3f7c9151b2df0a2cb53e55bdb7b0d61469353ff7a49fd47b0f0ab9285" "sha256:f6fc312c031f2d2344f885de114f1cb029dfcffd26aa6e57d2ee2296935c4e7d"
], ],
"version": "==16.7.2" "version": "==16.7.4"
}, },
"wcwidth": { "wcwidth": {
"hashes": [ "hashes": [
@ -439,10 +421,10 @@
}, },
"zipp": { "zipp": {
"hashes": [ "hashes": [
"sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
"sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
], ],
"version": "==0.5.2" "version": "==0.6.0"
} }
} }
} }

View File

@ -79,7 +79,22 @@ FORCE_OPTION = click.option(
'force', 'force',
is_flag=True, is_flag=True,
default=False, default=False,
help="Forces manifests to be written, regardless of undefined data.") help='Forces manifests to be written, regardless of undefined data.')
INTERMEDIARY_SCHEMA_OPTION = click.option(
'--intermediary-schema',
'intermediary_schema',
type=click.Path(exists=True, readable=True, dir_okay=False),
default=pkg_resources.resource_filename(
'spyglass', "schemas/intermediary_schema.json"),
help='Path to the intermediary schema to be used for validation.')
NO_INTERMEDIARY_VALIDATION_OPTION = click.option(
'--no-validation',
'no_validation',
is_flag=True,
default=False,
help='Skips validation on generated intermediary data.')
@click.option( @click.option(
@ -135,7 +150,9 @@ def intermediary_processor(plugin_type, **kwargs):
# Apply design rules to the data # Apply design rules to the data
LOG.info("Apply design rules to the extracted data") LOG.info("Apply design rules to the extracted data")
process_input_ob = ProcessDataSource( process_input_ob = ProcessDataSource(
kwargs['site_name'], data_extractor.data) kwargs['site_name'], data_extractor.data,
kwargs.get('intermediary_schema', None),
kwargs.get('no_validation', False))
return process_input_ob return process_input_ob

View File

@ -34,8 +34,8 @@ class SpyglassBaseException(Exception):
class UnsupportedPlugin(SpyglassBaseException): class UnsupportedPlugin(SpyglassBaseException):
"""Exception that occurs when a plugin is called that does not exist """Exception that occurs when a plugin is called that does not exist
:param plugin_name: name of the specified plugin :keyword plugin_name: name of the specified plugin
:param entry_point: the package used to access plugin_name :keyword entry_point: the package used to access plugin_name
""" """
message = ( message = (
'%(plugin_name) was not found in the package %(entry_point) ' '%(plugin_name) was not found in the package %(entry_point) '
@ -45,10 +45,19 @@ class UnsupportedPlugin(SpyglassBaseException):
# Data Extractor exceptions # Data Extractor exceptions
class IntermediaryValidationException(SpyglassBaseException):
"""Exception that occurs when the generated intermediary fails validation
:keyword errors: list of errors generated by the jsonschema validator
"""
message = (
'Intermediary validation failed with the following errors: %(errors)')
class InvalidIntermediary(SpyglassBaseException): class InvalidIntermediary(SpyglassBaseException):
"""Exception that occurs when data is missing from the intermediary file """Exception that occurs when data is missing from the intermediary file
:param key: dictionary key that Spyglass attempted to access :keyword key: dictionary key that Spyglass attempted to access
""" """
message = '%(key) is not defined in the given intermediary file.' message = '%(key) is not defined in the given intermediary file.'
@ -59,8 +68,8 @@ class InvalidIntermediary(SpyglassBaseException):
class PathDoesNotExistError(SpyglassBaseException): class PathDoesNotExistError(SpyglassBaseException):
"""Exception that occurs when the document or schema path does not exist """Exception that occurs when the document or schema path does not exist
:param file_type: type of the files being accessed, documents or schemas :keyword file_type: type of the files being accessed, documents or schemas
:param path: nonexistent path attempted to access :keyword path: nonexistent path attempted to access
""" """
message = '%(file_type) path: %(path) does not exist.' message = '%(file_type) path: %(path) does not exist.'
@ -68,8 +77,8 @@ class PathDoesNotExistError(SpyglassBaseException):
class UnexpectedFileType(SpyglassBaseException): class UnexpectedFileType(SpyglassBaseException):
"""Exception that occurs when an unexpected file type is given """Exception that occurs when an unexpected file type is given
:param found_ext: the extension of the file given :keyword found_ext: the extension of the file given
:param expected_ext: the extension that was expected for the file :keyword expected_ext: the extension that was expected for the file
""" """
message = ( message = (
'Unexpected file type %(found_ext), ' 'Unexpected file type %(found_ext), '
@ -82,7 +91,7 @@ class DirectoryEmptyError(SpyglassBaseException):
This exception can occur when either a directory is empty or if a directory This exception can occur when either a directory is empty or if a directory
does not have any files with the correct file extension. does not have any files with the correct file extension.
:param ext: file extension being searched for :keyword ext: file extension being searched for
:param path: path being searched for files of the specified extension :keyword path: path being searched for files of the specified extension
""" """
message = 'No files with %(ext) extension found in document path %(path)' message = 'No files with %(ext) extension found in document path %(path)'

View File

@ -12,29 +12,38 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import copy
import json import json
import logging import logging
import os import os
import pprint import pprint
import sys
import jsonschema from jsonschema import Draft7Validator
from netaddr import IPNetwork from netaddr import IPNetwork
from pkg_resources import resource_filename from pkg_resources import resource_filename
import yaml import yaml
from spyglass import exceptions
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class ProcessDataSource(object): class ProcessDataSource(object):
def __init__(self, region, extracted_data): def __init__(
self,
region,
extracted_data,
intermediary_schema=None,
no_validation=True):
# Initialize intermediary and save site type # Initialize intermediary and save site type
self.host_type = {} self.host_type = {}
self.sitetype = None self.sitetype = None
self.genesis_node = None self.genesis_node = None
self.network_subnets = None self.network_subnets = None
self.region_name = region self.region_name = region
self.no_validation = no_validation
if intermediary_schema and not self.no_validation:
with open(intermediary_schema, 'r') as loaded_schema:
self.intermediary_schema = json.load(loaded_schema)
LOG.info("Loading plugin data source") LOG.info("Loading plugin data source")
self.data = extracted_data self.data = extracted_data
@ -74,53 +83,24 @@ class ProcessDataSource(object):
"Genesis Node Details:\n{}".format( "Genesis Node Details:\n{}".format(
pprint.pformat(self.genesis_node))) pprint.pformat(self.genesis_node)))
@staticmethod def _validate_intermediary_data(self):
def _validate_intermediary_data(data):
"""Validates the intermediary data before generating manifests. """Validates the intermediary data before generating manifests.
It checks whether the data types and data format are as expected. It checks whether the data types and data format are as expected.
The method validates this with regex pattern defined for each The method validates this with regex pattern defined for each
data type. data type.
""" """
# TODO(ian-pittwood): Implement intermediary validation or remove
LOG.info("Validating Intermediary data") LOG.info("Validating Intermediary data")
# Performing a deep copy validator = Draft7Validator(self.intermediary_schema)
temp_data = copy.deepcopy(data) errors = sorted(
# Converting baremetal dict to list. validator.iter_errors(self.data.dict_from_class()),
baremetal_list = [] key=lambda e: e.path)
for rack in temp_data.baremetal: if errors:
temp = [{k: v} for k, v in temp_data["baremetal"][rack].items()] raise exceptions.IntermediaryValidationException(errors=errors)
baremetal_list = baremetal_list + temp
temp_data["baremetal"] = baremetal_list
schema_dir = resource_filename("spyglass", "schemas/")
schema_file = schema_dir + "data_schema.json"
json_data = json.loads(json.dumps(temp_data))
with open(schema_file, "r") as f:
json_schema = json.load(f)
try:
# Suppressing writing of data2.json. Can use it for debugging
# with open('data2.json', 'w') as outfile:
# json.dump(temp_data, outfile, sort_keys=True, indent=4)
jsonschema.validate(json_data, json_schema)
except jsonschema.exceptions.ValidationError as e:
LOG.error("Validation Error")
LOG.error("Message:{}".format(e.message))
LOG.error("Validator_path:{}".format(e.path))
LOG.error("Validator_pattern:{}".format(e.validator_value))
LOG.error("Validator:{}".format(e.validator))
sys.exit()
except jsonschema.exceptions.SchemaError as e:
LOG.error("Schema Validation Error!!")
LOG.error("Message:{}".format(e.message))
LOG.error("Schema:{}".format(e.schema))
LOG.error("Validator_value:{}".format(e.validator_value))
LOG.error("Validator:{}".format(e.validator))
LOG.error("path:{}".format(e.path))
sys.exit()
LOG.info("Data validation Passed!") LOG.info("Data validation Passed!")
return
def _apply_design_rules(self): def _apply_design_rules(self):
"""Applies design rules from rules.yaml """Applies design rules from rules.yaml
@ -309,5 +289,6 @@ class ProcessDataSource(object):
self._apply_design_rules() self._apply_design_rules()
self._get_genesis_node_details() self._get_genesis_node_details()
# This will validate the extracted data from different sources. # This will validate the extracted data from different sources.
# self._validate_intermediary_data(self.data) if not self.no_validation and self.intermediary_schema:
self._validate_intermediary_data()
return self.data return self.data

View File

@ -8,7 +8,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"baremetal": { "baremetal": {
"type": "array", "type": "object",
"items": { "items": {
"type": "object", "type": "object",
"$ref": "#/definitions/baremetal_list" "$ref": "#/definitions/baremetal_list"

View File

@ -29,6 +29,15 @@ def site_document_data_objects(request):
request.cls.site_document_data = site_document_data_factory(yaml_data) request.cls.site_document_data = site_document_data_factory(yaml_data)
@pytest.fixture(scope='class')
def invalid_site_document_data_objects(request):
with open(os.path.join(FIXTURE_DIR, 'invalid_intermediary.yaml'),
'r') as f:
yaml_data = yaml.safe_load(f)
request.cls.invalid_site_document_data = site_document_data_factory(
yaml_data)
@pytest.fixture(scope='class') @pytest.fixture(scope='class')
def rules_data(request): def rules_data(request):
with open(os.path.join(FIXTURE_DIR, 'rules.yaml'), 'r') as f: with open(os.path.join(FIXTURE_DIR, 'rules.yaml'), 'r') as f:

View File

@ -0,0 +1,373 @@
{
"$schema": "http://json-schema.org/schema#",
"metadata": {
"name": "spyglass/Intermediary/v1"
},
"title": "All",
"description": "All information",
"type": "object",
"properties": {
"baremetal": {
"type": "object",
"items": {
"type": "object",
"$ref": "#/definitions/baremetal_list"
}
},
"network": {
"type": "object",
"properties": {
"bgp": {
"type": "object",
"$ref": "#/definitions/bgp"
},
"vlan_network_data": {
"type": "array",
"$ref": "#/definitions/vlan_network_data"
}
},
"required": [
"bgp",
"vlan_network_data"
]
},
"site_info": {
"type": "object",
"$ref": "#/definitions/site_info"
},
"storage": {
"type": "object",
"$ref": "#/definitions/storage"
}
},
"required": [
"baremetal",
"network",
"site_info",
"storage"
],
"definitions": {
"baremetal_list": {
"type": "object",
"patternProperties": {
".*": {
"properties": {
"ip": {
"type": "object",
"properties": {
"calico": {
"type": "string",
"pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
},
"oam": {
"type": "string",
"pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
},
"oob": {
"type": "string",
"pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
},
"overlay": {
"type": "string",
"pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
},
"pxe": {
"type": "string",
"pattern": "^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(#CHANGE_ME)$"
},
"storage": {
"type": "string",
"pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
}
},
"required" :[
"calico",
"oam",
"oob",
"overlay",
"pxe",
"storage"
]
},
"host_profile": {
"description": "Host profile of the host",
"type": "string",
"pattern": "^([a-zA-Z]+)|(#CHANGE_ME)$"
},
"type": {
"description": "Host profile type:Compute or Controller or genesis ",
"type": "string",
"pattern": "^(?i)compute|controller|genesis$"
}
},
"required" :[
"ip",
"host_profile",
"type"
]
}
}
},
"bgp": {
"type": "object",
"properties": {
"asnumber": {
"type": "integer",
"pattern": "^[0-9]{1,10}$"
},
"peer_asnumber": {
"type": "integer",
"pattern": "^[0-9]{1,10}$"
},
"peers": {
"type": "array",
"items": [
{
"type": "string",
"pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
}
]
}
},
"required": [
"asnumber",
"peer_asnumber",
"peers"
]
},
"vlan_network_data": {
"type": "object",
"properties": {
"calico": {
"type": "object",
"properties": {
"subnet": {
"description": "Subnet address of the network",
"type": "array",
"items": {
"type": "string",
"pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([0-9]|[1-2][0-9]|3[0-2])$"
}
},
"vlan": {
"description": "Vlan id of the network",
"type": "string",
"pattern": "^([0-9]|[0-9][0-9]|[0-9][0-9][0-9]|[0-3][0-9][0-9][0-9]|40[0-9][0-5])$"
}
},
"required": [
"subnet",
"vlan"
]
},
"ingress": {
"type": "object",
"properties": {
"subnet": {
"description": "Subnet address of the network",
"type": "array",
"items": [
{
"type": "string",
"pattern":"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([0-9]|[1-2][0-9]|3[0-2])$"
}
]
}
},
"required": [
"subnet"
]
},
"oam": {
"type": "object",
"properties": {
"subnet": {
"description": "Subnet address of the network",
"type": "array",
"items": {
"type": "string",
"pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([0-9]|[1-2][0-9]|3[0-2])$"
}
},
"vlan": {
"description": "Vlan id of the network",
"type": "string",
"pattern": "^([0-9]|[0-9][0-9]|[0-9][0-9][0-9]|[0-3][0-9][0-9][0-9]|40[0-9][0-5])$"
}
},
"required": [
"subnet",
"vlan"
]
},
"oob": {
"type": "object",
"properties": {
"subnet": {
"description": "Subnet address of the network",
"type": "array",
"items": {
"type": "string",
"pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([0-9]|[1-2][0-9]|3[0-2])$"
}
},
"vlan": {
"description": "Vlan id of the network",
"type": "string",
"pattern": "^([0-9]|[0-9][0-9]|[0-9][0-9][0-9]|[0-3][0-9][0-9][0-9]|40[0-9][0-5])?$"
}
},
"required": [
"subnet"
]
},
"pxe": {
"type": "object",
"properties": {
"subnet": {
"description": "Subnet address of the network",
"type": "array",
"items": {
"type": "string",
"pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([0-9]|[1-2][0-9]|3[0-2])$"
}
},
"vlan": {
"description": "Vlan id of the network",
"type": "string",
"pattern": "^([0-9]|[0-9][0-9]|[0-9][0-9][0-9]|[0-3][0-9][0-9][0-9]|40[0-9][0-5])$"
}
},
"required": [
"subnet",
"vlan"
]
},
"storage": {
"type": "object",
"properties": {
"subnet": {
"description": "Subnet address of the network",
"type": "array",
"items": {
"type": "string",
"pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/([0-9]|[1-2][0-9]|3[0-2])$"
}
},
"vlan": {
"description": "Vlan id of the network",
"type": "string",
"pattern": "^([0-9]|[0-9][0-9]|[0-9][0-9][0-9]|[0-3][0-9][0-9][0-9]|40[0-9][0-5])$"
}
},
"required": [
"subnet",
"vlan"
]
}
},
"required" :[
"calico",
"ingress",
"oam",
"oob",
"overlay",
"pxe",
"storage"
]
},
"site_info": {
"type": "object",
"properties": {
"dns": {
"type": "object",
"properties": {
"servers": {
"type": "string",
"pattern": "^((((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]),)+)|(((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))+))+$"
}
}
},
"ntp": {
"type": "object",
"properties": {
"servers": {
"type": "string",
"pattern": "^((((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]),)+)|(((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))+))+$"
}
}
},
"ldap": {
"type": "object",
"properties": {
"common_name": {
"type": "string",
"pattern": "\\W+|\\w+"
},
"subdomain": {
"type": "string",
"pattern": "(?i)\\w+"
},
"url": {
"type": "string",
"pattern": "^\\w+://\\w+.*\\.[a-zA-Z]{2,3}$"
}
},
"required": [
"common_name",
"subdomain",
"url"
]
},
"country": {
"type": "string",
"pattern": "(?i)\\w+"
},
"name": {
"type": "string",
"pattern": "(?i)\\w+"
},
"state": {
"type": "string",
"pattern": "(?i)\\w+"
},
"sitetype": {
"type": "string",
"pattern": "(?i)\\w+"
},
"physical_location_id": {
"type": "string",
"pattern": "^\\w+"
},
"domain": {
"type": "string",
"pattern": "^\\w+.*\\.[a-zA-Z]{2,3}$"
}
},
"required": [
"dns",
"ntp",
"ldap",
"country",
"name",
"state",
"sitetype",
"physical_location_id",
"domain"
]
},
"storage": {
"type": "object",
"patternProperties": {
"ceph": {
"controller": {
"osd_count": {
"type": "integer",
"pattern": "^[0-9]{1,2}$"
}
}
}
}
}
}
}

View File

@ -0,0 +1,216 @@
baremetal:
rack72:
cab2r72c12:
host_profile: dp-r720
ip:
calico: 30.29.1.12
oam: 10.0.220.12
oob: 10.0.220.140
overlay: 30.19.0.12
pxe: 30.30.4.12
storage: 30.31.1.12
type: compute
cab2r72c13:
host_profile: dp-r720
ip:
calico: 30.29.1.13
oam: 10.0.220.13
oob: 10.0.220.141
overlay: 30.19.0.13
pxe: 30.30.4.13
storage: 30.31.1.13
type: compute
cab2r72c14:
host_profile: dp-r720
ip:
calico: 30.29.1.14
oam: 10.0.220.14
oob: 10.0.220.142
overlay: 30.19.0.14
pxe: 30.30.4.14
storage: 30.31.1.14
type: compute
cab2r72c15:
host_profile: dp-r720
ip:
calico: 30.29.1.15
oam: 10.0.220.15
oob: 10.0.220.143
overlay: 30.19.0.15
pxe: 30.30.4.15
storage: 30.31.1.15
type: compute
cab2r72c16:
host_profile: cp-r720
ip:
calico: 30.29.1.16
oam: 10.0.220.16
oob: 10.0.220.144
overlay: 30.19.0.16
pxe: 30.30.4.16
storage: 30.31.1.16
name: cab2r72c16
type: genesis
cab2r72c17:
host_profile: cp-r720
ip:
calico: 30.29.1.17
oam: 10.0.220.17
oob: 10.0.220.145
overlay: 30.19.0.17
pxe: 30.30.4.17
storage: 30.31.1.17
type: controller
rack73:
cab2r73c12:
host_profile: dp-r720
ip:
calico: 30.29.1.18
oam: 10.0.220.18
oob: 10.0.220.146
overlay: 30.19.0.18
pxe: 30.30.4.18
storage: 30.31.1.18
type: compute
cab2r73c13:
host_profile: dp-r720
ip:
calico: 30.29.1.19
oam: 10.0.220.19
oob: 10.0.220.147
overlay: 30.19.0.19
pxe: 30.30.4.19
storage: 30.31.1.19
type: compute
cab2r73c14:
host_profile: dp-r720
ip:
calico: 30.29.1.20
oam: 10.0.220.20
oob: 10.0.220.148
overlay: 30.19.0.20
pxe: 30.30.4.20
storage: 30.31.1.20
type: compute
cab2r73c15:
host_profile: dp-r720
ip:
calico: 30.29.1.21
oam: 10.0.220.21
oob: 10.0.220.149
overlay: 30.19.0.21
pxe: 30.30.4.21
storage: 30.31.1.21
type: compute
cab2r73c16:
host_profile: cp-r720
ip:
calico: 30.29.1.22
oam: 10.0.220.22
oob: 10.0.220.150
overlay: 30.19.0.22
pxe: 30.30.4.22
storage: 30.31.1.22
type: controller
cab2r73c17:
host_profile: cp-r720
ip:
calico: 30.29.1.23
oam: 10.0.220.23
oob: 10.0.220.151
overlay: 30.19.0.23
pxe: 30.30.4.23
storage: 30.31.1.23
type: controller
network:
bgp:
asnumber: 64671
ingress_vip: 132.68.226.73
peer_asnumber: 64688
peers:
- 172.29.0.2
- 172.29.0.3
public_service_cidr: 132.68.226.72/29
vlan_network_data:
calico:
gateway: 30.29.1.1
reserved_end: 30.29.1.12
reserved_start: 30.29.1.1
routes: []
static_end: 30.29.1.126
static_start: 30.29.1.13
subnet:
- 30.29.1.0/25
vlan: '22'
ingress:
subnet:
- 132.68.226.72/29
oob:
gateway: 10.0.220.129
reserved_end: 10.0.220.138
reserved_start: 10.0.220.129
routes: []
static_end: 10.0.220.158
static_start: 10.0.220.139
subnet:
- 10.0.220.128/27
- 10.0.220.160/27
- 10.0.220.192/27
- 10.0.220.224/27
overlay:
gateway: 30.19.0.1
reserved_end: 30.19.0.12
reserved_start: 30.19.0.1
routes: []
static_end: 30.19.0.126
static_start: 30.19.0.13
subnet:
- 30.19.0.0/25
vlan: '24'
pxe:
dhcp_end: 30.30.4.126
dhcp_start: 30.30.4.64
gateway: 30.30.4.1
reserved_end: 30.30.4.12
reserved_start: 30.30.4.1
routes: []
static_end: 30.30.4.63
static_start: 30.30.4.13
subnet:
- 30.30.4.0/25
- 30.30.4.128/25
- 30.30.5.0/25
- 30.30.5.128/25
vlan: '21'
storage:
gateway: 30.31.1.1
reserved_end: 30.31.1.12
reserved_start: 30.31.1.1
routes: []
static_end: 30.31.1.126
static_start: 30.31.1.13
subnet:
- 30.31.1.0/25
vlan: '23'
region_name: test
site_info:
corridor: c1
country: SampleCountry
dns:
servers: 8.8.8.8,8.8.4.4,208.67.222.222
domain: atlantafoundry.com
ldap:
common_name: test
domain: example
subdomain: test
url: ldap://ldap.example.com
name: SampleSiteName
ntp:
servers: 10.10.10.10,20.20.20.20,30.30.30.30
physical_location_id: XXXXXX21
sitetype: foundry
state: New Jersey
storage:
ceph:
controller:
osd_count: 6

View File

@ -19,6 +19,7 @@ from unittest import mock
from netaddr import IPNetwork from netaddr import IPNetwork
from pytest import mark from pytest import mark
from spyglass import exceptions
from spyglass.parser.engine import ProcessDataSource from spyglass.parser.engine import ProcessDataSource
FIXTURE_DIR = os.path.join( FIXTURE_DIR = os.path.join(
@ -27,6 +28,7 @@ FIXTURE_DIR = os.path.join(
@mark.usefixtures('tmpdir') @mark.usefixtures('tmpdir')
@mark.usefixtures('site_document_data_objects') @mark.usefixtures('site_document_data_objects')
@mark.usefixtures('invalid_site_document_data_objects')
@mark.usefixtures('rules_data') @mark.usefixtures('rules_data')
class TestProcessDataSource(unittest.TestCase): class TestProcessDataSource(unittest.TestCase):
REGION_NAME = 'test' REGION_NAME = 'test'
@ -69,9 +71,20 @@ class TestProcessDataSource(unittest.TestCase):
obj._get_genesis_node_details() obj._get_genesis_node_details()
self.assertEqual(expected_result, obj.genesis_node) self.assertEqual(expected_result, obj.genesis_node)
@unittest.skip('Not in use.')
def test__validate_intermediary_data(self): def test__validate_intermediary_data(self):
pass schema_path = os.path.join(FIXTURE_DIR, 'intermediary_schema.json')
obj = ProcessDataSource(
self.REGION_NAME, self.site_document_data, schema_path, False)
result = obj._validate_intermediary_data()
self.assertIsNone(result)
def test__validate_intermediary_data_invalid(self):
schema_path = os.path.join(FIXTURE_DIR, 'intermediary_schema.json')
obj = ProcessDataSource(
self.REGION_NAME, self.invalid_site_document_data, schema_path,
False)
with self.assertRaises(exceptions.IntermediaryValidationException):
obj._validate_intermediary_data()
@mock.patch.object(ProcessDataSource, '_apply_rule_ip_alloc_offset') @mock.patch.object(ProcessDataSource, '_apply_rule_ip_alloc_offset')
@mock.patch.object(ProcessDataSource, '_apply_rule_hardware_profile') @mock.patch.object(ProcessDataSource, '_apply_rule_hardware_profile')