Skip to content
This repository was archived by the owner on Jun 30, 2018. It is now read-only.

Commit 00f5d69

Browse files
pmilletteMaxime Belanger
authored andcommitted
Handle API response errors (#14)
Assert that exception message is not empty Added voluptuous
1 parent c621ede commit 00f5d69

File tree

12 files changed

+335
-39
lines changed

12 files changed

+335
-39
lines changed

almanach/adapters/api_route_v1.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from datetime import datetime
2020
from functools import wraps
21+
from almanach.common.validation_exception import InvalidAttributeException
2122
from flask import Blueprint, Response, request
2223
from werkzeug.wrappers import BaseResponse
2324

@@ -49,6 +50,9 @@ def decorator(*args, **kwargs):
4950
message = "The request you have made must have data. None was given."
5051
logging.warning(message)
5152
return encode({"error": message}), 400, {"Content-Type": "application/json"}
53+
except InvalidAttributeException as e:
54+
logging.warning(e.get_error_message())
55+
return encode({"error": e.get_error_message()}), 400, {"Content-Type": "application/json"}
5256
except Exception as e:
5357
logging.exception(e)
5458
return Response(encode({"error": e.message}), 500, {"Content-Type": "application/json"})
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class InvalidAttributeException(Exception):
2+
def __init__(self, errors):
3+
self.errors = errors
4+
5+
def get_error_message(self):
6+
messages = {}
7+
for error in self.errors:
8+
messages[error.path[0]] = error.msg
9+
10+
return messages

almanach/core/controller.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@
2222
from almanach.common.almanach_entity_not_found_exception import AlmanachEntityNotFoundException
2323
from almanach.common.date_format_exception import DateFormatException
2424
from almanach.core.model import Instance, Volume, VolumeType
25+
from almanach.validators.instance_validator import InstanceValidator
2526
from almanach import config
2627

2728

2829
class Controller(object):
29-
3030
def __init__(self, database_adapter):
3131
self.database_adapter = database_adapter
3232
self.metadata_whitelist = config.device_metadata_whitelist()
@@ -107,12 +107,11 @@ def rebuild_instance(self, instance_id, distro, version, os_type, rebuild_date):
107107
instance.last_event = rebuild_date
108108
self.database_adapter.insert_entity(instance)
109109

110-
def update_active_instance_entity(self, instance_id, start_date):
110+
def update_active_instance_entity(self, instance_id, **kwargs):
111111
try:
112+
InstanceValidator().validate_update(kwargs)
112113
instance = self.database_adapter.get_active_entity(instance_id)
113-
instance.start = self._validate_and_parse_date(start_date)
114-
115-
logging.info("Updating entity for instance '{0}' with a new start_date={1}".format(instance_id, start_date))
114+
self._update_instance_object(instance, **kwargs)
116115
self.database_adapter.update_active_entity(instance)
117116
return instance
118117
except KeyError as e:
@@ -155,6 +154,20 @@ def detach_volume(self, volume_id, date, attachments):
155154
logging.error("Trying to detach a volume with id '%s' not in the database yet." % volume_id)
156155
raise e
157156

157+
def _update_instance_object(self, instance, **kwargs):
158+
for attribute, key in dict(start="start_date", end="end_date").items():
159+
value = kwargs.get(key)
160+
if value:
161+
setattr(instance, attribute, self._validate_and_parse_date(value))
162+
logging.info("Updating entity for instance '{0}' with {1}={2}".format(instance.entity_id, key, value))
163+
164+
for attribute in ["name", "flavor", "os", "metadata"]:
165+
value = kwargs.get(attribute)
166+
if value:
167+
setattr(instance, attribute, value)
168+
logging.info(
169+
"Updating entity for instance '{0}' with {1}={2}".format(instance.entity_id, attribute, value))
170+
158171
def _volume_attach_instance(self, volume_id, date, attachments):
159172
volume = self.database_adapter.get_active_entity(volume_id)
160173
date = self._localize_date(date)

almanach/validators/__init__.py

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from almanach.common.validation_exception import InvalidAttributeException
2+
from voluptuous import Schema, MultipleInvalid, Datetime, Required
3+
4+
5+
class InstanceValidator(object):
6+
def __init__(self):
7+
self.schema = Schema({
8+
'name': unicode,
9+
'flavor': unicode,
10+
'os': {
11+
Required('distro'): unicode,
12+
Required('version'): unicode,
13+
Required('os_type'): unicode,
14+
},
15+
'metadata': dict,
16+
'start_date': Datetime(),
17+
'end_date': Datetime(),
18+
})
19+
20+
def validate_update(self, payload):
21+
try:
22+
return self.schema(payload)
23+
except MultipleInvalid as e:
24+
raise InvalidAttributeException(e.errors)

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ pymongo==2.7.2
55
kombu>=3.0.30
66
python-dateutil==2.2
77
python-pymongomodem==0.0.3
8-
pytz>=2014.10
8+
pytz>=2014.10
9+
voluptuous==0.8.11

test-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ nose-cov==1.6
66
nose-blockage==0.1.2
77
flexmock==0.9.4
88
mongomock==2.0.0
9-
PyHamcrest==1.8.1
9+
PyHamcrest==1.8.5
1010
flake8==2.5.4

tests/builder.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ def with_no_end(self):
6060
self.dict_object["end"] = None
6161
return self
6262

63+
def with_flavor(self, flavor):
64+
self.dict_object["flavor"] = flavor
65+
return self
66+
6367
def with_metadata(self, metadata):
6468
self.dict_object['metadata'] = metadata
6569
return self

tests/core/test_controller.py

Lines changed: 116 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,27 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import sys
16+
import logging
1517
import unittest
16-
from datetime import datetime, timedelta
17-
1818
import pytz
19-
from dateutil import parser as date_parser
19+
20+
from copy import copy
21+
from datetime import datetime, timedelta
22+
from dateutil.parser import parse
23+
from hamcrest import raises, calling, assert_that
2024
from flexmock import flexmock, flexmock_teardown
2125
from nose.tools import assert_raises
26+
from tests.builder import a, instance, volume, volume_type
27+
2228
from almanach import config
2329
from almanach.common.almanach_entity_not_found_exception import AlmanachEntityNotFoundException
2430
from almanach.common.date_format_exception import DateFormatException
31+
from almanach.common.validation_exception import InvalidAttributeException
2532
from almanach.core.controller import Controller
2633
from almanach.core.model import Instance, Volume
27-
from tests.builder import a, instance, volume, volume_type
34+
35+
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
2836

2937

3038
class ControllerTest(unittest.TestCase):
@@ -70,6 +78,9 @@ def test_resize_instance(self):
7078
fake_instance = a(instance())
7179

7280
dates_str = "2015-10-21T16:25:00.000000Z"
81+
fake_instance.start = parse(dates_str)
82+
fake_instance.end = None
83+
fake_instance.last_event = parse(dates_str)
7384

7485
(flexmock(self.database_adapter)
7586
.should_receive("get_active_entity")
@@ -78,39 +89,100 @@ def test_resize_instance(self):
7889
.once())
7990
(flexmock(self.database_adapter)
8091
.should_receive("close_active_entity")
81-
.with_args(fake_instance.entity_id, date_parser.parse(dates_str))
92+
.with_args(fake_instance.entity_id, parse(dates_str))
8293
.once())
83-
fake_instance.start = dates_str
84-
fake_instance.end = None
85-
fake_instance.last_event = dates_str
94+
8695
(flexmock(self.database_adapter)
8796
.should_receive("insert_entity")
8897
.with_args(fake_instance)
8998
.once())
9099

91100
self.controller.resize_instance(fake_instance.entity_id, "newly_flavor", dates_str)
92101

93-
def test_update_active_instance_entity_with_a_new_start_date(self):
102+
def test_update_active_instance_entity_with_a_new_flavor(self):
103+
flavor = u"my flavor name"
94104
fake_instance1 = a(instance())
95-
fake_instance2 = fake_instance1
96-
fake_instance2.start = "2015-10-21T16:25:00.000000Z"
105+
fake_instance2 = copy(fake_instance1)
106+
self._expect_get_active_entity_and_update(fake_instance1, fake_instance2, flavor=flavor)
97107

98-
(flexmock(self.database_adapter)
99-
.should_receive("get_active_entity")
100-
.with_args(fake_instance1.entity_id)
101-
.and_return(fake_instance1)
102-
.once())
108+
self.controller.update_active_instance_entity(
109+
instance_id=fake_instance1.entity_id,
110+
flavor=flavor,
111+
)
103112

104-
(flexmock(self.database_adapter)
105-
.should_receive("update_active_entity")
106-
.with_args(fake_instance2)
107-
.once())
113+
def test_update_active_instance_entity_with_a_new_name(self):
114+
name = u"my instance name"
115+
fake_instance1 = a(instance())
116+
fake_instance2 = copy(fake_instance1)
117+
self._expect_get_active_entity_and_update(fake_instance1, fake_instance2, name=name)
118+
119+
self.controller.update_active_instance_entity(
120+
instance_id=fake_instance1.entity_id,
121+
name=name,
122+
)
123+
124+
def test_update_active_instance_entity_with_a_new_os(self):
125+
os = {
126+
"os_type": u"linux",
127+
"version": u"7",
128+
"distro": u"centos"
129+
}
130+
fake_instance1 = a(instance())
131+
fake_instance2 = copy(fake_instance1)
132+
self._expect_get_active_entity_and_update(fake_instance1, fake_instance2, os=os)
133+
134+
self.controller.update_active_instance_entity(
135+
instance_id=fake_instance1.entity_id,
136+
os=os,
137+
)
138+
139+
def test_update_active_instance_entity_with_a_new_metadata(self):
140+
metadata = {
141+
"key": "value"
142+
}
143+
fake_instance1 = a(instance())
144+
fake_instance2 = copy(fake_instance1)
145+
self._expect_get_active_entity_and_update(fake_instance1, fake_instance2, metadata=metadata)
146+
147+
self.controller.update_active_instance_entity(
148+
instance_id=fake_instance1.entity_id,
149+
metadata=metadata,
150+
)
151+
152+
def test_update_active_instance_entity_with_a_new_start_date(self):
153+
fake_instance1 = a(instance())
154+
fake_instance2 = copy(fake_instance1)
155+
self._expect_get_active_entity_and_update(fake_instance1, fake_instance2, start="2015-10-21T16:25:00.000000Z")
108156

109157
self.controller.update_active_instance_entity(
110158
instance_id=fake_instance1.entity_id,
111159
start_date="2015-10-21T16:25:00.000000Z",
112160
)
113161

162+
def test_update_active_instance_entity_with_a_new_end_date(self):
163+
fake_instance1 = a(instance())
164+
fake_instance2 = copy(fake_instance1)
165+
self._expect_get_active_entity_and_update(fake_instance1, fake_instance2, end="2015-10-21T16:25:00.000000Z")
166+
167+
self.controller.update_active_instance_entity(
168+
instance_id=fake_instance1.entity_id,
169+
end_date="2015-10-21T16:25:00.000000Z",
170+
)
171+
172+
def test_instance_updated_wrong_attributes_raises_exception(self):
173+
fake_instance1 = a(instance())
174+
175+
(flexmock(self.database_adapter)
176+
.should_receive("get_active_entity")
177+
.with_args(fake_instance1.entity_id)
178+
.and_return(fake_instance1)
179+
.never())
180+
181+
assert_that(
182+
calling(self.controller.update_active_instance_entity).with_args(instance_id=fake_instance1.entity_id,
183+
wrong_attribute="this is wrong"),
184+
raises(InvalidAttributeException))
185+
114186
def test_instance_created_but_its_an_old_event(self):
115187
fake_instance = a(instance()
116188
.with_last_event(pytz.utc.localize(datetime(2015, 10, 21, 16, 29, 0))))
@@ -158,7 +230,7 @@ def test_instance_deleted(self):
158230

159231
(flexmock(self.database_adapter)
160232
.should_receive("close_active_entity")
161-
.with_args("id1", date_parser.parse("2015-10-21T16:25:00.000000Z"))
233+
.with_args("id1", parse("2015-10-21T16:25:00.000000Z"))
162234
.once())
163235

164236
self.controller.delete_instance("id1", "2015-10-21T16:25:00.000000Z")
@@ -384,6 +456,10 @@ def test_create_volume_but_its_an_old_event(self):
384456
def test_volume_updated(self):
385457
fake_volume = a(volume())
386458
dates_str = "2015-10-21T16:25:00.000000Z"
459+
fake_volume.size = "new_size"
460+
fake_volume.start = parse(dates_str)
461+
fake_volume.end = None
462+
fake_volume.last_event = parse(dates_str)
387463

388464
(flexmock(self.database_adapter)
389465
.should_receive("get_active_entity")
@@ -392,12 +468,9 @@ def test_volume_updated(self):
392468
.once())
393469
(flexmock(self.database_adapter)
394470
.should_receive("close_active_entity")
395-
.with_args(fake_volume.entity_id, date_parser.parse(dates_str))
471+
.with_args(fake_volume.entity_id, parse(dates_str))
396472
.once())
397-
fake_volume.size = "new_size"
398-
fake_volume.start = dates_str
399-
fake_volume.end = None
400-
fake_volume.last_event = dates_str
473+
401474
(flexmock(self.database_adapter)
402475
.should_receive("insert_entity")
403476
.with_args(fake_volume)
@@ -648,3 +721,20 @@ def test_list_volume_types(self):
648721
.once())
649722

650723
self.assertEqual(len(self.controller.list_volume_types()), 2)
724+
725+
def _expect_get_active_entity_and_update(self, fake_instance1, fake_instance2, **kwargs):
726+
for key, value in kwargs.items():
727+
if key in ['start', 'end']:
728+
value = parse(value)
729+
730+
setattr(fake_instance2, key, value)
731+
732+
(flexmock(self.database_adapter)
733+
.should_receive("get_active_entity")
734+
.with_args(fake_instance1.entity_id)
735+
.and_return(fake_instance1)
736+
.once())
737+
(flexmock(self.database_adapter)
738+
.should_receive("update_active_entity")
739+
.with_args(fake_instance2)
740+
.once())

0 commit comments

Comments
 (0)