shipyard/src/bin/shipyard_airflow/shipyard_airflow/control/configdocs/configdocs_api.py

315 lines
13 KiB
Python

# 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.
"""
Resources representing the configdocs API for shipyard
"""
import logging
import falcon
from oslo_config import cfg
from shipyard_airflow import policy
from shipyard_airflow.control.api_lock import (api_lock, ApiLockType)
from shipyard_airflow.control.base import BaseResource
from shipyard_airflow.control.helpers import configdocs_helper
from shipyard_airflow.control.helpers.configdocs_helper import (
ConfigdocsHelper, add_messages_to_validation_status)
from shipyard_airflow.errors import ApiError
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
VERSION_VALUES = ['buffer',
'committed',
'last_site_action',
'successful_site_action']
DEPLOYMENT_DATA_DOC = {'name': CONF.document_info.deployment_version_name,
'schema': CONF.document_info.deployment_version_schema}
class ConfigDocsStatusResource(BaseResource):
"""
Configdocs Status handles the retrieval of the configuration documents'
statuses
"""
@policy.ApiEnforcer(policy.GET_CONFIGDOCS_STATUS)
def on_get(self, req, resp):
"""Returns a list of the configdocs and their statuses"""
versions = req.params.get('versions') or None
helper = ConfigdocsHelper(req.context)
resp.body = self.to_json(helper.get_configdocs_status(versions))
resp.status = falcon.HTTP_200
class ConfigDocsResource(BaseResource):
"""
Configdocs handles the creation and retrieval of configuration
documents into Shipyard.
"""
@policy.ApiEnforcer(policy.CREATE_CONFIGDOCS)
@api_lock(ApiLockType.CONFIGDOCS_UPDATE)
def on_post(self, req, resp, collection_id):
"""
Ingests a collection of documents
"""
# Determine if this request is clearing the collection's contents.
empty_coll = req.get_param_as_bool('empty-collection') or False
if empty_coll:
document_data = ""
LOG.debug("Collection %s is being emptied", collection_id)
else:
# Note, a newline in a prior header can trigger subsequent
# headers to be "missing" (and hence cause this code to think
# that the content length is missing)
content_length = self.validate_content_length(req.content_length)
document_data = req.stream.read(content_length)
buffer_mode = req.get_param('buffermode')
helper = ConfigdocsHelper(req.context)
validations = self.post_collection(
helper=helper,
collection_id=collection_id,
document_data=document_data,
buffer_mode_param=buffer_mode,
empty_collection=empty_coll)
resp.status = falcon.HTTP_201
if validations and validations['status'] == 'Success':
validations['code'] = resp.status
resp.location = '/api/v1.0/configdocs/{}'.format(collection_id)
resp.body = self.to_json(validations)
def validate_content_length(self, content_length):
"""Validates that the content length header is valid
:param content_length: the value of the content-length header.
:returns: the validate content length value
"""
content_length = content_length or 0
if (content_length == 0):
raise ApiError(
title=('Content-Length is a required header'),
description='Content Length is 0 or not specified',
status=falcon.HTTP_400,
error_list=[{
'message': (
"The Content-Length specified is 0 or not set. To "
"clear a collection's contents, please specify "
"the query parameter 'empty-collection=true'."
"Otherwise, a non-zero length payload and "
"matching Content-Length header is required to "
"post a collection.")
}],
retry=False, )
return content_length
@policy.ApiEnforcer(policy.GET_CONFIGDOCS)
def on_get(self, req, resp, collection_id):
"""
Returns a collection of documents
"""
version = (req.params.get('version') or 'buffer')
cleartext_secrets = req.get_param_as_bool('cleartext-secrets') or False
self._validate_version_parameter(version)
helper = ConfigdocsHelper(req.context)
# Check access to cleartext_secrets
if cleartext_secrets:
policy.check_auth(req.context, policy.GET_CONFIGDOCS_CLRTXT)
# Not reformatting to JSON or YAML since just passing through
resp.body = self.get_collection(
helper=helper, collection_id=collection_id, version=version,
cleartext_secrets=cleartext_secrets)
resp.append_header('Content-Type', 'application/x-yaml')
resp.status = falcon.HTTP_200
def _validate_version_parameter(self, version):
# performs validation of version parameter
if version.lower() not in VERSION_VALUES:
raise ApiError(
title='Invalid version query parameter specified',
description=(
'version must be {}'.format(', '.join(VERSION_VALUES))),
status=falcon.HTTP_400,
retry=False, )
def get_collection(self, helper, collection_id, version='buffer',
cleartext_secrets=False):
"""
Attempts to retrieve the specified collection of documents
either from the buffer, committed version, last site action
or successful site action, as specified
"""
return helper.get_collection_docs(version, collection_id,
cleartext_secrets)
def _validate_deployment_version(self, helper, document_data):
"""
Validate that the received documents include a deployment version doc.
This function should only be called if needed, and will not do any
checking to see if shipyard is configured to skip this check.
Return True if the deployment version doc is present, False otherwise.
"""
LOG.info("Validating deployment data")
LOG.debug("Searching for schema: %s and name: %s",
DEPLOYMENT_DATA_DOC['schema'], DEPLOYMENT_DATA_DOC['name'])
return helper.check_for_document(document_data,
DEPLOYMENT_DATA_DOC['name'],
DEPLOYMENT_DATA_DOC['schema'])
def post_collection(self,
helper,
collection_id,
document_data,
buffer_mode_param=None,
empty_collection=False):
"""Ingest the collection after checking preconditions"""
extra_messages = {'warning': [], 'info': []}
validation_status = None
buffer_mode = ConfigdocsHelper.get_buffer_mode(buffer_mode_param)
# Validate that a deployment version document was provided, unless we
# were told to skip this check
ver_validation_cfg = CONF.validations.deployment_version_create.lower()
if empty_collection or ver_validation_cfg == 'skip':
LOG.debug('Skipping deployment version document validation')
else:
if not self._validate_deployment_version(helper, document_data):
title = 'Deployment version document missing from collection'
error_msg = ('Expected document to be present with schema: {} '
'and name: {}').format(
DEPLOYMENT_DATA_DOC['schema'],
DEPLOYMENT_DATA_DOC['name'])
if ver_validation_cfg in ['info', 'warning']:
extra_messages[ver_validation_cfg].append('{}. {}'.format(
title, error_msg))
else: # Error
raise ApiError(
title=title,
description=('Collection rejected due to missing '
'deployment data document'),
status=falcon.HTTP_400,
error_list=[{'message': error_msg}],
retry=False,
)
if helper.is_buffer_valid_for_bucket(collection_id, buffer_mode):
buffer_revision = helper.add_collection(collection_id,
document_data)
if not (empty_collection or helper.is_collection_in_buffer(
collection_id)):
# raise an error if adding the collection resulted in no new
# revision (meaning it was unchanged) and we're not explicitly
# clearing the collection
raise ApiError(
title=('Collection {} not added to Shipyard '
'buffer'.format(collection_id)),
description='Collection created no new revision',
status=falcon.HTTP_400,
error_list=[{
'message':
('The collection {} added no new revision, and has '
'been rejected as invalid input. This likely '
'means that the collection already exists and '
'was reloaded with the same contents'.format(
collection_id))
}],
retry=False,
)
else:
validation_status = helper.get_deckhand_validation_status(
buffer_revision)
else:
raise ApiError(
title='Invalid collection specified for buffer',
description='Buffermode : {}'.format(buffer_mode.value),
status=falcon.HTTP_409,
error_list=[{
'message': ('Buffer is either not empty or the '
'collection already exists in buffer. '
'Setting a different buffermode may '
'provide the desired functionality')
}],
retry=False,
)
for level, messages in extra_messages.items():
if len(messages):
add_messages_to_validation_status(validation_status,
messages,
level)
return validation_status
class CommitConfigDocsResource(BaseResource):
"""
Commits the buffered configdocs, if the validations pass (or are
overridden (force = true))
Returns the list of validations.
"""
unable_to_commmit = 'Unable to commit configuration documents'
@policy.ApiEnforcer(policy.COMMIT_CONFIGDOCS)
@api_lock(ApiLockType.CONFIGDOCS_UPDATE)
def on_post(self, req, resp):
"""
Get validations from all Airship components
Functionality does not exist yet
"""
# force and dryrun query parameter is False unless explicitly true
force = req.get_param_as_bool(name='force') or False
dryrun = req.get_param_as_bool(name='dryrun') or False
helper = ConfigdocsHelper(req.context)
validations = self.commit_configdocs(helper, force, dryrun)
resp.body = self.to_json(validations)
resp.status = validations.get('code', falcon.HTTP_200)
def commit_configdocs(self, helper, force, dryrun):
"""
Attempts to commit the configdocs
"""
if helper.is_buffer_empty():
raise ApiError(
title=CommitConfigDocsResource.unable_to_commmit,
description='There are no documents in the buffer to commit',
status=falcon.HTTP_409,
retry=True)
validations = helper.get_validations_for_revision(
helper.get_revision_id(configdocs_helper.BUFFER)
)
if dryrun:
validations['code'] = falcon.HTTP_200
if 'message' in validations:
validations['message'] = (
validations['message'] + ' DRYRUN')
else:
validations['message'] = 'DRYRUN'
else:
if force or validations.get('status') == 'Success':
helper.tag_buffer(configdocs_helper.COMMITTED)
if force and validations.get('status') == 'Failure':
# override the status in the response
validations['code'] = falcon.HTTP_200
if 'message' in validations:
validations['message'] = (
validations['message'] + ' FORCED SUCCESS')
else:
validations['message'] = 'FORCED SUCCESS'
return validations