Common exception handling

Change-Id: Ibb2b9623f8d5fa7380c930e1b099028a94932b70
This commit is contained in:
Feodor Tersin 2015-02-20 20:21:14 +03:00
parent 8a9b4d53f5
commit 1c30d32264
12 changed files with 105 additions and 188 deletions

View File

@ -317,7 +317,7 @@ def exception_to_ec2code(ex):
return code return code
def ec2_error_ex(ex, req, code=None, message=None, unexpected=False): def ec2_error_ex(ex, req, unexpected=False):
"""Return an EC2 error response. """Return an EC2 error response.
Return an EC2 error response based on passed exception and log Return an EC2 error response based on passed exception and log
@ -332,12 +332,12 @@ def ec2_error_ex(ex, req, code=None, message=None, unexpected=False):
Unexpected 5xx errors may contain sensitive information, Unexpected 5xx errors may contain sensitive information,
suppress their messages for security. suppress their messages for security.
""" """
if not code: code = exception_to_ec2code(ex)
code = exception_to_ec2code(ex) for status_name in ('code', 'status', 'status_code', 'http_status'):
status = getattr(ex, 'code', None) status = getattr(ex, status_name, None)
if not isinstance(status, int): if isinstance(status, int):
status = getattr(ex, 'status', None) break
if not status or not isinstance(status, int): else:
status = 500 status = 500
if unexpected: if unexpected:
@ -347,12 +347,6 @@ def ec2_error_ex(ex, req, code=None, message=None, unexpected=False):
else: else:
log_fun = LOG.debug log_fun = LOG.debug
log_msg = _("%(ex_name)s raised: %(ex_str)s") log_msg = _("%(ex_name)s raised: %(ex_str)s")
# NOTE(jruzicka): For compatibility with EC2 API, treat expected
# exceptions as client (4xx) errors. The exception error code is 500
# by default and most exceptions inherit this from EC2Exception even
# though they are actually client errors in most cases.
if status >= 500:
status = 400
exc_info = None exc_info = None
context = req.environ['ec2api.context'] context = req.environ['ec2api.context']
@ -363,8 +357,14 @@ def ec2_error_ex(ex, req, code=None, message=None, unexpected=False):
} }
log_fun(log_msg % log_msg_args, context=context, exc_info=exc_info) log_fun(log_msg % log_msg_args, context=context, exc_info=exc_info)
if ex.args and not message and (not unexpected or status < 500): if unexpected and status >= 500:
message = _('Unknown error occurred.')
elif getattr(ex, 'message', None):
message = unicode(ex.message)
elif ex.args and any(arg for arg in ex.args):
message = " ".join(map(unicode, ex.args)) message = " ".join(map(unicode, ex.args))
else:
message = unicode(ex)
if unexpected: if unexpected:
# Log filtered environment for unexpected errors. # Log filtered environment for unexpected errors.
env = req.environ.copy() env = req.environ.copy()
@ -372,8 +372,6 @@ def ec2_error_ex(ex, req, code=None, message=None, unexpected=False):
if not isinstance(env[k], six.string_types): if not isinstance(env[k], six.string_types):
env.pop(k) env.pop(k)
log_fun(_('Environment: %s') % jsonutils.dumps(env)) log_fun(_('Environment: %s') % jsonutils.dumps(env))
if not message:
message = _('Unknown error occurred.')
return faults.ec2_error_response(request_id, code, message, status=status) return faults.ec2_error_response(request_id, code, message, status=status)

View File

@ -25,7 +25,7 @@ import time
import boto.s3.connection import boto.s3.connection
import eventlet import eventlet
from glanceclient import exc as glance_exception from glanceclient.common import exceptions as glance_exception
from lxml import etree from lxml import etree
from oslo.config import cfg from oslo.config import cfg
from oslo_concurrency import processutils from oslo_concurrency import processutils

View File

@ -19,7 +19,7 @@ import itertools
import random import random
import re import re
from glanceclient import exc as glance_exception from glanceclient.common import exceptions as glance_exception
from novaclient import exceptions as nova_exception from novaclient import exceptions as nova_exception
from oslo.config import cfg from oslo.config import cfg

View File

