Modify api to support edition of closed entity

This commit is contained in:
Marx314 2016-05-18 12:05:09 -04:00 committed by Frédéric Guillot
parent 47ab4568ce
commit a81c0554b5
24 changed files with 364 additions and 119 deletions

View File

@ -7,11 +7,15 @@ ADD README.md /opt/almanach/src/
ADD requirements.txt /opt/almanach/src/ ADD requirements.txt /opt/almanach/src/
ADD LICENSE /opt/almanach/src/ ADD LICENSE /opt/almanach/src/
ADD almanach/resources/config/almanach.cfg /etc/almanach.cfg ADD almanach/resources/config/almanach.cfg /etc/almanach.cfg
COPY docker-entrypoint.sh /opt/almanach/entrypoint.sh
WORKDIR /opt/almanach RUN cd /opt/almanach/src && \
RUN cd src && \
pip install -r requirements.txt && \ 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 USER nobody
ENTRYPOINT ["/opt/almanach/entrypoint.sh"]

View File

@ -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. The actual Docker configuration assume that you already have RabbitMQ (mandatory for Openstack) and MongoDB configured for Almanach.
```bash ```bash
export RABBITMQ_URL="amqp://openstack:openstack@my-hostname:5672/" export RABBITMQ_URL="amqp://guest:guest@messaging:5672/"
export MONGODB_URL="mongodb://almanach:almanach@my-hostname:27017/almanach" export MONGODB_URL="mongodb://almanach:almanach@database:27017/almanach"
docker-compose build docker-compose build
docker-compose up docker-compose up

View File

