# Copyright 2016 Internap. # # 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. from datetime import datetime from functools import wraps import flask import jsonpickle from oslo_log import log from oslo_serialization import jsonutils from werkzeug import wrappers from almanach.core import exception LOG = log.getLogger(__name__) api = flask.Blueprint("api", __name__) controller = None auth_adapter = None def to_json(api_call): def encode_result(data): return jsonpickle.encode(data, unpicklable=False) def send_response(data, status_code): return flask.Response(encode_result(data), status_code, {"Content-Type": "application/json"}) @wraps(api_call) def decorator(*args, **kwargs): try: result = api_call(*args, **kwargs) return result if isinstance(result, wrappers.BaseResponse) else send_response(result, 200) except (exception.DateFormatException, exception.MultipleEntitiesMatchingQueryException) as e: LOG.warning(e.message) return send_response({"error": e.message}, 400) except KeyError as e: message = "The {param} param is mandatory for the request you have made.".format(param=e) LOG.warning(message) return send_response({"error": message}, 400) except (TypeError, ValueError): message = "Invalid parameter or payload" LOG.warning(message) return send_response({"error": message}, 400) except exception.InvalidAttributeException as e: LOG.warning(e.get_error_message()) return send_response({"error": e.get_error_message()}, 400) except (exception.AlmanachEntityNotFoundException, exception.VolumeTypeNotFoundException) as e: LOG.warning(e.message) return send_response({"error": e.message}, 404) except exception.AlmanachException as e: LOG.exception(e) return send_response({"error": e.message}, 500) except Exception as e: LOG.exception(e) return send_response({"error": e}, 500) return decorator def authenticated(api_call): @wraps(api_call) def decorator(*args, **kwargs): try: auth_adapter.validate(flask.request.headers.get('X-Auth-Token')) return api_call(*args, **kwargs) except exception.AuthenticationFailureException as e: LOG.error("Authentication failure: %s", e) return flask.Response('Unauthorized', 401) return decorator @api.route("/info", methods=["GET"]) @to_json def get_info(): """Display information about the current version and counts of entities in the database. :code 200 OK: Service is available """ return controller.get_application_info() @api.route("/project//instance", methods=["POST"]) @authenticated @to_json def create_instance(project_id): """Create an instance for a tenant. :arg uuid project_id: Tenant Uuid :arg uuid id: The instance uuid :arg datetime created_at: Y-m-d H:M:S.f :arg uuid flavor: The flavor uuid :arg str os_type: The OS type :arg str os_distro: The OS distro :arg str os_version: The OS version :arg str name: The instance name :code 201 Created: Instance successfully created :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If tenant does not exist """ instance = jsonutils.loads(flask.request.data) LOG.info("Creating instance for tenant %s with data %s", project_id, instance) controller.create_instance( tenant_id=project_id, instance_id=instance['id'], create_date=instance['created_at'], flavor=instance['flavor'], os_type=instance['os_type'], distro=instance['os_distro'], version=instance['os_version'], name=instance['name'], metadata={} ) return flask.Response(status=201) @api.route("/instance/", methods=["DELETE"]) @authenticated @to_json def delete_instance(instance_id): """Delete the instance. :arg uuid instance_id: Instance Uuid :arg datetime date: Y-m-d H:M:S.f :code 202 Accepted: Instance successfully deleted :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If instance does not exist """ data = jsonutils.loads(flask.request.data) LOG.info("Deleting instance with id %s with data %s", instance_id, data) controller.delete_instance( instance_id=instance_id, delete_date=data['date'] ) return flask.Response(status=202) @api.route("/instance//resize", methods=["PUT"]) @authenticated @to_json def resize_instance(instance_id): """Re-size an instance when the instance flavor was changed in OpenStack. :arg uuid instance_id: Instance Uuid :arg datetime date: Y-m-d H:M:S.f :arg uuid flavor: The flavor uuid :code 200 OK: Instance successfully re-sized :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If instance does not exist """ instance = jsonutils.loads(flask.request.data) LOG.info("Resizing instance with id %s with data %s", instance_id, instance) controller.resize_instance( instance_id=instance_id, resize_date=instance['date'], flavor=instance['flavor'] ) return flask.Response(status=200) @api.route("/instance//rebuild", methods=["PUT"]) @authenticated @to_json def rebuild_instance(instance_id): """Rebuild an instance when the instance image was changed in OpenStack. :arg uuid instance_id: Instance Uuid :arg str distro: The OS distro :arg str version: The OS version :arg str os_type: The OS type :arg datetime rebuild_date: Y-m-d H:M:S.f :code 200 OK: Instance successfully rebuilt :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If instance does not exist """ instance = jsonutils.loads(flask.request.data) LOG.info("Rebuilding instance with id %s with data %s", instance_id, instance) controller.rebuild_instance( instance_id=instance_id, distro=instance['distro'], version=instance['version'], os_type=instance['os_type'], rebuild_date=instance['rebuild_date'], ) return flask.Response(status=200) @api.route("/project//instances", methods=["GET"]) @authenticated @to_json def list_instances(project_id): """List instances for a tenant. :arg uuid project_id: Tenant Uuid :arg datetime start: Y-m-d H:M:S.f :arg datetime end: Y-m-d H:M:S.f :code 200 OK: instance list exists :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If tenant does not exist. """ start, end = get_period() LOG.info("Listing instances between %s and %s", start, end) return controller.list_instances(project_id, start, end) @api.route("/project//volume", methods=["POST"]) @authenticated @to_json def create_volume(project_id): """Create a volume for a tenant. :arg uuid project_id: Tenant Uuid :arg uuid volume_id: The Volume Uuid :arg datetime start: Y-m-d H:M:S.f :arg uuid volume_type: The Volume Type Uuid :arg str size: The Volume Size :arg str volume_name: The Volume Name :arg uuid attached_to: The Instance Uuid the volume is attached to :code 201 Created: Volume successfully created :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If tenant does not exist. """ volume = jsonutils.loads(flask.request.data) LOG.info("Creating volume for tenant %s with data %s", project_id, volume) controller.create_volume( project_id=project_id, volume_id=volume['volume_id'], start=volume['start'], volume_type=volume['volume_type'], size=volume['size'], volume_name=volume['volume_name'], attached_to=volume['attached_to'] ) return flask.Response(status=201) @api.route("/volume/", methods=["DELETE"]) @authenticated @to_json def delete_volume(volume_id): """Delete the volume. :arg uuid volume_id: Volume Uuid :arg datetime date: Y-m-d H:M:S.f :code 202 Accepted: Volume successfully deleted :code 400 Bad Request: If data invalid or missing :code 404 Not Found: If volume does not exist. """ data = jsonutils.loads(flask.request.data) LOG.info("Deleting volume with id %s with data %s", volume_id, data) controller.delete_volume( volume_id=volume_id, delete_date=data['date'] ) return flask.Response(status=202) @api.route("/volume//resize", methods=["PUT"]) @authenticated @to_json def resize_volume(volume_id): """Re-size a volume when the volume size was changed in OpenStack. :arg uuid volume_id: Volume Uuid :arg str size: The size of the volume :arg datetime date: Y-m-d H:M:S.f :code 200 OK: Volume successfully re-sized :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If volume does not exist. """ volume = jsonutils.loads(flask.request.data) LOG.info("Resizing volume with id %s with data %s", volume_id, volume) controller.resize_volume( volume_id=volume_id, size=volume['size'], update_date=volume['date'] ) return flask.Response(status=200) @api.route("/volume//attach", methods=["PUT"]) @authenticated @to_json def attach_volume(volume_id): """Update the attachments for a volume when the volume attachments have been changed in OpenStack. :arg uuid volume_id: Volume Uuid :arg datetime date: Y-m-d H:M:S.f :arg dict attachments: The volume attachments :code 200 OK: Volume successfully attached :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If volume does not exist. """ volume = jsonutils.loads(flask.request.data) LOG.info("Attaching volume with id %s with data %s", volume_id, volume) controller.attach_volume( volume_id=volume_id, date=volume['date'], attachments=volume['attachments'] ) return flask.Response(status=200) @api.route("/volume//detach", methods=["PUT"]) @authenticated @to_json def detach_volume(volume_id): """Detaches a volume when the volume is detached in OpenStack. :arg uuid volume_id: Volume Uuid :arg datetime date: Y-m-d H:M:S.f :arg dict attachments: The volume attachments :code 200 OK: Volume successfully detached :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If volume does not exist. """ volume = jsonutils.loads(flask.request.data) LOG.info("Detaching volume with id %s with data %s", volume_id, volume) controller.detach_volume( volume_id=volume_id, date=volume['date'], attachments=volume['attachments'] ) return flask.Response(status=200) @api.route("/project//volumes", methods=["GET"]) @authenticated @to_json def list_volumes(project_id): """List volumes for a tenant. :arg uuid project_id: Tenant Uuid :arg datetime start: Y-m-d H:M:S.f :arg datetime end: Y-m-d H:M:S.f :code 200 OK: volume list exists :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If tenant does not exist. """ start, end = get_period() LOG.info("Listing volumes between %s and %s", start, end) return controller.list_volumes(project_id, start, end) @api.route("/project//entities", methods=["GET"]) @authenticated @to_json def list_entity(project_id): """List instances and volumes for a tenant. :arg uuid project_id: Tenant Uuid :arg datetime start: Y-m-d H:M:S.f :arg datetime end: Y-m-d H:M:S.f :code 200 OK: instances and volumes list exists :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If tenant does not exist. """ start, end = get_period() LOG.info("Listing entities between %s and %s", start, end) return controller.list_entities(project_id, start, end) @api.route("/entity/instance/", methods=["PUT"]) @authenticated @to_json def update_instance_entity(instance_id): """Update an instance entity. :arg uuid instance_id: Instance Uuid :arg datetime start: Y-m-d H:M:S.f :arg datetime end: Y-m-d H:M:S.f :code 200 OK: Entity successfully updated :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If instance does not exist. """ data = jsonutils.loads(flask.request.data) LOG.info("Updating instance entity with id %s with data %s", instance_id, data) if 'start' in flask.request.args: start, end = get_period() result = controller.update_inactive_entity(instance_id=instance_id, start=start, end=end, **data) else: result = controller.update_active_instance_entity(instance_id=instance_id, **data) return result @api.route("/entity/", methods=["HEAD"]) @authenticated def entity_exists(entity_id): """Verify that an entity exists. :arg uuid entity_id: Entity Uuid :code 200 OK: if the entity exists :code 404 Not Found: if the entity does not exist """ LOG.info("Does entity with id %s exists", entity_id) response = flask.Response('', 404) if controller.entity_exists(entity_id=entity_id): response = flask.Response('', 200) return response @api.route("/entity/", methods=["GET"]) @authenticated @to_json def get_entity(entity_id): """Get an entity. :arg uuid entity_id: Entity Uuid :code 200 OK: Entity exists :code 404 Not Found: If the entity does not exist """ return controller.get_all_entities_by_id(entity_id) @api.route("/volume_types", methods=["GET"]) @authenticated @to_json def list_volume_types(): """List volume types. :code 200 OK: Volume types exist """ LOG.info("Listing volumes types") return controller.list_volume_types() @api.route("/volume_type/", methods=["GET"]) @authenticated @to_json def get_volume_type(type_id): """Get a volume type. :arg uuid type_id: Volume Type Uuid :code 200 OK: Volume type exists :code 400 Bad Request: If request data has an invalid or missing field :code 404 Not Found: If the volume type does not exist """ LOG.info("Get volumes type for id %s", type_id) return controller.get_volume_type(type_id) @api.route("/volume_type", methods=["POST"]) @authenticated @to_json def create_volume_type(): """Create a volume type. :arg str type_id: The Volume Type id :arg str type_name: The Volume Type name :code 201 Created: Volume successfully created :code 400 Bad Request: If request data has an invalid or missing field """ volume_type = jsonutils.loads(flask.request.data) LOG.info("Creating volume type with data '%s'", volume_type) controller.create_volume_type( volume_type_id=volume_type['type_id'], volume_type_name=volume_type['type_name'] ) return flask.Response(status=201) @api.route("/volume_type/", methods=["DELETE"]) @authenticated @to_json def delete_volume_type(type_id): """Delete the volume type. :arg uuid type_id: Volume Type Uuid :code 202 Accepted: Volume successfully deleted :code 404 Not Found: If volume type does not exist. """ LOG.info("Deleting volume type with id '%s'", type_id) controller.delete_volume_type(type_id) return flask.Response(status=202) def get_period(): start = datetime.strptime(flask.request.args["start"], "%Y-%m-%d %H:%M:%S.%f") if "end" not in flask.request.args: end = datetime.now() else: end = datetime.strptime(flask.request.args["end"], "%Y-%m-%d %H:%M:%S.%f") return start, end