@ -67,9 +67,9 @@ def delete_route(context, route_table_id, destination_cidr_block):
raise exception.InvalidParameterValue(msg) raise exception.InvalidParameterValue(msg)
break break
else: else:
raise exception.InvalidRouteNotFound({ raise exception.InvalidRouteNotFound(
'route_table_id': route_table_id, route_table_id=route_table_id,
'destination_cidr_block': destination_cidr_block}) destination_cidr_block=destination_cidr_block)
rollback_route_table_state = copy.deepcopy(route_table) rollback_route_table_state = copy.deepcopy(route_table)
del route_table['routes'][route_index] del route_table['routes'][route_index]
with common.OnCrashCleaner() as cleaner: with common.OnCrashCleaner() as cleaner:

View File

@ -23,6 +23,7 @@ SHOULD include dedicated exception logging.
import sys import sys
from oslo.config import cfg from oslo.config import cfg
import six
from ec2api.openstack.common.gettextutils import _ from ec2api.openstack.common.gettextutils import _
from ec2api.openstack.common import log as logging from ec2api.openstack.common import log as logging
@ -60,7 +61,7 @@ class EC2Exception(Exception):
""" """
msg_fmt = _("An unknown exception occurred.") msg_fmt = _("An unknown exception occurred.")
code = 500 code = 400
headers = {} headers = {}
safe = False safe = False
@ -76,12 +77,12 @@ class EC2Exception(Exception):
if not message: if not message:
try: try:
message = self.msg_fmt % kwargs message = self.msg_fmt % kwargs
except Exception: except Exception:
exc_info = sys.exc_info() exc_info = sys.exc_info()
# kwargs doesn't match a variable in the message # kwargs doesn't match a variable in the message
# log the issue and the kwargs # log the issue and the kwargs
LOG.exception(_('Exception in string format operation')) LOG.exception(_('Exception in string format operation for '
'%s exception'), self.__class__.__name__)
for name, value in kwargs.iteritems(): for name, value in kwargs.iteritems():
LOG.error("%s: %s" % (name, value)) LOG.error("%s: %s" % (name, value))
@ -90,6 +91,14 @@ class EC2Exception(Exception):
else: else:
# at least get the core message out if something happened # at least get the core message out if something happened
message = self.msg_fmt message = self.msg_fmt
elif not isinstance(message, six.string_types):
LOG.error(_("Message '%(msg)s' for %(ex)s exception is not "
"a string"),
{'msg': message, 'ex': self.__class__.__name__})
if CONF.fatal_exception_format_errors:
raise TypeError(_('Invalid exception message format'))
else:
message = self.msg_fmt
super(EC2Exception, self).__init__(message) super(EC2Exception, self).__init__(message)
@ -101,17 +110,14 @@ class EC2Exception(Exception):
class Unsupported(EC2Exception): class Unsupported(EC2Exception):
msg_fmt = _("The specified request is unsupported. %(reason)s") msg_fmt = _("The specified request is unsupported. %(reason)s")
code = 400
class Overlimit(EC2Exception): class Overlimit(EC2Exception):
msg_fmt = _("Limit exceeded.") msg_fmt = _("Limit exceeded.")
code = 400
class Invalid(EC2Exception): class Invalid(EC2Exception):
msg_fmt = _("Unacceptable parameters.") msg_fmt = _("Unacceptable parameters.")
code = 400
class InvalidRequest(Invalid): class InvalidRequest(Invalid):
@ -155,7 +161,6 @@ class ValidationError(Invalid):
class EC2NotFound(EC2Exception): class EC2NotFound(EC2Exception):
msg_fmt = _("Resource could not be found.") msg_fmt = _("Resource could not be found.")
code = 400
class InvalidInstanceIDNotFound(EC2NotFound): class InvalidInstanceIDNotFound(EC2NotFound):
@ -258,7 +263,6 @@ class InvalidAvailabilityZoneNotFound(EC2NotFound):
class IncorrectState(EC2Exception): class IncorrectState(EC2Exception):
ec2_code = 'IncorrectState' ec2_code = 'IncorrectState'
code = 400
msg_fmt = _("The resource is in incorrect state for the request - reason: " msg_fmt = _("The resource is in incorrect state for the request - reason: "
"'%(reason)s'") "'%(reason)s'")
@ -407,4 +411,4 @@ class RulesPerSecurityGroupLimitExceeded(Overlimit):
class NovaDbInstanceNotFound(EC2Exception): class NovaDbInstanceNotFound(EC2Exception):
pass code = 500