@ -14,16 +14,20 @@
import logging import logging
import json import json
import jsonpickle
from datetime import datetime from datetime import datetime
from functools import wraps from functools import wraps
from almanach.common.validation_exception import InvalidAttributeException
import jsonpickle
from flask import Blueprint, Response, request from flask import Blueprint, Response, request
from werkzeug.wrappers import BaseResponse 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 import config
from almanach.common.date_format_exception import DateFormatException from almanach.common.exceptions.date_format_exception import DateFormatException
api = Blueprint("api", __name__) api = Blueprint("api", __name__)
controller = None controller = None
@ -53,9 +57,18 @@ def to_json(api_call):
except InvalidAttributeException as e: except InvalidAttributeException as e:
logging.warning(e.get_error_message()) logging.warning(e.get_error_message())
return encode({"error": e.get_error_message()}), 400, {"Content-Type": "application/json"} 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: except Exception as e:
logging.exception(e) logging.exception(e)
return Response(encode({"error": e.message}), 500, {"Content-Type": "application/json"}) return Response(encode({"error": e.message}), 500, {"Content-Type": "application/json"})
return decorator return decorator
@ -256,7 +269,12 @@ def list_entity(project_id):
def update_instance_entity(instance_id): def update_instance_entity(instance_id):
data = json.loads(request.data) data = json.loads(request.data)
logging.info("Updating instance entity with id %s with data %s", instance_id, 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"]) @api.route("/volume_types", methods=["GET"])

View File

@ -13,15 +13,16 @@
# limitations under the License. # limitations under the License.
import logging import logging
import pymongo
import pymongo
from pymongo.errors import ConfigurationError 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 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 database(function):
def _connection(self, *args, **kwargs): def _connection(self, *args, **kwargs):
@ -55,7 +56,6 @@ def ensureindex(db):
class DatabaseAdapter(object): class DatabaseAdapter(object):
def __init__(self): def __init__(self):
self.db = None self.db = None
@ -90,6 +90,22 @@ class DatabaseAdapter(object):
entities = self._get_entities_from_db(args) entities = self._get_entities_from_db(args)
return [build_entity_from_dict(entity) for entity in entities] 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 @database
def insert_entity(self, entity): def insert_entity(self, entity):
self._insert_entity(entity.as_dict()) self._insert_entity(entity.as_dict())

View File

@ -1,2 +0,0 @@
class AlmanachEntityNotFoundException(Exception):
pass

View File

View File

@ -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

View File

@ -11,9 +11,10 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from almanach.common.exceptions.almanach_exception import AlmanachException
class DateFormatException(Exception): class DateFormatException(AlmanachException):
def __init__(self, message=None): def __init__(self, message=None):
if not message: if not message:

View File

@ -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

View File

@ -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

View File

@ -11,9 +11,10 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # 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): def __init__(self, volume_type_id, message=None):
if not message: if not message:

View File

@ -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

View File

@ -16,7 +16,7 @@ import ConfigParser
import os import os
import os.path as os_path import os.path as os_path
from almanach.common.almanach_exception import AlmanachException from almanach.common.exceptions.almanach_exception import AlmanachException
configuration = ConfigParser.RawConfigParser() configuration = ConfigParser.RawConfigParser()

View File

@ -13,14 +13,16 @@
# limitations under the License. # limitations under the License.
import logging import logging
import pytz
from datetime import timedelta from datetime import timedelta
import pytz
from dateutil import parser as date_parser from dateutil import parser as date_parser
from pkg_resources import get_distribution from pkg_resources import get_distribution
from almanach.common.almanach_entity_not_found_exception import AlmanachEntityNotFoundException from almanach.common.exceptions.almanach_entity_not_found_exception import AlmanachEntityNotFoundException
from almanach.common.date_format_exception import DateFormatException 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.core.model import Instance, Volume, VolumeType
from almanach.validators.instance_validator import InstanceValidator from almanach.validators.instance_validator import InstanceValidator
from almanach import config from almanach import config
@ -107,6 +109,19 @@ class Controller(object):
instance.last_event = rebuild_date instance.last_event = rebuild_date
self.database_adapter.insert_entity(instance) 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): def update_active_instance_entity(self, instance_id, **kwargs):
try: try:
InstanceValidator().validate_update(kwargs) InstanceValidator().validate_update(kwargs)
@ -155,18 +170,20 @@ class Controller(object):
raise e raise e
def _update_instance_object(self, instance, **kwargs): 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(): for attribute, key in dict(start="start_date", end="end_date").items():
value = kwargs.get(key) if kwargs.get(key):
if value: entity[attribute] = self._validate_and_parse_date(kwargs.get(key))
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))
for attribute in ["name", "flavor", "os", "metadata"]: for attribute in ["name", "flavor", "os", "metadata"]:
value = kwargs.get(attribute) if kwargs.get(attribute):
if value: entity[attribute] = kwargs.get(attribute)
setattr(instance, attribute, value) return entity
logging.info(
"Updating entity for instance '{0}' with {1}={2}".format(instance.entity_id, attribute, value))
def _volume_attach_instance(self, volume_id, date, attachments): def _volume_attach_instance(self, volume_id, date, attachments):
volume = self.database_adapter.get_active_entity(volume_id) volume = self.database_adapter.get_active_entity(volume_id)

View File

@ -1,6 +1,7 @@
from almanach.common.validation_exception import InvalidAttributeException
from voluptuous import Schema, MultipleInvalid, Datetime, Required from voluptuous import Schema, MultipleInvalid, Datetime, Required
from almanach.common.exceptions.validation_exception import InvalidAttributeException
class InstanceValidator(object): class InstanceValidator(object):
def __init__(self): def __init__(self):

View File

@ -4,7 +4,7 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
command: almanach api /etc/almanach.cfg --host 0.0.0.0 command: api
environment: environment:
MONGODB_URL: ${MONGODB_URL} MONGODB_URL: ${MONGODB_URL}
ports: ports:
@ -13,7 +13,7 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
command: almanach collector /etc/almanach.cfg command: collector
environment: environment:
MONGODB_URL: ${MONGODB_URL} MONGODB_URL: ${MONGODB_URL}
RABBITMQ_URL: ${RABBITMQ_URL} RABBITMQ_URL: ${RABBITMQ_URL}

