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:
parent
f368aa9fc0
commit
3d4efe9907
|
@ -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:
|
||||
|
|
|
@ -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)
|
|
@ -187,3 +187,8 @@ class NetworkLinkTrunkingMode(BaseDrydockEnum):
|
|||
|
||||
class NetworkLinkTrunkingModeField(fields.BaseEnumField):
|
||||
AUTO_TYPE = NetworkLinkTrunkingMode()
|
||||
|
||||
|
||||
class ValidationResult(BaseDrydockEnum):
|
||||
Success = 'success'
|
||||
Failure = 'failure'
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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]
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
@ -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))
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue