Add Validation API to Drydock

This ps adds the validation endpoint to the Drydock API and includes
the unit tests for post_validation

Change-Id: I09f0602603e46a593dea948d226070d8fb67ff1d
This commit is contained in:
Krysta Knight 2017-11-06 15:29:18 -06:00 committed by Krysta
parent f368aa9fc0
commit 3d4efe9907
11 changed files with 383 additions and 22 deletions

View File

@ -25,6 +25,7 @@ from .health import HealthResource
from .bootaction import BootactionUnitsResource
from .bootaction import BootactionFilesResource
from .bootaction import BootactionResource
from .validation import ValidationResource
from .base import DrydockRequest, BaseResource
from .middleware import AuthMiddleware, ContextMiddleware, LoggingMiddleware
@ -78,6 +79,10 @@ def start_api(state_manager=None, ingester=None, orchestrator=None):
state_manager=state_manager, orchestrator=orchestrator)),
('/bootactions/{action_id}', BootactionResource(
state_manager=state_manager, orchestrator=orchestrator)),
# API to validate schemas
('/validatedesign', ValidationResource(
state_manager=state_manager, orchestrator=orchestrator)),
]
for path, res in v1_0_routes:

View File

@ -0,0 +1,94 @@
# 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.
import falcon
import json
from drydock_provisioner import policy
from drydock_provisioner.control.base import StatefulResource
import drydock_provisioner.error as errors
class ValidationResource(StatefulResource):
"""
Drydock validation endpoint
"""
def __init__(self, orchestrator=None, **kwargs):
"""Object initializer.
:param orchestrator: instance of orchestrator.Orchestrator
"""
super().__init__(**kwargs)
self.orchestrator = orchestrator
@policy.ApiEnforcer('physical_provisioner:validate_site_design')
def on_post(self, req, resp):
# create resp message
resp_message = {
'kind': 'Status',
'apiVersion': 'v1',
'metaData': {},
'status': '',
'message': '',
'reason': 'Validation',
'details': {
'errorCount': 0,
'messageList': []
},
'code': '',
}
try:
json_data = self.req_json(req)
if json_data is None:
resp.status = falcon.HTTP_400
err_message = 'Request body must not be empty for validation.'
self.error(req.context, err_message)
return self.return_error(resp, falcon.HTTP_400, err_message)
design_ref = json_data.get('href', None)
if not design_ref:
resp.status = falcon.HTTP_400
err_message = 'The "href" key must be provided in the request body.'
self.error(req.context, err_message)
return self.return_error(resp, falcon.HTTP_400, err_message)
message, design_data = self.orchestrator.get_effective_site(
design_ref)
resp_message['details']['errorCount'] = message.error_count
resp_message['details']['messageList'] = [m.to_dict() for m in message.message_list]
if message.error_count == 0:
resp_message['status'] = 'Valid'
resp_message['message'] = 'Drydock Validations succeeded'
resp_message['code'] = 200
resp.status = falcon.HTTP_200
resp.body = json.dumps(resp_message)
else:
resp_message['status'] = 'Invalid'
resp_message['message'] = 'Drydock Validations failed'
resp_message['code'] = 400
resp.status = falcon.HTTP_400
resp.body = json.dumps(resp_message)
except errors.InvalidFormat as e:
err_message = str(e)
resp.status = falcon.HTTP_400
self.error(req.context, err_message)
self.return_error(resp, falcon.HTTP_400, err_message)

View File

@ -187,3 +187,8 @@ class NetworkLinkTrunkingMode(BaseDrydockEnum):
class NetworkLinkTrunkingModeField(fields.BaseEnumField):
AUTO_TYPE = NetworkLinkTrunkingMode()
class ValidationResult(BaseDrydockEnum):
Success = 'success'
Failure = 'failure'

View File