13
docker-entrypoint.sh Executable file
View File

@ -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 "$@"

View File

@ -13,12 +13,12 @@
# limitations under the License. # limitations under the License.
import unittest import unittest
import pytz
from datetime import datetime from datetime import datetime
import pytz
from flexmock import flexmock, flexmock_teardown 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 tests import messages
from almanach.adapters.bus_adapter import BusAdapter from almanach.adapters.bus_adapter import BusAdapter

View File

@ -12,18 +12,20 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import pkg_resources
import unittest import unittest
import mongomock
from datetime import datetime from datetime import datetime
import pkg_resources
import mongomock
from flexmock import flexmock, flexmock_teardown from flexmock import flexmock, flexmock_teardown
from hamcrest import assert_that, contains_inanyorder from hamcrest import assert_that, contains_inanyorder
from pymongo import MongoClient from pymongo import MongoClient
import pytz
from almanach.adapters.database_adapter import DatabaseAdapter from almanach.adapters.database_adapter import DatabaseAdapter
from almanach.common.volume_type_not_found_exception import VolumeTypeNotFoundException from almanach.common.exceptions.volume_type_not_found_exception import VolumeTypeNotFoundException
from almanach.common.almanach_exception import AlmanachException from almanach.common.exceptions.almanach_exception import AlmanachException
from almanach import config from almanach import config
from almanach.core.model import todict from almanach.core.model import todict
from tests.builder import a, instance, volume, volume_type 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] [self.db.entity.insert(todict(fake_entity)) for fake_entity in fake_instances + fake_volumes]
entities = self.adapter.list_entities("project_id", datetime( 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)) assert_that(entities, contains_inanyorder(*fake_instances))
def test_list_instances_with_decode_output(self): 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] [self.db.entity.insert(todict(fake_entity)) for fake_entity in fake_instances]
entities = self.adapter.list_entities("project_id", datetime( 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)) assert_that(entities, contains_inanyorder(*expected_instances))
self.assert_entities_metadata_have_been_sanitize(entities) 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] for fake_entity in fake_entities_in_period + fake_entities_out_period]
entities = self.adapter.list_entities("project_id", datetime( 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)) 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()) fake_entity = a(instance())
end_date = datetime(2015, 10, 21, 16, 29, 0) 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) 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): def test_replace_entity(self):
fake_entity = a(instance()) fake_entity = a(instance())
fake_entity.os.distro = "Centos" fake_entity.os.distro = "Centos"

View File

@ -45,7 +45,7 @@ class EntityBuilder(Builder):
return self return self
def with_start(self, year, month, day, hour, minute, second): 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 return self
def with_datetime_start(self, date): def with_datetime_start(self, date):
@ -53,7 +53,7 @@ class EntityBuilder(Builder):
return self return self
def with_end(self, year, month, day, hour, minute, second): 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 return self
def with_no_end(self): def with_no_end(self):

View File

@ -15,20 +15,21 @@
import sys import sys
import logging import logging
import unittest import unittest
import pytz
from copy import copy from copy import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pytz
from dateutil.parser import parse from dateutil.parser import parse
from hamcrest import raises, calling, assert_that from hamcrest import raises, calling, assert_that
from flexmock import flexmock, flexmock_teardown from flexmock import flexmock, flexmock_teardown
from nose.tools import assert_raises 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 import config
from almanach.common.almanach_entity_not_found_exception import AlmanachEntityNotFoundException from almanach.common.exceptions.date_format_exception import DateFormatException
from almanach.common.date_format_exception import DateFormatException from almanach.common.exceptions.validation_exception import InvalidAttributeException
from almanach.common.validation_exception import InvalidAttributeException
from almanach.core.controller import Controller from almanach.core.controller import Controller
from almanach.core.model import Instance, Volume 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) 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): def test_update_active_instance_entity_with_a_new_flavor(self):
flavor = u"my flavor name" flavor = u"my flavor name"
fake_instance1 = a(instance()) fake_instance1 = a(instance())