View File

@ -24,7 +24,7 @@ CONF = config.CONF
class ImageTest(base.EC2TestCase): class ImageTest(base.EC2TestCase):
@testtools.skipUnless(CONF.aws.run_incompatible_tests, @testtools.skipUnless(CONF.aws.run_incompatible_tests,
"Openstack doesn't report right RootDeviceType") "Openstack doesn't report right RootDeviceType")
@testtools.skipUnless(CONF.aws.ebs_image_id, "EBS image id is not defined") @testtools.skipUnless(CONF.aws.ebs_image_id, "EBS image id is not defined")
def test_check_ebs_image_type(self): def test_check_ebs_image_type(self):
image_id = CONF.aws.ebs_image_id image_id = CONF.aws.ebs_image_id
@ -59,6 +59,7 @@ class ImageTest(base.EC2TestCase):
self.assertIsNotNone(ebs.get('VolumeSize')) self.assertIsNotNone(ebs.get('VolumeSize'))
self.assertIsNotNone(ebs.get('VolumeType')) self.assertIsNotNone(ebs.get('VolumeType'))
@testtools.skipUnless(CONF.aws.ebs_image_id, "EBS image id is not defined")
def test_describe_image_with_filters(self): def test_describe_image_with_filters(self):
image_id = CONF.aws.ebs_image_id image_id = CONF.aws.ebs_image_id
resp, data = self.client.DescribeImages(ImageIds=[image_id]) resp, data = self.client.DescribeImages(ImageIds=[image_id])

View File

@ -15,6 +15,7 @@
import itertools import itertools
import mock import mock
from oslo.config import cfg
from oslotest import base as test_base from oslotest import base as test_base
import ec2api.api.apirequest import ec2api.api.apirequest
@ -73,6 +74,10 @@ class ApiTestCase(test_base.BaseTestCase):
self.isotime = isotime_patcher.start() self.isotime = isotime_patcher.start()
self.addCleanup(isotime_patcher.stop) self.addCleanup(isotime_patcher.stop)
conf = cfg.CONF
conf.set_override('fatal_exception_format_errors', True)
self.addCleanup(conf.reset)
def execute(self, action, args): def execute(self, action, args):
ec2_request = ec2api.api.apirequest.APIRequest(action, 'fake_v1', args) ec2_request = ec2api.api.apirequest.APIRequest(action, 'fake_v1', args)
ec2_context = self._create_context() ec2_context = self._create_context()

View File

@ -15,7 +15,13 @@
import collections import collections
import uuid import uuid
from boto import exception as boto_exception
from cinderclient import exceptions as cinder_exception
from glanceclient.common import exceptions as glance_exception
from keystoneclient import exceptions as keystone_exception
import mock import mock
from neutronclient.common import exceptions as neutron_exception
from novaclient import exceptions as nova_exception
from oslotest import base as test_base from oslotest import base as test_base
from ec2api import api from ec2api import api
@ -83,9 +89,21 @@ class ApiInitTestCase(test_base.BaseTestCase):
self.controller.fake_action.assert_called_once_with( self.controller.fake_action.assert_called_once_with(
self.fake_context, param='fake_param') self.fake_context, param='fake_param')
do_check(exception.EC2Exception('fake_msg'), 400, do_check(exception.EC2Exception('fake_msg'),
'EC2Exception', 'fake_msg') 400, 'EC2Exception', 'fake_msg')
do_check(KeyError('fake_msg'), 500, do_check(KeyError('fake_msg'),
'KeyError', 'Unknown error occurred.') 500, 'KeyError', 'Unknown error occurred.')
do_check(exception.InvalidVpcIDNotFound('fake_msg'), 400, do_check(exception.InvalidVpcIDNotFound('fake_msg'),
'InvalidVpcID.NotFound', 'fake_msg') 400, 'InvalidVpcID.NotFound', 'fake_msg')
do_check(nova_exception.BadRequest(400, message='fake_msg'),
400, 'BadRequest', 'fake_msg')
do_check(glance_exception.HTTPBadRequest(),
400, 'HTTPBadRequest', 'HTTPBadRequest (HTTP 400)')
do_check(cinder_exception.BadRequest(400, message='fake_msg'),
400, 'BadRequest', 'fake_msg')
do_check(neutron_exception.BadRequest(message='fake_msg'),
400, 'BadRequest', 'fake_msg')
do_check(keystone_exception.BadRequest(message='fake_msg'),
400, 'BadRequest', 'fake_msg')
do_check(boto_exception.S3ResponseError(400, '', 'fake_msg'),
400, 'S3ResponseError', 'fake_msg')

