From a81c0554b5bff8af1da73c48c49f961747c09b94 Mon Sep 17 00:00:00 2001 From: Marx314 Date: Wed, 18 May 2016 12:05:09 -0400 Subject: [PATCH] Modify api to support edition of closed entity --- Dockerfile | 12 +- README.md | 4 +- almanach/adapters/api_route_v1.py | 28 +++- almanach/adapters/database_adapter.py | 28 +++- .../almanach_entity_not_found_exception.py | 2 - almanach/common/exceptions/__init__.py | 0 .../almanach_entity_not_found_exception.py | 18 +++ .../{ => exceptions}/almanach_exception.py | 0 .../{ => exceptions}/date_format_exception.py | 3 +- .../multiple_entities_matching_query.py | 18 +++ .../common/exceptions/validation_exception.py | 26 ++++ .../volume_type_not_found_exception.py | 3 +- almanach/common/validation_exception.py | 10 -- almanach/config.py | 2 +- almanach/core/controller.py | 43 ++++-- almanach/validators/instance_validator.py | 3 +- docker-compose.yml | 4 +- docker-entrypoint.sh | 13 ++ tests/adapters/test_bus_adapter.py | 6 +- tests/adapters/test_database_adapter.py | 48 +++++-- tests/builder.py | 4 +- tests/core/test_controller.py | 71 +++++++++- tests/test_api.py | 132 +++++++++++------- tests/validators/test_instance_validator.py | 5 +- 24 files changed, 364 insertions(+), 119 deletions(-) delete mode 100644 almanach/common/almanach_entity_not_found_exception.py create mode 100644 almanach/common/exceptions/__init__.py create mode 100644 almanach/common/exceptions/almanach_entity_not_found_exception.py rename almanach/common/{ => exceptions}/almanach_exception.py (100%) rename almanach/common/{ => exceptions}/date_format_exception.py (87%) create mode 100644 almanach/common/exceptions/multiple_entities_matching_query.py create mode 100644 almanach/common/exceptions/validation_exception.py rename almanach/common/{ => exceptions}/volume_type_not_found_exception.py (86%) delete mode 100644 almanach/common/validation_exception.py create mode 100755 docker-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 93d17ef..2c8e099 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,11 +7,15 @@ ADD README.md /opt/almanach/src/ ADD requirements.txt /opt/almanach/src/ ADD LICENSE /opt/almanach/src/ ADD almanach/resources/config/almanach.cfg /etc/almanach.cfg +COPY docker-entrypoint.sh /opt/almanach/entrypoint.sh -WORKDIR /opt/almanach - -RUN cd src && \ +RUN cd /opt/almanach/src && \ pip install -r requirements.txt && \ - PBR_VERSION=2.0.dev0 python setup.py install + PBR_VERSION=2.0.dev0 python setup.py install && \ + chmod +x /opt/almanach/entrypoint.sh + +VOLUME /opt/almanach USER nobody + +ENTRYPOINT ["/opt/almanach/entrypoint.sh"] diff --git a/README.md b/README.md index 70e4cf3..eb07bb1 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,8 @@ Running Almanach with Docker The actual Docker configuration assume that you already have RabbitMQ (mandatory for Openstack) and MongoDB configured for Almanach. ```bash -export RABBITMQ_URL="amqp://openstack:openstack@my-hostname:5672/" -export MONGODB_URL="mongodb://almanach:almanach@my-hostname:27017/almanach" +export RABBITMQ_URL="amqp://guest:guest@messaging:5672/" +export MONGODB_URL="mongodb://almanach:almanach@database:27017/almanach" docker-compose build docker-compose up diff --git a/almanach/adapters/api_route_v1.py b/almanach/adapters/api_route_v1.py index 45e87d6..790b96f 100644 --- a/almanach/adapters/api_route_v1.py +++ b/almanach/adapters/api_route_v1.py @@ -14,16 +14,20 @@ import logging import json -import jsonpickle - from datetime import datetime from functools import wraps -from almanach.common.validation_exception import InvalidAttributeException + +import jsonpickle + from flask import Blueprint, Response, request + from werkzeug.wrappers import BaseResponse +from almanach.common.exceptions.almanach_entity_not_found_exception import AlmanachEntityNotFoundException +from almanach.common.exceptions.multiple_entities_matching_query import MultipleEntitiesMatchingQuery +from almanach.common.exceptions.validation_exception import InvalidAttributeException from almanach import config -from almanach.common.date_format_exception import DateFormatException +from almanach.common.exceptions.date_format_exception import DateFormatException api = Blueprint("api", __name__) controller = None @@ -53,9 +57,18 @@ def to_json(api_call): except InvalidAttributeException as e: logging.warning(e.get_error_message()) return encode({"error": e.get_error_message()}), 400, {"Content-Type": "application/json"} + except MultipleEntitiesMatchingQuery as e: + logging.warning(e.message) + return encode({"error": "Multiple entities found while updating closed"}), 400, { + "Content-Type": "application/json"} + except AlmanachEntityNotFoundException as e: + logging.warning(e.message) + return encode({"error": "Entity not found for updating closed"}), 400, {"Content-Type": "application/json"} + except Exception as e: logging.exception(e) return Response(encode({"error": e.message}), 500, {"Content-Type": "application/json"}) + return decorator @@ -256,7 +269,12 @@ def list_entity(project_id): def update_instance_entity(instance_id): data = json.loads(request.data) logging.info("Updating instance entity with id %s with data %s", instance_id, data) - return controller.update_active_instance_entity(instance_id=instance_id, **data) + if 'start' in 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("/volume_types", methods=["GET"]) diff --git a/almanach/adapters/database_adapter.py b/almanach/adapters/database_adapter.py index 8058d1d..74df458 100644 --- a/almanach/adapters/database_adapter.py +++ b/almanach/adapters/database_adapter.py @@ -13,15 +13,16 @@ # limitations under the License. import logging -import pymongo +import pymongo from pymongo.errors import ConfigurationError -from almanach import config -from almanach.common.almanach_exception import AlmanachException -from almanach.common.volume_type_not_found_exception import VolumeTypeNotFoundException -from almanach.core.model import build_entity_from_dict, VolumeType from pymongomodem.utils import decode_output, encode_input +from almanach import config +from almanach.common.exceptions.almanach_exception import AlmanachException +from almanach.common.exceptions.volume_type_not_found_exception import VolumeTypeNotFoundException +from almanach.core.model import build_entity_from_dict, VolumeType + def database(function): def _connection(self, *args, **kwargs): @@ -55,7 +56,6 @@ def ensureindex(db): class DatabaseAdapter(object): - def __init__(self): self.db = None @@ -90,6 +90,22 @@ class DatabaseAdapter(object): entities = self._get_entities_from_db(args) return [build_entity_from_dict(entity) for entity in entities] + @database + def list_entities_by_id(self, entity_id, start, end): + entities = self.db.entity.find({"entity_id": entity_id, + "start": {"$gte": start}, + "$and": [ + {"end": {"$ne": None}}, + {"end": {"$lte": end}} + ] + }, {"_id": 0}) + return [build_entity_from_dict(entity) for entity in entities] + + @database + def update_closed_entity(self, entity, data): + self.db.entity.update({"entity_id": entity.entity_id, "start": entity.start, "end": entity.end}, + {"$set": data}) + @database def insert_entity(self, entity): self._insert_entity(entity.as_dict()) diff --git a/almanach/common/almanach_entity_not_found_exception.py b/almanach/common/almanach_entity_not_found_exception.py deleted file mode 100644 index 4d26f9e..0000000 --- a/almanach/common/almanach_entity_not_found_exception.py +++ /dev/null @@ -1,2 +0,0 @@ -class AlmanachEntityNotFoundException(Exception): - pass diff --git a/almanach/common/exceptions/__init__.py b/almanach/common/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/almanach/common/exceptions/almanach_entity_not_found_exception.py b/almanach/common/exceptions/almanach_entity_not_found_exception.py new file mode 100644 index 0000000..ff39e52 --- /dev/null +++ b/almanach/common/exceptions/almanach_entity_not_found_exception.py @@ -0,0 +1,18 @@ +# 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 almanach.common.exceptions.almanach_exception import AlmanachException + + +class AlmanachEntityNotFoundException(AlmanachException): + pass diff --git a/almanach/common/almanach_exception.py b/almanach/common/exceptions/almanach_exception.py similarity index 100% rename from almanach/common/almanach_exception.py rename to almanach/common/exceptions/almanach_exception.py diff --git a/almanach/common/date_format_exception.py b/almanach/common/exceptions/date_format_exception.py similarity index 87% rename from almanach/common/date_format_exception.py rename to almanach/common/exceptions/date_format_exception.py index 9fb4149..ca6b207 100644 --- a/almanach/common/date_format_exception.py +++ b/almanach/common/exceptions/date_format_exception.py @@ -11,9 +11,10 @@ # 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 almanach.common.exceptions.almanach_exception import AlmanachException -class DateFormatException(Exception): +class DateFormatException(AlmanachException): def __init__(self, message=None): if not message: diff --git a/almanach/common/exceptions/multiple_entities_matching_query.py b/almanach/common/exceptions/multiple_entities_matching_query.py new file mode 100644 index 0000000..9085adb --- /dev/null +++ b/almanach/common/exceptions/multiple_entities_matching_query.py @@ -0,0 +1,18 @@ +# 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 almanach.common.exceptions.almanach_exception import AlmanachException + + +class MultipleEntitiesMatchingQuery(AlmanachException): + pass diff --git a/almanach/common/exceptions/validation_exception.py b/almanach/common/exceptions/validation_exception.py new file mode 100644 index 0000000..2004a14 --- /dev/null +++ b/almanach/common/exceptions/validation_exception.py @@ -0,0 +1,26 @@ +# 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 almanach.common.exceptions.almanach_exception import AlmanachException + + +class InvalidAttributeException(AlmanachException): + def __init__(self, errors): + self.errors = errors + + def get_error_message(self): + messages = {} + for error in self.errors: + messages[error.path[0]] = error.msg + + return messages diff --git a/almanach/common/volume_type_not_found_exception.py b/almanach/common/exceptions/volume_type_not_found_exception.py similarity index 86% rename from almanach/common/volume_type_not_found_exception.py rename to almanach/common/exceptions/volume_type_not_found_exception.py index a1f6221..f7c7327 100644 --- a/almanach/common/volume_type_not_found_exception.py +++ b/almanach/common/exceptions/volume_type_not_found_exception.py @@ -11,9 +11,10 @@ # 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 almanach.common.exceptions.almanach_exception import AlmanachException -class VolumeTypeNotFoundException(Exception): +class VolumeTypeNotFoundException(AlmanachException): def __init__(self, volume_type_id, message=None): if not message: diff --git a/almanach/common/validation_exception.py b/almanach/common/validation_exception.py deleted file mode 100644 index 776ed7f..0000000 --- a/almanach/common/validation_exception.py +++ /dev/null @@ -1,10 +0,0 @@ -class InvalidAttributeException(Exception): - def __init__(self, errors): - self.errors = errors - - def get_error_message(self): - messages = {} - for error in self.errors: - messages[error.path[0]] = error.msg - - return messages diff --git a/almanach/config.py b/almanach/config.py index c7f9d02..5fae6f1 100644 --- a/almanach/config.py +++ b/almanach/config.py @@ -16,7 +16,7 @@ import ConfigParser import os import os.path as os_path -from almanach.common.almanach_exception import AlmanachException +from almanach.common.exceptions.almanach_exception import AlmanachException configuration = ConfigParser.RawConfigParser() diff --git a/almanach/core/controller.py b/almanach/core/controller.py index bdba1a4..18b292e 100644 --- a/almanach/core/controller.py +++ b/almanach/core/controller.py @@ -13,14 +13,16 @@ # limitations under the License. import logging -import pytz - from datetime import timedelta + +import pytz from dateutil import parser as date_parser + from pkg_resources import get_distribution -from almanach.common.almanach_entity_not_found_exception import AlmanachEntityNotFoundException -from almanach.common.date_format_exception import DateFormatException +from almanach.common.exceptions.almanach_entity_not_found_exception import AlmanachEntityNotFoundException +from almanach.common.exceptions.date_format_exception import DateFormatException +from almanach.common.exceptions.multiple_entities_matching_query import MultipleEntitiesMatchingQuery from almanach.core.model import Instance, Volume, VolumeType from almanach.validators.instance_validator import InstanceValidator from almanach import config @@ -107,6 +109,19 @@ class Controller(object): instance.last_event = rebuild_date self.database_adapter.insert_entity(instance) + def update_inactive_entity(self, instance_id, start, end, **kwargs): + inactive_entities = self.database_adapter.list_entities_by_id(instance_id, start, end) + if len(inactive_entities) > 1: + raise MultipleEntitiesMatchingQuery() + if len(inactive_entities) < 1: + raise AlmanachEntityNotFoundException("InstanceId: {0} Not Found with start".format(instance_id)) + entity = inactive_entities[0] + entity_update = self._transform_attribute_to_match_entity_attribute(**kwargs) + self.database_adapter.update_closed_entity(entity=entity, data=entity_update) + start = entity_update.get('start') or start + end = entity_update.get('end') or end + return self.database_adapter.list_entities_by_id(instance_id, start, end)[0] + def update_active_instance_entity(self, instance_id, **kwargs): try: InstanceValidator().validate_update(kwargs) @@ -155,18 +170,20 @@ class Controller(object): raise e def _update_instance_object(self, instance, **kwargs): + for key, value in self._transform_attribute_to_match_entity_attribute(**kwargs).items(): + setattr(instance, key, value) + logging.info("Updating entity for instance '{0}' with {1}={2}".format(instance.entity_id, key, value)) + + def _transform_attribute_to_match_entity_attribute(self, **kwargs): + entity = {} for attribute, key in dict(start="start_date", end="end_date").items(): - value = kwargs.get(key) - if value: - setattr(instance, attribute, self._validate_and_parse_date(value)) - logging.info("Updating entity for instance '{0}' with {1}={2}".format(instance.entity_id, key, value)) + if kwargs.get(key): + entity[attribute] = self._validate_and_parse_date(kwargs.get(key)) for attribute in ["name", "flavor", "os", "metadata"]: - value = kwargs.get(attribute) - if value: - setattr(instance, attribute, value) - logging.info( - "Updating entity for instance '{0}' with {1}={2}".format(instance.entity_id, attribute, value)) + if kwargs.get(attribute): + entity[attribute] = kwargs.get(attribute) + return entity def _volume_attach_instance(self, volume_id, date, attachments): volume = self.database_adapter.get_active_entity(volume_id) diff --git a/almanach/validators/instance_validator.py b/almanach/validators/instance_validator.py index cdbd53c..c39c82f 100644 --- a/almanach/validators/instance_validator.py +++ b/almanach/validators/instance_validator.py @@ -1,6 +1,7 @@ -from almanach.common.validation_exception import InvalidAttributeException from voluptuous import Schema, MultipleInvalid, Datetime, Required +from almanach.common.exceptions.validation_exception import InvalidAttributeException + class InstanceValidator(object): def __init__(self): diff --git a/docker-compose.yml b/docker-compose.yml index ebe4f31..9b21b00 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: build: context: . dockerfile: Dockerfile - command: almanach api /etc/almanach.cfg --host 0.0.0.0 + command: api environment: MONGODB_URL: ${MONGODB_URL} ports: @@ -13,7 +13,7 @@ services: build: context: . dockerfile: Dockerfile - command: almanach collector /etc/almanach.cfg + command: collector environment: MONGODB_URL: ${MONGODB_URL} RABBITMQ_URL: ${RABBITMQ_URL} diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..31f5448 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e + +echo "Entering the entrypoint" +if [ "$1" = 'api' ]; then + echo "Starting the api" + almanach api /etc/almanach.cfg --host 0.0.0.0 +elif [ "$1" = 'collector' ]; then + echo "Starting the collector" + almanach collector /etc/almanach.cfg +fi + +exec "$@" \ No newline at end of file diff --git a/tests/adapters/test_bus_adapter.py b/tests/adapters/test_bus_adapter.py index d4feb70..a9aeb45 100644 --- a/tests/adapters/test_bus_adapter.py +++ b/tests/adapters/test_bus_adapter.py @@ -13,12 +13,12 @@ # limitations under the License. import unittest -import pytz - from datetime import datetime + +import pytz from flexmock import flexmock, flexmock_teardown -from almanach.common.almanach_entity_not_found_exception import AlmanachEntityNotFoundException +from almanach.common.exceptions.almanach_entity_not_found_exception import AlmanachEntityNotFoundException from tests import messages from almanach.adapters.bus_adapter import BusAdapter diff --git a/tests/adapters/test_database_adapter.py b/tests/adapters/test_database_adapter.py index 7c5d6c6..3f4d40d 100644 --- a/tests/adapters/test_database_adapter.py +++ b/tests/adapters/test_database_adapter.py @@ -12,18 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -import pkg_resources import unittest -import mongomock - from datetime import datetime + +import pkg_resources +import mongomock from flexmock import flexmock, flexmock_teardown from hamcrest import assert_that, contains_inanyorder + from pymongo import MongoClient +import pytz from almanach.adapters.database_adapter import DatabaseAdapter -from almanach.common.volume_type_not_found_exception import VolumeTypeNotFoundException -from almanach.common.almanach_exception import AlmanachException +from almanach.common.exceptions.volume_type_not_found_exception import VolumeTypeNotFoundException +from almanach.common.exceptions.almanach_exception import AlmanachException from almanach import config from almanach.core.model import todict from tests.builder import a, instance, volume, volume_type @@ -128,7 +130,7 @@ class DatabaseAdapterTest(unittest.TestCase): [self.db.entity.insert(todict(fake_entity)) for fake_entity in fake_instances + fake_volumes] entities = self.adapter.list_entities("project_id", datetime( - 2014, 1, 1, 0, 0, 0), datetime(2014, 1, 1, 12, 0, 0), "instance") + 2014, 1, 1, 0, 0, 0, tzinfo=pytz.utc), datetime(2014, 1, 1, 12, 0, 0, tzinfo=pytz.utc), "instance") assert_that(entities, contains_inanyorder(*fake_instances)) def test_list_instances_with_decode_output(self): @@ -167,7 +169,7 @@ class DatabaseAdapterTest(unittest.TestCase): [self.db.entity.insert(todict(fake_entity)) for fake_entity in fake_instances] entities = self.adapter.list_entities("project_id", datetime( - 2014, 1, 1, 0, 0, 0), datetime(2014, 1, 1, 12, 0, 0), "instance") + 2014, 1, 1, 0, 0, 0, tzinfo=pytz.utc), datetime(2014, 1, 1, 12, 0, 0, tzinfo=pytz.utc), "instance") assert_that(entities, contains_inanyorder(*expected_instances)) self.assert_entities_metadata_have_been_sanitize(entities) @@ -195,10 +197,27 @@ class DatabaseAdapterTest(unittest.TestCase): for fake_entity in fake_entities_in_period + fake_entities_out_period] entities = self.adapter.list_entities("project_id", datetime( - 2014, 1, 1, 6, 0, 0), datetime(2014, 1, 1, 9, 0, 0)) + 2014, 1, 1, 6, 0, 0, tzinfo=pytz.utc), datetime(2014, 1, 1, 9, 0, 0, tzinfo=pytz.utc)) assert_that(entities, contains_inanyorder(*fake_entities_in_period)) - def test_update_entity(self): + def test_list_entities_by_id(self): + start = datetime(2016, 3, 1, 0, 0, 0, 0, pytz.utc) + end = datetime(2016, 3, 3, 0, 0, 0, 0, pytz.utc) + proper_instance = a(instance().with_id("id1").with_start(2016, 3, 1, 0, 0, 0).with_end(2016, 3, 2, 0, 0, 0)) + instances = [ + proper_instance, + a(instance() + .with_id("id1") + .with_start(2016, 3, 2, 0, 0, 0) + .with_no_end()), + ] + [self.db.entity.insert(todict(fake_instance)) for fake_instance in instances] + + instance_list = self.adapter.list_entities_by_id("id1", start, end) + + assert_that(instance_list, contains_inanyorder(*[proper_instance])) + + def test_update_active_entity(self): fake_entity = a(instance()) end_date = datetime(2015, 10, 21, 16, 29, 0) @@ -207,6 +226,17 @@ class DatabaseAdapterTest(unittest.TestCase): self.assertEqual(self.db.entity.find_one({"entity_id": fake_entity.entity_id})["end"], end_date) + def test_update_closed_entity(self): + fake_entity = a(instance().with_end(2016, 3, 2, 0, 0, 0)) + + self.db.entity.insert(todict(fake_entity)) + fake_entity.flavor = "my_new_flavor" + self.adapter.update_closed_entity(fake_entity, data={"flavor": fake_entity.flavor}) + + db_entity = self.db.entity.find_one({"entity_id": fake_entity.entity_id}) + assert_that(db_entity['flavor'], fake_entity.flavor) + assert_that(db_entity['end'], fake_entity.end) + def test_replace_entity(self): fake_entity = a(instance()) fake_entity.os.distro = "Centos" diff --git a/tests/builder.py b/tests/builder.py index 3702bc9..880d212 100644 --- a/tests/builder.py +++ b/tests/builder.py @@ -45,7 +45,7 @@ class EntityBuilder(Builder): return self def with_start(self, year, month, day, hour, minute, second): - self.with_datetime_start(datetime(year, month, day, hour, minute, second)) + self.with_datetime_start(datetime(year, month, day, hour, minute, second, tzinfo=pytz.utc)) return self def with_datetime_start(self, date): @@ -53,7 +53,7 @@ class EntityBuilder(Builder): return self def with_end(self, year, month, day, hour, minute, second): - self.dict_object["end"] = datetime(year, month, day, hour, minute, second) + self.dict_object["end"] = datetime(year, month, day, hour, minute, second, tzinfo=pytz.utc) return self def with_no_end(self): diff --git a/tests/core/test_controller.py b/tests/core/test_controller.py index 18701bf..59a7fe4 100644 --- a/tests/core/test_controller.py +++ b/tests/core/test_controller.py @@ -15,20 +15,21 @@ import sys import logging import unittest -import pytz - from copy import copy from datetime import datetime, timedelta + +import pytz from dateutil.parser import parse from hamcrest import raises, calling, assert_that from flexmock import flexmock, flexmock_teardown from nose.tools import assert_raises -from tests.builder import a, instance, volume, volume_type +from almanach.common.exceptions.almanach_entity_not_found_exception import AlmanachEntityNotFoundException +from almanach.common.exceptions.multiple_entities_matching_query import MultipleEntitiesMatchingQuery +from tests.builder import a, instance, volume, volume_type from almanach import config -from almanach.common.almanach_entity_not_found_exception import AlmanachEntityNotFoundException -from almanach.common.date_format_exception import DateFormatException -from almanach.common.validation_exception import InvalidAttributeException +from almanach.common.exceptions.date_format_exception import DateFormatException +from almanach.common.exceptions.validation_exception import InvalidAttributeException from almanach.core.controller import Controller from almanach.core.model import Instance, Volume @@ -99,6 +100,64 @@ class ControllerTest(unittest.TestCase): self.controller.resize_instance(fake_instance.entity_id, "newly_flavor", dates_str) + def test_update_entity_closed_entity_flavor(self): + start = datetime(2016, 3, 1, 0, 0, 0, 0, pytz.utc) + end = datetime(2016, 3, 3, 0, 0, 0, 0, pytz.utc) + flavor = 'a_new_flavor' + fake_instance1 = a(instance().with_start(2016, 3, 1, 0, 0, 0).with_end(2016, 3, 2, 0, 0, 0)) + + (flexmock(self.database_adapter) + .should_receive("list_entities_by_id") + .with_args(fake_instance1.entity_id, start, end) + .and_return([fake_instance1]) + .twice()) + + (flexmock(self.database_adapter) + .should_receive("update_closed_entity") + .with_args(entity=fake_instance1, data={"flavor": flavor}) + .once()) + + self.controller.update_inactive_entity( + instance_id=fake_instance1.entity_id, + start=start, + end=end, + flavor=flavor, + ) + + def test_update_one_close_entity_return_multiple_entities(self): + fake_instances = [a(instance()), a(instance())] + + (flexmock(self.database_adapter) + .should_receive("list_entities_by_id") + .with_args(fake_instances[0].entity_id, fake_instances[0].start, fake_instances[0].end) + .and_return(fake_instances) + .once()) + + assert_that( + calling(self.controller.update_inactive_entity).with_args(instance_id=fake_instances[0].entity_id, + start=fake_instances[0].start, + end=fake_instances[0].end, + flavor=fake_instances[0].flavor), + raises(MultipleEntitiesMatchingQuery) + ) + + def test_update_one_close_entity_return_no_entity(self): + fake_instances = a(instance()) + + (flexmock(self.database_adapter) + .should_receive("list_entities_by_id") + .with_args(fake_instances.entity_id, fake_instances.start, fake_instances.end) + .and_return([]) + .once()) + + assert_that( + calling(self.controller.update_inactive_entity).with_args(instance_id=fake_instances.entity_id, + start=fake_instances.start, + end=fake_instances.end, + flavor=fake_instances.flavor), + raises(AlmanachEntityNotFoundException) + ) + def test_update_active_instance_entity_with_a_new_flavor(self): flavor = u"my flavor name" fake_instance1 = a(instance()) diff --git a/tests/test_api.py b/tests/test_api.py index 3becb62..311201f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -13,27 +13,26 @@ # limitations under the License. import json -import flask - from uuid import uuid4 from unittest import TestCase from datetime import datetime + +import flask from voluptuous import Invalid -from almanach.common.validation_exception import InvalidAttributeException from flexmock import flexmock, flexmock_teardown + from hamcrest import assert_that, has_key, equal_to, has_length, has_entry, has_entries, is_ +from almanach.common.exceptions.validation_exception import InvalidAttributeException from almanach import config -from almanach.common.date_format_exception import DateFormatException -from almanach.common.almanach_exception import AlmanachException +from almanach.common.exceptions.date_format_exception import DateFormatException +from almanach.common.exceptions.almanach_exception import AlmanachException from almanach.adapters import api_route_v1 as api_route - from tests.builder import a, instance, volume_type class ApiTest(TestCase): - def setUp(self): self.controller = flexmock() api_route.controller = self.controller @@ -59,9 +58,9 @@ class ApiTest(TestCase): def test_instances_with_authentication(self): self.having_config('api_auth_token', 'some token value') - self.controller.should_receive('list_instances')\ + self.controller.should_receive('list_instances') \ .with_args('TENANT_ID', a_date_matching("2014-01-01 00:00:00.0000"), - a_date_matching("2014-02-01 00:00:00.0000"))\ + a_date_matching("2014-02-01 00:00:00.0000")) \ .and_return([a(instance().with_id('123'))]) code, result = self.api_get('/project/TENANT_ID/instances', @@ -76,6 +75,42 @@ class ApiTest(TestCase): assert_that(result[0], has_key('entity_id')) assert_that(result[0]['entity_id'], equal_to('123')) + def test_update_instance_flavor_for_terminated_instance(self): + some_new_flavor = 'some_new_flavor' + data = dict(flavor=some_new_flavor) + start = '2016-03-01 00:00:00.000000' + end = '2016-03-03 00:00:00.000000' + + self.having_config('api_auth_token', 'some token value') + + self.controller.should_receive('update_inactive_entity') \ + .with_args( + instance_id="INSTANCE_ID", + start=a_date_matching(start), + end=a_date_matching(end), + flavor=some_new_flavor, + ).and_return(a( + instance(). + with_id('INSTANCE_ID'). + with_start(2016, 03, 01, 00, 0, 00). + with_end(2016, 03, 03, 00, 0, 00). + with_flavor(some_new_flavor)) + ) + + code, result = self.api_put( + '/entity/instance/INSTANCE_ID', + headers={'X-Auth-Token': 'some token value'}, + query_string={ + 'start': start, + 'end': end, + }, + data=data, + ) + assert_that(code, equal_to(200)) + assert_that(result, has_key('entity_id')) + assert_that(result, has_key('flavor')) + assert_that(result['flavor'], is_(some_new_flavor)) + def test_update_instance_entity_with_a_new_start_date(self): data = { "start_date": "2014-01-01 00:00:00.0000", @@ -83,10 +118,10 @@ class ApiTest(TestCase): self.having_config('api_auth_token', 'some token value') - self.controller.should_receive('update_active_instance_entity')\ + self.controller.should_receive('update_active_instance_entity') \ .with_args( - instance_id="INSTANCE_ID", - start_date=data["start_date"], + instance_id="INSTANCE_ID", + start_date=data["start_date"], ).and_return(a(instance().with_id('INSTANCE_ID').with_start(2014, 01, 01, 00, 0, 00))) code, result = self.api_put( @@ -99,7 +134,7 @@ class ApiTest(TestCase): assert_that(result, has_key('entity_id')) assert_that(result, has_key('start')) assert_that(result, has_key('end')) - assert_that(result['start'], is_("2014-01-01 00:00:00")) + assert_that(result['start'], is_("2014-01-01 00:00:00+00:00")) def test_instances_with_wrong_authentication(self): self.having_config('api_auth_token', 'some token value') @@ -205,8 +240,8 @@ class ApiTest(TestCase): self.controller.should_receive('create_volume_type') \ .with_args( - volume_type_id=data['type_id'], - volume_type_name=data['type_name']) \ + volume_type_id=data['type_id'], + volume_type_name=data['type_name']) \ .once() code, result = self.api_post('/volume_type', data=data, headers={'X-Auth-Token': 'some token value'}) @@ -323,9 +358,9 @@ class ApiTest(TestCase): ) assert_that(result, has_entries( { - "error": "The provided date has an invalid format. " - "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" - } + "error": "The provided date has an invalid format. " + "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" + } )) assert_that(code, equal_to(400)) @@ -381,9 +416,9 @@ class ApiTest(TestCase): code, result = self.api_delete('/volume/VOLUME_ID', data=data, headers={'X-Auth-Token': 'some token value'}) assert_that(result, has_entries( { - "error": "The provided date has an invalid format. " - "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" - } + "error": "The provided date has an invalid format. " + "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" + } )) assert_that(code, equal_to(400)) @@ -434,9 +469,9 @@ class ApiTest(TestCase): code, result = self.api_put('/volume/VOLUME_ID/resize', data=data, headers={'X-Auth-Token': 'some token value'}) assert_that(result, has_entries( { - "error": "The provided date has an invalid format. " - "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" - } + "error": "The provided date has an invalid format. " + "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" + } )) assert_that(code, equal_to(400)) @@ -494,9 +529,9 @@ class ApiTest(TestCase): code, result = self.api_put('/volume/VOLUME_ID/attach', data=data, headers={'X-Auth-Token': 'some token value'}) assert_that(result, has_entries( { - "error": "The provided date has an invalid format. " - "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" - } + "error": "The provided date has an invalid format. " + "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" + } )) assert_that(code, equal_to(400)) @@ -550,9 +585,9 @@ class ApiTest(TestCase): code, result = self.api_put('/volume/VOLUME_ID/detach', data=data, headers={'X-Auth-Token': 'some token value'}) assert_that(result, has_entries( { - "error": "The provided date has an invalid format. " - "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" - } + "error": "The provided date has an invalid format. " + "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" + } )) assert_that(code, equal_to(400)) @@ -642,9 +677,9 @@ class ApiTest(TestCase): ) assert_that(result, has_entries( { - "error": "The provided date has an invalid format. " - "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" - } + "error": "The provided date has an invalid format. " + "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" + } )) assert_that(code, equal_to(400)) @@ -723,9 +758,9 @@ class ApiTest(TestCase): code, result = self.api_delete('/instance/INSTANCE_ID', data=data, headers={'X-Auth-Token': 'some token value'}) assert_that(result, has_entries( { - "error": "The provided date has an invalid format. " - "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" - } + "error": "The provided date has an invalid format. " + "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" + } )) assert_that(code, equal_to(400)) @@ -770,9 +805,9 @@ class ApiTest(TestCase): ) assert_that(result, has_entries( { - "error": "The provided date has an invalid format. " - "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" - } + "error": "The provided date has an invalid format. " + "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z" + } )) assert_that(code, equal_to(400)) @@ -794,11 +829,11 @@ class ApiTest(TestCase): } self.controller.should_receive('rebuild_instance') \ .with_args( - instance_id=instance_id, - distro=data.get('distro'), - version=data.get('version'), - os_type=data.get('os_type'), - rebuild_date=data.get('rebuild_date')) \ + instance_id=instance_id, + distro=data.get('distro'), + version=data.get('version'), + os_type=data.get('os_type'), + rebuild_date=data.get('rebuild_date')) \ .once() code, result = self.api_put( @@ -880,7 +915,7 @@ class ApiTest(TestCase): headers = {} headers['Accept'] = accept result = getattr(http_client, method)(url, data=json.dumps(data), query_string=query_string, headers=headers) - return_data = json.loads(result.data)\ + return_data = json.loads(result.data) \ if result.headers.get('Content-Type') == 'application/json' \ else result.data return result.status_code, return_data @@ -914,9 +949,9 @@ class ApiTest(TestCase): .and_raise(InvalidAttributeException(errors)) code, result = self.api_put( - '/entity/instance/INSTANCE_ID', - data=data, - headers={'X-Auth-Token': 'some token value'} + '/entity/instance/INSTANCE_ID', + data=data, + headers={'X-Auth-Token': 'some token value'} ) assert_that(result, has_entries({ "error": formatted_errors @@ -925,7 +960,6 @@ class ApiTest(TestCase): class DateMatcher(object): - def __init__(self, date): self.date = date diff --git a/tests/validators/test_instance_validator.py b/tests/validators/test_instance_validator.py index 6030d56..fff14b9 100644 --- a/tests/validators/test_instance_validator.py +++ b/tests/validators/test_instance_validator.py @@ -1,9 +1,10 @@ import unittest -from almanach.common.validation_exception import InvalidAttributeException -from almanach.validators.instance_validator import InstanceValidator from hamcrest import assert_that, calling, raises, is_ +from almanach.common.exceptions.validation_exception import InvalidAttributeException +from almanach.validators.instance_validator import InstanceValidator + class InstanceValidatorTests(unittest.TestCase): def test_validate_update_with_invalid_attribute(self):