View File

@ -13,27 +13,26 @@
# limitations under the License. # limitations under the License.
import json import json
import flask
from uuid import uuid4 from uuid import uuid4
from unittest import TestCase from unittest import TestCase
from datetime import datetime from datetime import datetime
import flask
from voluptuous import Invalid from voluptuous import Invalid
from almanach.common.validation_exception import InvalidAttributeException
from flexmock import flexmock, flexmock_teardown from flexmock import flexmock, flexmock_teardown
from hamcrest import assert_that, has_key, equal_to, has_length, has_entry, has_entries, is_ 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 import config
from almanach.common.date_format_exception import DateFormatException from almanach.common.exceptions.date_format_exception import DateFormatException
from almanach.common.almanach_exception import AlmanachException from almanach.common.exceptions.almanach_exception import AlmanachException
from almanach.adapters import api_route_v1 as api_route from almanach.adapters import api_route_v1 as api_route
from tests.builder import a, instance, volume_type from tests.builder import a, instance, volume_type
class ApiTest(TestCase): class ApiTest(TestCase):
def setUp(self): def setUp(self):
self.controller = flexmock() self.controller = flexmock()
api_route.controller = self.controller api_route.controller = self.controller
@ -59,9 +58,9 @@ class ApiTest(TestCase):
def test_instances_with_authentication(self): def test_instances_with_authentication(self):
self.having_config('api_auth_token', 'some token value') 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"), .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'))]) .and_return([a(instance().with_id('123'))])
code, result = self.api_get('/project/TENANT_ID/instances', 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], has_key('entity_id'))
assert_that(result[0]['entity_id'], equal_to('123')) 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): def test_update_instance_entity_with_a_new_start_date(self):
data = { data = {
"start_date": "2014-01-01 00:00:00.0000", "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.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( .with_args(
instance_id="INSTANCE_ID", instance_id="INSTANCE_ID",
start_date=data["start_date"], start_date=data["start_date"],
).and_return(a(instance().with_id('INSTANCE_ID').with_start(2014, 01, 01, 00, 0, 00))) ).and_return(a(instance().with_id('INSTANCE_ID').with_start(2014, 01, 01, 00, 0, 00)))
code, result = self.api_put( code, result = self.api_put(
@ -99,7 +134,7 @@ class ApiTest(TestCase):
assert_that(result, has_key('entity_id')) assert_that(result, has_key('entity_id'))
assert_that(result, has_key('start')) assert_that(result, has_key('start'))
assert_that(result, has_key('end')) 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): def test_instances_with_wrong_authentication(self):
self.having_config('api_auth_token', 'some token value') self.having_config('api_auth_token', 'some token value')
@ -205,8 +240,8 @@ class ApiTest(TestCase):
self.controller.should_receive('create_volume_type') \ self.controller.should_receive('create_volume_type') \
.with_args( .with_args(
volume_type_id=data['type_id'], volume_type_id=data['type_id'],
volume_type_name=data['type_name']) \ volume_type_name=data['type_name']) \
.once() .once()
code, result = self.api_post('/volume_type', data=data, headers={'X-Auth-Token': 'some token value'}) 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( assert_that(result, has_entries(
{ {
"error": "The provided date has an invalid format. " "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" "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z"
} }
)) ))
assert_that(code, equal_to(400)) 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'}) code, result = self.api_delete('/volume/VOLUME_ID', data=data, headers={'X-Auth-Token': 'some token value'})
assert_that(result, has_entries( assert_that(result, has_entries(
{ {
"error": "The provided date has an invalid format. " "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" "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z"
} }
)) ))
assert_that(code, equal_to(400)) 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'}) code, result = self.api_put('/volume/VOLUME_ID/resize', data=data, headers={'X-Auth-Token': 'some token value'})
assert_that(result, has_entries( assert_that(result, has_entries(
{ {
"error": "The provided date has an invalid format. " "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" "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z"
} }
)) ))
assert_that(code, equal_to(400)) 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'}) code, result = self.api_put('/volume/VOLUME_ID/attach', data=data, headers={'X-Auth-Token': 'some token value'})
assert_that(result, has_entries( assert_that(result, has_entries(
{ {
"error": "The provided date has an invalid format. " "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" "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z"
} }
)) ))
assert_that(code, equal_to(400)) 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'}) code, result = self.api_put('/volume/VOLUME_ID/detach', data=data, headers={'X-Auth-Token': 'some token value'})
assert_that(result, has_entries( assert_that(result, has_entries(
{ {
"error": "The provided date has an invalid format. " "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" "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z"
} }
)) ))
assert_that(code, equal_to(400)) assert_that(code, equal_to(400))
@ -642,9 +677,9 @@ class ApiTest(TestCase):
) )
assert_that(result, has_entries( assert_that(result, has_entries(
{ {
"error": "The provided date has an invalid format. " "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" "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z"
} }
)) ))
assert_that(code, equal_to(400)) 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'}) code, result = self.api_delete('/instance/INSTANCE_ID', data=data, headers={'X-Auth-Token': 'some token value'})
assert_that(result, has_entries( assert_that(result, has_entries(
{ {
"error": "The provided date has an invalid format. " "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" "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z"
} }
)) ))
assert_that(code, equal_to(400)) assert_that(code, equal_to(400))
@ -770,9 +805,9 @@ class ApiTest(TestCase):
) )
assert_that(result, has_entries( assert_that(result, has_entries(
{ {
"error": "The provided date has an invalid format. " "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" "Format should be of yyyy-mm-ddThh:mm:ss.msZ, ex: 2015-01-31T18:24:34.1523Z"
} }
)) ))
assert_that(code, equal_to(400)) assert_that(code, equal_to(400))
@ -794,11 +829,11 @@ class ApiTest(TestCase):
} }
self.controller.should_receive('rebuild_instance') \ self.controller.should_receive('rebuild_instance') \
.with_args( .with_args(
instance_id=instance_id, instance_id=instance_id,
distro=data.get('distro'), distro=data.get('distro'),
version=data.get('version'), version=data.get('version'),
os_type=data.get('os_type'), os_type=data.get('os_type'),
rebuild_date=data.get('rebuild_date')) \ rebuild_date=data.get('rebuild_date')) \
.once() .once()
code, result = self.api_put( code, result = self.api_put(
@ -880,7 +915,7 @@ class ApiTest(TestCase):
headers = {} headers = {}
headers['Accept'] = accept headers['Accept'] = accept
result = getattr(http_client, method)(url, data=json.dumps(data), query_string=query_string, headers=headers) 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' \ if result.headers.get('Content-Type') == 'application/json' \
else result.data else result.data
return result.status_code, return_data return result.status_code, return_data
@ -914,9 +949,9 @@ class ApiTest(TestCase):
.and_raise(InvalidAttributeException(errors)) .and_raise(InvalidAttributeException(errors))
code, result = self.api_put( code, result = self.api_put(
'/entity/instance/INSTANCE_ID', '/entity/instance/INSTANCE_ID',
data=data, data=data,
headers={'X-Auth-Token': 'some token value'} headers={'X-Auth-Token': 'some token value'}
) )
assert_that(result, has_entries({ assert_that(result, has_entries({
"error": formatted_errors "error": formatted_errors
@ -925,7 +960,6 @@ class ApiTest(TestCase):
class DateMatcher(object): class DateMatcher(object):
def __init__(self, date): def __init__(self, date):
self.date = date self.date = date

View File

@ -1,9 +1,10 @@
import unittest 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 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): class InstanceValidatorTests(unittest.TestCase):
def test_validate_update_with_invalid_attribute(self): def test_validate_update_with_invalid_attribute(self):