View File

@ -1,132 +0,0 @@
#
# Copyright 2013 - Red Hat, Inc.
#
# 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.
#
"""
Unit tests for EC2 error responses.
"""
from lxml import etree
from oslotest import base as test_base
from ec2api import api as ec2
from ec2api import context
from ec2api import wsgi
class TestClientExceptionEC2(Exception):
ec2_code = 'ClientException.Test'
message = "Test Client Exception."
code = 400
class TestServerExceptionEC2(Exception):
ec2_code = 'ServerException.Test'
message = "Test Server Exception."
code = 500
class Ec2ErrorResponseTestCase(test_base.BaseTestCase):
"""Test EC2 error responses.
This deals mostly with api/ec2/__init__.py code, especially
the ec2_error_ex helper.
"""
def setUp(self):
super(Ec2ErrorResponseTestCase, self).setUp()
self.context = context.RequestContext('test_user_id',
'test_project_id')
self.req = wsgi.Request.blank('/test')
self.req.environ['ec2api.context'] = self.context
def _validate_ec2_error(self, response, http_status, ec2_code, msg=None,
unknown_msg=False):
self.assertEqual(response.status_code, http_status,
'Expected HTTP status %s' % http_status)
root_e = etree.XML(response.body)
self.assertEqual(root_e.tag, 'Response',
"Top element must be Response.")
errors_e = root_e.find('Errors')
self.assertEqual(len(errors_e), 1,
"Expected exactly one Error element in Errors.")
error_e = errors_e[0]
self.assertEqual(error_e.tag, 'Error',
"Expected Error element.")
# Code
code_e = error_e.find('Code')
self.assertIsNotNone(code_e, "Code element must be present.")
self.assertEqual(code_e.text, ec2_code)
# Message
if msg or unknown_msg:
message_e = error_e.find('Message')
self.assertIsNotNone(code_e, "Message element must be present.")
if msg:
self.assertEqual(message_e.text, msg)
elif unknown_msg:
self.assertEqual(message_e.text, "Unknown error occurred.",
"Error message should be anonymous.")
# RequestID
requestid_e = root_e.find('RequestID')
self.assertIsNotNone(requestid_e,
'RequestID element should be present.')
self.assertEqual(requestid_e.text, self.context.request_id)
def test_exception_ec2_4xx(self):
"""Test response to EC2 exception with code = 400."""
msg = "Test client failure."
err = ec2.ec2_error_ex(TestClientExceptionEC2(msg), self.req)
self._validate_ec2_error(err, TestClientExceptionEC2.code,
TestClientExceptionEC2.ec2_code, msg)
def test_exception_ec2_5xx(self):
"""Test response to EC2 exception with code = 500.
Expected errors are treated as client ones even with 5xx code.
"""
msg = "Test client failure with 5xx error code."
err = ec2.ec2_error_ex(TestServerExceptionEC2(msg), self.req)
self._validate_ec2_error(err, 400, TestServerExceptionEC2.ec2_code,
msg)
def test_unexpected_exception_ec2_4xx(self):
"""Test response to unexpected EC2 exception with code = 400."""
msg = "Test unexpected client failure."
err = ec2.ec2_error_ex(TestClientExceptionEC2(msg), self.req,
unexpected=True)
self._validate_ec2_error(err, TestClientExceptionEC2.code,
TestClientExceptionEC2.ec2_code, msg)
def test_unexpected_exception_ec2_5xx(self):
"""Test response to unexpected EC2 exception with code = 500.
Server exception messages (with code >= 500 or without code) should
be filtered as they might contain sensitive information.
"""
msg = "Test server failure."
err = ec2.ec2_error_ex(TestServerExceptionEC2(msg), self.req,
unexpected=True)
self._validate_ec2_error(err, TestServerExceptionEC2.code,
TestServerExceptionEC2.ec2_code,
unknown_msg=True)
def test_unexpected_exception_builtin(self):
"""Test response to builtin unexpected exception.
Server exception messages (with code >= 500 or without code) should
be filtered as they might contain sensitive information.
"""
msg = "Test server failure."
err = ec2.ec2_error_ex(RuntimeError(msg), self.req, unexpected=True)
self._validate_ec2_error(err, 500, 'RuntimeError', unknown_msg=True)