@ -20,7 +20,6 @@ import uuid
import ulid2
import concurrent.futures
import os
import yaml
import drydock_provisioner.config as config
import drydock_provisioner.objects as objects
@ -35,6 +34,7 @@ from .actions.orchestrator import VerifyNodes
from .actions.orchestrator import PrepareNodes
from .actions.orchestrator import DeployNodes
from .actions.orchestrator import DestroyNodes
from .validations.validator import Validator
class Orchestrator(object):
@ -265,24 +265,6 @@ class Orchestrator(object):
return status, site_design
def _validate_design(self, site_design, result_status=None):
"""Validate the design in site_design passes all validation rules.
Apply all validation rules to the design in site_design. If result_status is
defined, update it with validation messages. Otherwise a new status instance
will be created and returned.
:param site_design: instance of objects.SiteDesign
:param result_status: instance of objects.TaskStatus
"""
# TODO(sh8121att) actually implement the validation rules defined in the readme
if result_status is not None:
result_status = objects.TaskStatus()
result_status.set_status(hd_fields.ActionResult.Success)
return result_status
def get_effective_site(self, design_ref):
"""Ingest design data and compile the effective model of the design.
@ -293,13 +275,13 @@ class Orchestrator(object):
"""
status = None
site_design = None
val = Validator()
try:
status, site_design = self.get_described_site(design_ref)
if status.status == hd_fields.ActionResult.Success:
self.compute_model_inheritance(site_design)
self.compute_bootaction_targets(site_design)
status = self._validate_design(site_design, result_status=status)
self.logger.debug("Status of effective design:\n%s" % yaml.dump(status.to_dict()))
status = val.validate_design(site_design, result_status=status)
except Exception as ex:
if status is not None:
status.add_status_msg(

View File

@ -0,0 +1,71 @@
# 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.
"""Business Logic Validation"""
import drydock_provisioner.objects.fields as hd_fields
from drydock_provisioner.objects.task import TaskStatus
from drydock_provisioner.objects.task import TaskStatusMessage
class Validator():
def validate_design(self, site_design, result_status=None):
"""Validate the design in site_design passes all validation rules.
Apply all validation rules to the design in site_design. If result_status is
defined, update it with validation messages. Otherwise a new status instance
will be created and returned.
:param site_design: instance of objects.SiteDesign
:param result_status: instance of objects.TaskStatus
"""
if result_status is None:
result_status = TaskStatus()
validation_error = False
for rule in rule_set:
output = rule(site_design)
result_status.message_list.extend(output)
error_msg = [m for m in output if m.error]
result_status.error_count = result_status.error_count + len(
error_msg)
if len(error_msg) > 0:
validation_error = True
if validation_error:
result_status.set_status(hd_fields.ValidationResult.Failure)
else:
result_status.set_status(hd_fields.ValidationResult.Success)
return result_status
# TODO: (sh8121att) actually implement validation logic
@classmethod
def no_duplicate_IPs_check(cls, site_design):
message_list = []
message_list.append(TaskStatusMessage(msg='Unique Ip', error=False, ctx_type='NA', ctx='NA'))
return message_list
# TODO: (sh8121att) actually implement validation logic
@classmethod
def no_outside_IPs_check(cls, site_design):
message_list = []
message_list.append(TaskStatusMessage(msg='No outside Ip', error=False, ctx_type='NA', ctx='NA'))
return message_list
rule_set = [Validator.no_duplicate_IPs_check, Validator.no_outside_IPs_check]

View File

@ -121,6 +121,17 @@ class DrydockPolicy(object):
}])
]
# Validate Design Policy
validation_rules = [
policy.DocumentedRuleDefault(
'physical_provisioner:validate_site_design', 'role:admin',
'Validate site design',
[{
'path': '/api/v1.0/validatedesign',
'method': 'POST'
}]),
]
def __init__(self):
self.enforcer = policy.Enforcer(cfg.CONF)
@ -128,6 +139,7 @@ class DrydockPolicy(object):
self.enforcer.register_defaults(DrydockPolicy.base_rules)
self.enforcer.register_defaults(DrydockPolicy.task_rules)
self.enforcer.register_defaults(DrydockPolicy.data_rules)
self.enforcer.register_defaults(DrydockPolicy.validation_rules)
self.enforcer.load_rules()
def authorize(self, action, ctx):
@ -183,5 +195,6 @@ def list_policies():
default_policy.extend(DrydockPolicy.base_rules)
default_policy.extend(DrydockPolicy.task_rules)
default_policy.extend(DrydockPolicy.data_rules)
default_policy.extend(DrydockPolicy.validation_rules)
return default_policy

View File

