Enhance validation message flow
- Update validation message format to latest UCP standard - Support message levels of ERROR, WARN, INFO - Update ingestion and schema validation to use new validation message format Change-Id: Ic463badaea8025c5dece88b3999075ead3419bec
This commit is contained in:
parent
cff99e4d1c
commit
47a27981ec
@ -53,6 +53,14 @@ class InvalidDesignReference(DesignError):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class UnsupportedDocumentType(DesignError):
|
||||||
|
"""
|
||||||
|
**Message:** *Site definition document in an unknown format*.
|
||||||
|
|
||||||
|
**Troubleshoot:**
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class StateError(Exception):
|
class StateError(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -74,8 +74,8 @@ class DeckhandIngester(IngesterPlugin):
|
|||||||
raise errors.IngesterError("Error parsing YAML: %s" % (err))
|
raise errors.IngesterError("Error parsing YAML: %s" % (err))
|
||||||
|
|
||||||
# tracking processing status to provide a complete summary of issues
|
# tracking processing status to provide a complete summary of issues
|
||||||
ps = objects.TaskStatus()
|
ps = objects.Validation()
|
||||||
ps.set_status(hd_fields.ActionResult.Success)
|
ps.set_status(hd_fields.ValidationResult.Success)
|
||||||
for d in parsed_data:
|
for d in parsed_data:
|
||||||
try:
|
try:
|
||||||
(schema_ns, doc_kind, doc_version) = d.get('schema',
|
(schema_ns, doc_kind, doc_version) = d.get('schema',
|
||||||
@ -87,45 +87,56 @@ class DeckhandIngester(IngesterPlugin):
|
|||||||
continue
|
continue
|
||||||
if schema_ns == 'drydock':
|
if schema_ns == 'drydock':
|
||||||
try:
|
try:
|
||||||
|
doc_ref = objects.DocumentReference(
|
||||||
|
doc_type=hd_fields.DocumentType.Deckhand,
|
||||||
|
doc_schema=d.get('schema'),
|
||||||
|
doc_name=d.get('metadata', {}).get('name', 'Unknown'))
|
||||||
doc_errors = self.validate_drydock_document(d)
|
doc_errors = self.validate_drydock_document(d)
|
||||||
if len(doc_errors) > 0:
|
if len(doc_errors) > 0:
|
||||||
doc_ctx = d.get('metadata', {}).get('name', 'Unknown')
|
|
||||||
for e in doc_errors:
|
for e in doc_errors:
|
||||||
ps.add_status_msg(
|
ps.add_detail_msg(
|
||||||
msg="%s:%s validation error: %s" %
|
objects.ValidationMessage(
|
||||||
(doc_kind, doc_version, e),
|
msg="%s:%s schema validation error: %s" %
|
||||||
error=True,
|
(doc_kind, doc_version, e),
|
||||||
ctx_type='document',
|
name="DD001",
|
||||||
ctx=doc_ctx)
|
docs=[doc_ref],
|
||||||
|
error=True,
|
||||||
|
level=hd_fields.MessageLevels.ERROR,
|
||||||
|
diagnostic=
|
||||||
|
"Invalid input file - see Drydock Troubleshooting Guide for DD001"
|
||||||
|
))
|
||||||
ps.set_status(hd_fields.ActionResult.Failure)
|
ps.set_status(hd_fields.ActionResult.Failure)
|
||||||
continue
|
continue
|
||||||
model = self.process_drydock_document(d)
|
model = self.process_drydock_document(d)
|
||||||
ps.add_status_msg(
|
model.doc_ref = doc_ref
|
||||||
msg="Successfully processed Drydock document type %s."
|
|
||||||
% doc_kind,
|
|
||||||
error=False,
|
|
||||||
ctx_type='document',
|
|
||||||
ctx=model.get_id())
|
|
||||||
models.append(model)
|
models.append(model)
|
||||||
except errors.IngesterError as ie:
|
except errors.IngesterError as ie:
|
||||||
msg = "Error processing document: %s" % str(ie)
|
msg = "Error processing document: %s" % str(ie)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
if d.get('metadata', {}).get('name', None) is not None:
|
ps.add_detail_msg(
|
||||||
ctx = d.get('metadata').get('name')
|
objects.ValidationMessage(
|
||||||
else:
|
msg=msg,
|
||||||
ctx = 'Unknown'
|
name="DD000",
|
||||||
ps.add_status_msg(
|
error=True,
|
||||||
msg=msg, error=True, ctx_type='document', ctx=ctx)
|
level=hd_fields.MessageLevels.ERROR,
|
||||||
|
docs=[doc_ref],
|
||||||
|
diagnostic="Exception during document processing "
|
||||||
|
"- see Drydock Troubleshooting Guide "
|
||||||
|
"for DD000"))
|
||||||
ps.set_status(hd_fields.ActionResult.Failure)
|
ps.set_status(hd_fields.ActionResult.Failure)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
msg = "Unexpected error processing document: %s" % str(ex)
|
msg = "Unexpected error processing document: %s" % str(ex)
|
||||||
self.logger.error(msg, exc_info=True)
|
self.logger.error(msg, exc_info=True)
|
||||||
if d.get('metadata', {}).get('name', None) is not None:
|
ps.add_detail_msg(
|
||||||
ctx = d.get('metadata').get('name')
|
objects.ValidationMessage(
|
||||||
else:
|
msg=msg,
|
||||||
ctx = 'Unknown'
|
name="DD000",
|
||||||
ps.add_status_msg(
|
error=True,
|
||||||
msg=msg, error=True, ctx_type='document', ctx=ctx)
|
level=hd_fields.MessageLevels.ERROR,
|
||||||
|
docs=[doc_ref],
|
||||||
|
diagnostic="Unexpected exception during document "
|
||||||
|
"processing - see Drydock Troubleshooting "
|
||||||
|
"Guide for DD000"))
|
||||||
ps.set_status(hd_fields.ActionResult.Failure)
|
ps.set_status(hd_fields.ActionResult.Failure)
|
||||||
return (ps, models)
|
return (ps, models)
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ def register_all():
|
|||||||
importlib.import_module('drydock_provisioner.objects.bootaction')
|
importlib.import_module('drydock_provisioner.objects.bootaction')
|
||||||
importlib.import_module('drydock_provisioner.objects.task')
|
importlib.import_module('drydock_provisioner.objects.task')
|
||||||
importlib.import_module('drydock_provisioner.objects.builddata')
|
importlib.import_module('drydock_provisioner.objects.builddata')
|
||||||
|
importlib.import_module('drydock_provisioner.objects.validation')
|
||||||
|
|
||||||
|
|
||||||
# Utility class for calculating inheritance
|
# Utility class for calculating inheritance
|
||||||
|
@ -34,6 +34,11 @@ class DrydockObject(base.VersionedObject):
|
|||||||
|
|
||||||
OBJ_PROJECT_NAMESPACE = 'drydock_provisioner.objects'
|
OBJ_PROJECT_NAMESPACE = 'drydock_provisioner.objects'
|
||||||
|
|
||||||
|
# Maintain a reference to the source document for the model
|
||||||
|
fields = {
|
||||||
|
'doc_ref': obj_fields.ObjectField('DocumentReference', nullable=True)
|
||||||
|
}
|
||||||
|
|
||||||
# Return None for undefined attributes
|
# Return None for undefined attributes
|
||||||
def obj_load_attr(self, attrname):
|
def obj_load_attr(self, attrname):
|
||||||
if attrname in self.fields.keys():
|
if attrname in self.fields.keys():
|
||||||
|
@ -193,3 +193,13 @@ class NetworkLinkTrunkingModeField(fields.BaseEnumField):
|
|||||||
class ValidationResult(BaseDrydockEnum):
|
class ValidationResult(BaseDrydockEnum):
|
||||||
Success = 'success'
|
Success = 'success'
|
||||||
Failure = 'failure'
|
Failure = 'failure'
|
||||||
|
|
||||||
|
|
||||||
|
class MessageLevels(BaseDrydockEnum):
|
||||||
|
INFO = 'Info'
|
||||||
|
WARN = 'Warning'
|
||||||
|
ERROR = 'Error'
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentType(BaseDrydockEnum):
|
||||||
|
Deckhand = 'deckhand'
|
||||||
|
120
drydock_provisioner/objects/validation.py
Normal file
120
drydock_provisioner/objects/validation.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# Copyright 2017 AT&T Intellectual Property. All other 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.
|
||||||
|
"""Models for representing asynchronous tasks."""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import oslo_versionedobjects.fields as ovo_fields
|
||||||
|
|
||||||
|
from drydock_provisioner import objects
|
||||||
|
|
||||||
|
import drydock_provisioner.objects.base as base
|
||||||
|
import drydock_provisioner.error as errors
|
||||||
|
import drydock_provisioner.objects.fields as hd_fields
|
||||||
|
|
||||||
|
from .task import TaskStatus, TaskStatusMessage
|
||||||
|
|
||||||
|
|
||||||
|
class Validation(TaskStatus):
|
||||||
|
"""Specialized status for design validation status."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def add_detail_msg(self, msg=None):
|
||||||
|
"""Add a detailed validation message.
|
||||||
|
|
||||||
|
:param msg: instance of ValidationMessage
|
||||||
|
"""
|
||||||
|
self.message_list.append(msg)
|
||||||
|
|
||||||
|
if msg.error or msg.level == "Error":
|
||||||
|
self.error_count = self.error_count + 1
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'kind': 'Status',
|
||||||
|
'apiVersion': 'v1.0',
|
||||||
|
'metadata': {},
|
||||||
|
'message': self.message,
|
||||||
|
'reason': self.reason,
|
||||||
|
'status': self.status,
|
||||||
|
'details': {
|
||||||
|
'errorCount': self.error_count,
|
||||||
|
'messageList': [x.to_dict() for x in self.message_list],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationMessage(TaskStatusMessage):
|
||||||
|
"""Message describing details of a validation."""
|
||||||
|
|
||||||
|
def __init__(self, msg, name, error=False, level=None, docs=None, diagnostic=None):
|
||||||
|
self.name = name
|
||||||
|
self.message = msg
|
||||||
|
self.error = error
|
||||||
|
self.level = level
|
||||||
|
self.diagnostic = diagnostic
|
||||||
|
self.ts = datetime.utcnow()
|
||||||
|
self.docs = docs
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Convert to a dictionary in prep for JSON/YAML serialization."""
|
||||||
|
_dict = {
|
||||||
|
'kind': 'ValidationMessage',
|
||||||
|
'name': self.name,
|
||||||
|
'message': self.message,
|
||||||
|
'error': self.error,
|
||||||
|
'level': self.level,
|
||||||
|
'diagnostic': self.diagnostic,
|
||||||
|
'ts': str(self.ts),
|
||||||
|
'documents': [x.to_dict() for x in self.docs]
|
||||||
|
}
|
||||||
|
return _dict
|
||||||
|
|
||||||
|
|
||||||
|
@base.DrydockObjectRegistry.register
|
||||||
|
class DocumentReference(base.DrydockObject):
|
||||||
|
"""Keep a reference to the original document that data was loaded from."""
|
||||||
|
|
||||||
|
VERSION = '1.0'
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'doc_type': ovo_fields.StringField(),
|
||||||
|
'doc_schema': ovo_fields.StringField(nullable=True),
|
||||||
|
'doc_name': ovo_fields.StringField(nullable=True),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
if (self.doc_type == hd_fields.DocumentType.Deckhand):
|
||||||
|
if not all([self.doc_schema, self.doc_name]):
|
||||||
|
raise ValueError("doc_schema and doc_name required for Deckhand sources.")
|
||||||
|
else:
|
||||||
|
raise errors.UnsupportedDocumentType(
|
||||||
|
"Document type %s not supported." % self.doc_type)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""Serialize to a dictionary for further serialization."""
|
||||||
|
d = dict()
|
||||||
|
if self.doc_type == hd_fields.DocumentType.Deckhand:
|
||||||
|
d['schema'] = self.doc_schema
|
||||||
|
d['name'] = self.doc_name
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
# Emulate OVO object registration
|
||||||
|
setattr(objects, Validation.obj_name(), Validation)
|
||||||
|
setattr(objects, ValidationMessage.obj_name(), ValidationMessage)
|
@ -19,8 +19,6 @@ import drydock_provisioner.objects as objects
|
|||||||
|
|
||||||
class TestClass(object):
|
class TestClass(object):
|
||||||
def test_ingest_deckhand(self, input_files, setup, deckhand_ingester):
|
def test_ingest_deckhand(self, input_files, setup, deckhand_ingester):
|
||||||
objects.register_all()
|
|
||||||
|
|
||||||
input_file = input_files.join("deckhand_fullsite.yaml")
|
input_file = input_files.join("deckhand_fullsite.yaml")
|
||||||
|
|
||||||
design_state = DrydockState()
|
design_state = DrydockState()
|
||||||
@ -29,14 +27,26 @@ class TestClass(object):
|
|||||||
design_status, design_data = deckhand_ingester.ingest_data(
|
design_status, design_data = deckhand_ingester.ingest_data(
|
||||||
design_state=design_state, design_ref=design_ref)
|
design_state=design_state, design_ref=design_ref)
|
||||||
|
|
||||||
print("%s" % str(design_status.to_dict()))
|
assert design_status.status == objects.fields.ValidationResult.Success
|
||||||
assert design_status.status == objects.fields.ActionResult.Success
|
|
||||||
assert len(design_data.host_profiles) == 2
|
assert len(design_data.host_profiles) == 2
|
||||||
assert len(design_data.baremetal_nodes) == 2
|
assert len(design_data.baremetal_nodes) == 2
|
||||||
|
|
||||||
def test_ingest_yaml(self, input_files, setup, yaml_ingester):
|
def test_ingest_deckhand_docref_exists(self, input_files, setup, deckhand_ingester):
|
||||||
objects.register_all()
|
"""Test that each processed document has a doc_ref."""
|
||||||
|
input_file = input_files.join('deckhand_fullsite.yaml')
|
||||||
|
|
||||||
|
design_state = DrydockState()
|
||||||
|
design_ref = "file://%s" % str(input_file)
|
||||||
|
design_status, design_data = deckhand_ingester.ingest_data(
|
||||||
|
design_state=design_state, design_ref=design_ref)
|
||||||
|
|
||||||
|
assert design_status.status == objects.fields.ValidationResult.Success
|
||||||
|
for p in design_data.host_profiles:
|
||||||
|
assert p.doc_ref is not None
|
||||||
|
assert p.doc_ref.doc_schema == 'drydock/HostProfile/v1'
|
||||||
|
assert p.doc_ref.doc_name is not None
|
||||||
|
|
||||||
|
def test_ingest_yaml(self, input_files, setup, yaml_ingester):
|
||||||
input_file = input_files.join("fullsite.yaml")
|
input_file = input_files.join("fullsite.yaml")
|
||||||
|
|
||||||
design_state = DrydockState()
|
design_state = DrydockState()
|
||||||
@ -45,7 +55,6 @@ class TestClass(object):
|
|||||||
design_status, design_data = yaml_ingester.ingest_data(
|
design_status, design_data = yaml_ingester.ingest_data(
|
||||||
design_state=design_state, design_ref=design_ref)
|
design_state=design_state, design_ref=design_ref)
|
||||||
|
|
||||||
print("%s" % str(design_status.to_dict()))
|
assert design_status.status == objects.fields.ValidationResult.Success
|
||||||
assert design_status.status == objects.fields.ActionResult.Success
|
|
||||||
assert len(design_data.host_profiles) == 2
|
assert len(design_data.host_profiles) == 2
|
||||||
assert len(design_data.baremetal_nodes) == 2
|
assert len(design_data.baremetal_nodes) == 2
|
||||||
|
Loading…
Reference in New Issue
Block a user