View File

@ -160,7 +160,7 @@ class ImageTestCase(base.ApiTestCase):
{'ImageLocation': fakes.LOCATION_IMAGE_1}) {'ImageLocation': fakes.LOCATION_IMAGE_1})
self.assertThat(resp, matchers.DictMatches( self.assertThat(resp, matchers.DictMatches(
{'http_status_code': 200, {'http_status_code': 200,
'imageId': fakes.ID_EC2_IMAGE_1})) 'imageId': fakes.ID_EC2_IMAGE_1}))
s3_create.assert_called_once_with( s3_create.assert_called_once_with(
mock.ANY, mock.ANY,
@ -174,7 +174,7 @@ class ImageTestCase(base.ApiTestCase):
'Name': 'an image name'}) 'Name': 'an image name'})
self.assertThat(resp, matchers.DictMatches( self.assertThat(resp, matchers.DictMatches(
{'http_status_code': 200, {'http_status_code': 200,
'imageId': fakes.ID_EC2_IMAGE_1})) 'imageId': fakes.ID_EC2_IMAGE_1}))
s3_create.assert_called_once_with( s3_create.assert_called_once_with(
mock.ANY, mock.ANY,
@ -196,19 +196,31 @@ class ImageTestCase(base.ApiTestCase):
'BlockDeviceMapping.1.Ebs.SnapshotId': fakes.ID_EC2_SNAPSHOT_1}) 'BlockDeviceMapping.1.Ebs.SnapshotId': fakes.ID_EC2_SNAPSHOT_1})
self.assertThat(resp, matchers.DictMatches( self.assertThat(resp, matchers.DictMatches(
{'http_status_code': 200, {'http_status_code': 200,
'imageId': fakes.ID_EC2_IMAGE_2})) 'imageId': fakes.ID_EC2_IMAGE_2}))
self.db_api.get_item_by_id.assert_called_once_with( self.db_api.get_item_by_id.assert_called_once_with(
mock.ANY, 'snap', fakes.ID_EC2_SNAPSHOT_1) mock.ANY, 'snap', fakes.ID_EC2_SNAPSHOT_1)
self.db_api.add_item.assert_called_once_with( self.db_api.add_item.assert_called_once_with(
mock.ANY, 'ami', {'os_id': fakes.ID_OS_IMAGE_2, mock.ANY, 'ami', {'os_id': fakes.ID_OS_IMAGE_2,
'is_public': False}) 'is_public': False})
self.glance.images.create.assert_called_once_with( self.assertEqual(1, self.glance.images.create.call_count)
is_public=False, size=0, name='fake_name', self.assertEqual((), self.glance.images.create.call_args[0])
properties={'root_device_name': fakes.ROOT_DEVICE_NAME_IMAGE_2, self.assertIn('properties', self.glance.images.create.call_args[1])
'block_device_mapping': json.dumps( self.assertIsInstance(
[{'device_name': fakes.ROOT_DEVICE_NAME_IMAGE_2, self.glance.images.create.call_args[1]['properties'],
'delete_on_termination': True, dict)
'snapshot_id': fakes.ID_OS_SNAPSHOT_1}])}) bdm = self.glance.images.create.call_args[1]['properties'].pop(
'block_device_mapping', None)
self.assertEqual(
{'is_public': False,
'size': 0,
'name': 'fake_name',
'properties': {
'root_device_name': fakes.ROOT_DEVICE_NAME_IMAGE_2}},
self.glance.images.create.call_args[1])
self.assertEqual([{'device_name': fakes.ROOT_DEVICE_NAME_IMAGE_2,
'delete_on_termination': True,
'snapshot_id': fakes.ID_OS_SNAPSHOT_1}],
json.loads(bdm))
def test_register_image_invalid_parameters(self): def test_register_image_invalid_parameters(self):
resp = self.execute('RegisterImage', {}) resp = self.execute('RegisterImage', {})
@ -264,11 +276,11 @@ class ImageTestCase(base.ApiTestCase):
'DescribeImages', 'imagesSet', 'DescribeImages', 'imagesSet',
[('architecture', 'x86_64'), [('architecture', 'x86_64'),
# TODO(ft): store a description in DB # TODO(ft): store a description in DB
# ('description', ''), # ('description', ''),
('image-id', fakes.ID_EC2_IMAGE_1), ('image-id', fakes.ID_EC2_IMAGE_1),
('image-type', 'machine'), ('image-type', 'machine'),
# TODO(ft): support filtering by a boolean value # TODO(ft): support filtering by a boolean value
# ('is-public', True), # ('is-public', True),
('kernel_id', fakes.ID_EC2_IMAGE_AKI_1,), ('kernel_id', fakes.ID_EC2_IMAGE_AKI_1,),
('name', 'fake_name'), ('name', 'fake_name'),
('owner-id', fakes.ID_OS_PROJECT), ('owner-id', fakes.ID_OS_PROJECT),
@ -330,13 +342,13 @@ class ImageTestCase(base.ApiTestCase):
do_check('blockDeviceMapping', do_check('blockDeviceMapping',
fakes.ID_EC2_IMAGE_1, fakes.ID_EC2_IMAGE_1,
{'blockDeviceMapping': {'blockDeviceMapping': (
fakes.EC2_IMAGE_1['blockDeviceMapping']}) fakes.EC2_IMAGE_1['blockDeviceMapping'])})
do_check('blockDeviceMapping', do_check('blockDeviceMapping',
fakes.ID_EC2_IMAGE_2, fakes.ID_EC2_IMAGE_2,
{'blockDeviceMapping': {'blockDeviceMapping': (
fakes.EC2_IMAGE_2['blockDeviceMapping']}) fakes.EC2_IMAGE_2['blockDeviceMapping'])})
@mock.patch.object(fakes.OSImage, 'update', autospec=True) @mock.patch.object(fakes.OSImage, 'update', autospec=True)
def test_modify_image_attributes(self, osimage_update): def test_modify_image_attributes(self, osimage_update):
@ -604,9 +616,11 @@ class S3TestCase(base.ApiTestCase):
'image_location': 'fake_bucket/fake_manifest'}) 'image_location': 'fake_bucket/fake_manifest'})
def test_s3_malicious_tarballs(self): def test_s3_malicious_tarballs(self):
self.assertRaises(exception.Invalid, self.assertRaises(
exception.Invalid,
image_api._s3_test_for_malicious_tarball, image_api._s3_test_for_malicious_tarball,
"/unused", os.path.join(os.path.dirname(__file__), 'abs.tar.gz')) "/unused", os.path.join(os.path.dirname(__file__), 'abs.tar.gz'))
self.assertRaises(exception.Invalid, self.assertRaises(
exception.Invalid,
image_api._s3_test_for_malicious_tarball, image_api._s3_test_for_malicious_tarball,
"/unused", os.path.join(os.path.dirname(__file__), 'rel.tar.gz')) "/unused", os.path.join(os.path.dirname(__file__), 'rel.tar.gz'))