@ -0,0 +1,54 @@
# Actions requiring admin authority
#"admin_required": "role:admin or is_admin:1"
# Get task status
# GET /api/v1.0/tasks
# GET /api/v1.0/tasks/{task_id}
#"physical_provisioner:read_task": "role:admin"
# Create a task
# POST /api/v1.0/tasks
#"physical_provisioner:create_task": "role:admin"
# Create validate_design task
# POST /api/v1.0/tasks
#"physical_provisioner:validate_design": "role:admin"
# Create verify_site task
# POST /api/v1.0/tasks
#"physical_provisioner:verify_site": "role:admin"
# Create prepare_site task
# POST /api/v1.0/tasks
#"physical_provisioner:prepare_site": "role:admin"
# Create verify_nodes task
# POST /api/v1.0/tasks
#"physical_provisioner:verify_nodes": "role:admin"
# Create prepare_nodes task
# POST /api/v1.0/tasks
#"physical_provisioner:prepare_nodes": "role:admin"
# Create deploy_nodes task
# POST /api/v1.0/tasks
#"physical_provisioner:deploy_nodes": "role:admin"
# Create destroy_nodes task
# POST /api/v1.0/tasks
#"physical_provisioner:destroy_nodes": "role:admin"
# Read loaded design data
# GET /api/v1.0/designs
# GET /api/v1.0/designs/{design_id}
#"physical_provisioner:read_data": "role:admin"
# Load design data
# POST /api/v1.0/designs
# POST /api/v1.0/designs/{design_id}/parts
#"physical_provisioner:ingest_data": "role:admin"
# Validate site design
# POST /api/v1.0/validatedesign
#"physical_provisioner:validate_site_design": "role:admin"

View File

@ -0,0 +1,94 @@
# 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.
"""Test Validation API"""
from falcon import testing
import pytest
import json
from drydock_provisioner import policy
from drydock_provisioner.control.api import start_api
import falcon
class TestValidationApi(object):
def test_post_validation_resp(self, input_files, falcontest):
input_file = input_files.join("deckhand_fullsite.yaml")
design_ref = "file://%s" % str(input_file)
url = '/api/v1.0/validatedesign'
hdr = {
'Content-Type': 'application/json',
'X-IDENTITY-STATUS': 'Confirmed',
'X-USER-NAME': 'Test',
'X-ROLES': 'admin'
}
body = {
'rel': "design",
'href': design_ref,
'type': "application/x-yaml",
}
result = falcontest.simulate_post(
url, headers=hdr, body=json.dumps(body))
assert result.status == falcon.HTTP_200
def test_href_error(self, input_files, falcontest):
url = '/api/v1.0/validatedesign'
hdr = {
'Content-Type': 'application/json',
'X-IDENTITY-STATUS': 'Confirmed',
'X-USER-NAME': 'Test',
'X-ROLES': 'admin'
}
body = {
'rel': "design",
'href': '',
'type': "application/x-yaml",
}
result = falcontest.simulate_post(
url, headers=hdr, body=json.dumps(body))
assert result.status == falcon.HTTP_400
def test_json_data_error(self, input_files, falcontest):
url = '/api/v1.0/validatedesign'
hdr = {
'Content-Type': 'application/json',
'X-IDENTITY-STATUS': 'Confirmed',
'X-USER-NAME': 'Test',
'X-ROLES': 'admin'
}
body = {}
result = falcontest.simulate_post(
url, headers=hdr, body=json.dumps(body))
assert result.status == falcon.HTTP_400
@pytest.fixture()
def falcontest(self, drydock_state, deckhand_ingester, deckhand_orchestrator):
"""Create a test harness for the the Falcon API framework."""
policy.policy_engine = policy.DrydockPolicy()
policy.policy_engine.register_policy()
return testing.TestClient(
start_api(
state_manager=drydock_state,
ingester=deckhand_ingester,
orchestrator=deckhand_orchestrator))

View File

@ -29,7 +29,8 @@ class TestDefaultRules():
expected_calls = [
mocker.call.register_defaults(DrydockPolicy.base_rules),
mocker.call.register_defaults(DrydockPolicy.task_rules),
mocker.call.register_defaults(DrydockPolicy.data_rules)
mocker.call.register_defaults(DrydockPolicy.data_rules),
mocker.call.register_defaults(DrydockPolicy.validation_rules)
]
# Validate the oslo_policy Enforcer was loaded with expected default policy rules

View File

@ -0,0 +1,42 @@
# 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.
import drydock_provisioner.objects.fields as hd_fields
from drydock_provisioner.statemgmt.state import DrydockState
from drydock_provisioner.ingester.ingester import Ingester
from drydock_provisioner.orchestrator.validations.validator import Validator
class TestDesignValidator(object):
def test_validate_design(self, input_files, setup):
"""Test the basic validation engine."""
input_file = input_files.join("fullsite.yaml")
design_state = DrydockState()
design_ref = "file://%s" % str(input_file)
ingester = Ingester()
ingester.enable_plugin(
'drydock_provisioner.ingester.plugins.yaml.YamlIngester')
design_status, design_data = ingester.ingest_data(
design_state=design_state, design_ref=design_ref)
val = Validator()
response = val.validate_design(design_data)
for msg in response.message_list:
assert msg.error is False
assert response.error_count == 0
assert response.status == hd_fields.ValidationResult.Success