# 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) from shipyard_airflow.errors import ApiError CONF = cfg.CONF LOG = logging.getLogger(__name__) VERSION_VALUES = ['buffer', 'committed', 'last_site_action', 'successful_site_action'] 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 post_collection(self, helper, collection_id, document_data, buffer_mode_param=None, empty_collection=False): """ Ingest the collection after checking preconditions """ buffer_mode = ConfigdocsHelper.get_buffer_mode(buffer_mode_param) 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: return 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, ) 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