View File

@ -17,7 +17,7 @@ import datetime
import itertools import itertools
import random import random
from glanceclient import exc as glance_exception from glanceclient.common import exceptions as glance_exception
import mock import mock
from novaclient import exceptions as nova_exception from novaclient import exceptions as nova_exception
from oslotest import base as test_base from oslotest import base as test_base

View File

@ -15,6 +15,8 @@
import testtools import testtools
from ec2api import exception
from ec2api.tests.unit import base
from ec2api.tests.unit import tools from ec2api.tests.unit import tools
@ -43,3 +45,10 @@ class TestToolsTestCase(testtools.TestCase):
res = tools.patch_dict(d1, d2, ('b', 'e')) res = tools.patch_dict(d1, d2, ('b', 'e'))
self.assertEqual({'a': 1, 'c': 33, 'd': 44}, res) self.assertEqual({'a': 1, 'c': 33, 'd': 44}, res)
self.assertEqual({'a': 1, 'b': 2, 'c': 3}, d1) self.assertEqual({'a': 1, 'b': 2, 'c': 3}, d1)
class TestBaseTestCase(base.ApiTestCase):
def test_validate_exception_format_is_enabled_for_tests(self):
self.assertRaises(KeyError, exception.InvalidVpcRange, fake='value')
self.assertRaises(TypeError, exception.InvalidId, {'id': 'value'})