From 3f93ff110bec754e5a3d955fef2daaed3510aaf4 Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Wed, 5 Oct 2016 11:09:24 -0400 Subject: [PATCH] Multi-Region Support This is an initial attempt at supporting multiple regions. It should handle the mechanics of deploying an instance/volume to a remote region. Additional changes may be required to allow the guest agent on the instance to connect back to the originating region. Co-Authored-By: Petr Malik Change-Id: I780de59dae5f90955139ab8393cf7d59ff3a21f6 --- .../db-backup-restore-response-json-http.txt | 2 +- .../samples/db-backup-restore-response.json | 1 + .../db-create-instance-response-json-http.txt | 2 +- .../samples/db-create-instance-response.json | 1 + ...tance-status-detail-response-json-http.txt | 2 +- .../db-instance-status-detail-response.json | 1 + ...es-index-pagination-response-json-http.txt | 2 +- ...b-instances-index-pagination-response.json | 2 + .../db-instances-index-response-json-http.txt | 2 +- .../samples/db-instances-index-response.json | 1 + ...et-instance-details-response-json-http.txt | 2 +- ...db-mgmt-get-instance-details-response.json | 1 + ...mgmt-instance-index-response-json-http.txt | 2 +- .../db-mgmt-instance-index-response.json | 1 + .../notes/multi-region-cd8da560bfe00de5.yaml | 3 + requirements.txt | 1 + tools/trove-pylint.config | 24 ++++ trove/cluster/models.py | 35 +++--- trove/cluster/service.py | 1 + trove/common/apischema.py | 5 +- trove/common/cfg.py | 18 ++- trove/common/glance_remote.py | 53 ++++++++ trove/common/models.py | 17 ++- trove/common/notification.py | 13 +- trove/common/remote.py | 20 +-- trove/common/single_tenant_remote.py | 11 +- .../cluster/experimental/cassandra/api.py | 3 +- .../cluster/experimental/galera_common/api.py | 3 +- .../cluster/experimental/mongodb/api.py | 12 +- .../cluster/experimental/redis/api.py | 4 +- .../cluster/experimental/vertica/api.py | 6 +- trove/common/trove_remote.py | 56 +++++++++ .../migrate_repo/versions/039_region.py | 35 ++++++ trove/extensions/mgmt/instances/models.py | 6 +- trove/extensions/mgmt/instances/service.py | 22 ++-- trove/extensions/mgmt/volume/models.py | 4 +- trove/extensions/mgmt/volume/service.py | 4 +- trove/extensions/security_group/models.py | 53 ++++---- trove/extensions/security_group/service.py | 5 +- trove/instance/models.py | 116 ++++++++++++++---- trove/instance/service.py | 6 +- trove/instance/views.py | 1 + trove/network/neutron.py | 4 +- trove/network/nova.py | 4 +- trove/taskmanager/manager.py | 15 +-- trove/taskmanager/models.py | 13 +- trove/tests/api/instances.py | 10 +- trove/tests/api/mgmt/instances.py | 3 + trove/tests/fakes/guestagent.py | 3 + trove/tests/fakes/nova.py | 6 +- .../cluster/test_cluster_controller.py | 1 + .../cluster/test_cluster_pxc_controller.py | 1 + .../cluster/test_cluster_redis_controller.py | 3 + .../test_cluster_vertica_controller.py | 1 + trove/tests/unittests/cluster/test_models.py | 10 +- trove/tests/unittests/common/test_remote.py | 42 +++++++ .../unittests/network/test_neutron_driver.py | 30 +++-- .../secgroups/test_security_group.py | 26 ++-- 58 files changed, 569 insertions(+), 162 deletions(-) create mode 100644 releasenotes/notes/multi-region-cd8da560bfe00de5.yaml create mode 100644 trove/common/glance_remote.py create mode 100644 trove/common/trove_remote.py create mode 100644 trove/db/sqlalchemy/migrate_repo/versions/039_region.py diff --git a/api-ref/source/samples/db-backup-restore-response-json-http.txt b/api-ref/source/samples/db-backup-restore-response-json-http.txt index 2ce0d7537f..ce9bfb084d 100644 --- a/api-ref/source/samples/db-backup-restore-response-json-http.txt +++ b/api-ref/source/samples/db-backup-restore-response-json-http.txt @@ -1,5 +1,5 @@ HTTP/1.1 200 OK Content-Type: application/json -Content-Length: 694 +Content-Length: 717 Date: Mon, 18 Mar 2013 19:09:17 GMT diff --git a/api-ref/source/samples/db-backup-restore-response.json b/api-ref/source/samples/db-backup-restore-response.json index 005899d157..cafd6197ec 100644 --- a/api-ref/source/samples/db-backup-restore-response.json +++ b/api-ref/source/samples/db-backup-restore-response.json @@ -31,6 +31,7 @@ } ], "name": "backup_instance", + "region": "RegionOne", "status": "BUILD", "updated": "2014-10-30T12:30:00", "volume": { diff --git a/api-ref/source/samples/db-create-instance-response-json-http.txt b/api-ref/source/samples/db-create-instance-response-json-http.txt index 2128454105..646d02a620 100644 --- a/api-ref/source/samples/db-create-instance-response-json-http.txt +++ b/api-ref/source/samples/db-create-instance-response-json-http.txt @@ -1,5 +1,5 @@ HTTP/1.1 200 OK Content-Type: application/json -Content-Length: 697 +Content-Length: 720 Date: Mon, 18 Mar 2013 19:09:17 GMT diff --git a/api-ref/source/samples/db-create-instance-response.json b/api-ref/source/samples/db-create-instance-response.json index a1bf8e9a4e..47e75bfe52 100644 --- a/api-ref/source/samples/db-create-instance-response.json +++ b/api-ref/source/samples/db-create-instance-response.json @@ -31,6 +31,7 @@ } ], "name": "json_rack_instance", + "region": "RegionOne", "status": "BUILD", "updated": "2014-10-30T12:30:00", "volume": { diff --git a/api-ref/source/samples/db-instance-status-detail-response-json-http.txt b/api-ref/source/samples/db-instance-status-detail-response-json-http.txt index 0825e83555..ab457b307a 100644 --- a/api-ref/source/samples/db-instance-status-detail-response-json-http.txt +++ b/api-ref/source/samples/db-instance-status-detail-response-json-http.txt @@ -1,5 +1,5 @@ HTTP/1.1 200 OK Content-Type: application/json -Content-Length: 712 +Content-Length: 735 Date: Mon, 18 Mar 2013 19:09:17 GMT diff --git a/api-ref/source/samples/db-instance-status-detail-response.json b/api-ref/source/samples/db-instance-status-detail-response.json index fa96976acb..eacb3fc464 100644 --- a/api-ref/source/samples/db-instance-status-detail-response.json +++ b/api-ref/source/samples/db-instance-status-detail-response.json @@ -31,6 +31,7 @@ } ], "name": "json_rack_instance", + "region": "RegionOne", "status": "ACTIVE", "updated": "2014-10-30T12:30:00", "volume": { diff --git a/api-ref/source/samples/db-instances-index-pagination-response-json-http.txt b/api-ref/source/samples/db-instances-index-pagination-response-json-http.txt index 0ab8faf14d..b1c87ce65d 100644 --- a/api-ref/source/samples/db-instances-index-pagination-response-json-http.txt +++ b/api-ref/source/samples/db-instances-index-pagination-response-json-http.txt @@ -1,5 +1,5 @@ HTTP/1.1 200 OK Content-Type: application/json -Content-Length: 1251 +Content-Length: 1297 Date: Mon, 18 Mar 2013 19:09:17 GMT diff --git a/api-ref/source/samples/db-instances-index-pagination-response.json b/api-ref/source/samples/db-instances-index-pagination-response.json index 9dcd84800c..8556935098 100644 --- a/api-ref/source/samples/db-instances-index-pagination-response.json +++ b/api-ref/source/samples/db-instances-index-pagination-response.json @@ -31,6 +31,7 @@ } ], "name": "The Third Instance", + "region": "RegionOne", "status": "ACTIVE", "volume": { "size": 2 @@ -67,6 +68,7 @@ } ], "name": "json_rack_instance", + "region": "RegionOne", "status": "ACTIVE", "volume": { "size": 2 diff --git a/api-ref/source/samples/db-instances-index-response-json-http.txt b/api-ref/source/samples/db-instances-index-response-json-http.txt index f7a85bd0e3..dd2cde6792 100644 --- a/api-ref/source/samples/db-instances-index-response-json-http.txt +++ b/api-ref/source/samples/db-instances-index-response-json-http.txt @@ -1,5 +1,5 @@ HTTP/1.1 200 OK Content-Type: application/json -Content-Length: 633 +Content-Length: 656 Date: Mon, 18 Mar 2013 19:09:17 GMT diff --git a/api-ref/source/samples/db-instances-index-response.json b/api-ref/source/samples/db-instances-index-response.json index c644e7c849..341308eac5 100644 --- a/api-ref/source/samples/db-instances-index-response.json +++ b/api-ref/source/samples/db-instances-index-response.json @@ -31,6 +31,7 @@ } ], "name": "json_rack_instance", + "region": "RegionOne", "status": "ACTIVE", "volume": { "size": 2 diff --git a/api-ref/source/samples/db-mgmt-get-instance-details-response-json-http.txt b/api-ref/source/samples/db-mgmt-get-instance-details-response-json-http.txt index 97dd151e68..4aa235cb2f 100644 --- a/api-ref/source/samples/db-mgmt-get-instance-details-response-json-http.txt +++ b/api-ref/source/samples/db-mgmt-get-instance-details-response-json-http.txt @@ -1,5 +1,5 @@ HTTP/1.1 200 OK Content-Type: application/json -Content-Length: 1533 +Content-Length: 1556 Date: Mon, 18 Mar 2013 19:09:17 GMT diff --git a/api-ref/source/samples/db-mgmt-get-instance-details-response.json b/api-ref/source/samples/db-mgmt-get-instance-details-response.json index c58aeb20e3..2d08d35e68 100644 --- a/api-ref/source/samples/db-mgmt-get-instance-details-response.json +++ b/api-ref/source/samples/db-mgmt-get-instance-details-response.json @@ -36,6 +36,7 @@ } ], "name": "json_rack_instance", + "region": "RegionOne", "root_enabled": "2014-10-30T12:30:00", "root_enabled_by": "3000", "server": { diff --git a/api-ref/source/samples/db-mgmt-instance-index-response-json-http.txt b/api-ref/source/samples/db-mgmt-instance-index-response-json-http.txt index 6eb4f3ca8c..9acc82364c 100644 --- a/api-ref/source/samples/db-mgmt-instance-index-response-json-http.txt +++ b/api-ref/source/samples/db-mgmt-instance-index-response-json-http.txt @@ -1,5 +1,5 @@ HTTP/1.1 200 OK Content-Type: application/json -Content-Length: 1082 +Content-Length: 1105 Date: Mon, 18 Mar 2013 19:09:17 GMT diff --git a/api-ref/source/samples/db-mgmt-instance-index-response.json b/api-ref/source/samples/db-mgmt-instance-index-response.json index 9c6eb89f3a..a29a2fcebb 100644 --- a/api-ref/source/samples/db-mgmt-instance-index-response.json +++ b/api-ref/source/samples/db-mgmt-instance-index-response.json @@ -34,6 +34,7 @@ } ], "name": "json_rack_instance", + "region": "RegionOne", "server": { "deleted": false, "deleted_at": null, diff --git a/releasenotes/notes/multi-region-cd8da560bfe00de5.yaml b/releasenotes/notes/multi-region-cd8da560bfe00de5.yaml new file mode 100644 index 0000000000..49a9dbf54d --- /dev/null +++ b/releasenotes/notes/multi-region-cd8da560bfe00de5.yaml @@ -0,0 +1,3 @@ +features: + - Adds a region property to the instance model and table. This is the + first step in multi-region support. diff --git a/requirements.txt b/requirements.txt index d16cd8251c..f0d2fb482f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ python-keystoneclient>=3.6.0 # Apache-2.0 python-swiftclient>=2.2.0 # Apache-2.0 python-designateclient>=1.5.0 # Apache-2.0 python-neutronclient>=5.1.0 # Apache-2.0 +python-glanceclient>=2.5.0 # Apache-2.0 iso8601>=0.1.11 # MIT jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT Jinja2>=2.8 # BSD License (3 clause) diff --git a/tools/trove-pylint.config b/tools/trove-pylint.config index ba9a6f6b17..ea041c5d66 100644 --- a/tools/trove-pylint.config +++ b/tools/trove-pylint.config @@ -693,6 +693,30 @@ "Instance of 'Table' has no 'create_column' member", "upgrade" ], + [ + "trove/db/sqlalchemy/migrate_repo/versions/039_region.py", + "E1101", + "Instance of 'Table' has no 'create_column' member", + "upgrade" + ], + [ + "trove/db/sqlalchemy/migrate_repo/versions/039_region.py", + "E1120", + "No value for argument 'dml' in method call", + "upgrade" + ], + [ + "trove/db/sqlalchemy/migrate_repo/versions/039_region.py", + "no-member", + "Instance of 'Table' has no 'create_column' member", + "upgrade" + ], + [ + "trove/db/sqlalchemy/migrate_repo/versions/039_region.py", + "no-value-for-parameter", + "No value for argument 'dml' in method call", + "upgrade" + ], [ "trove/db/sqlalchemy/migration.py", "E0611", diff --git a/trove/cluster/models.py b/trove/cluster/models.py index 26a4e8d8f4..f6e4a9cd0c 100644 --- a/trove/cluster/models.py +++ b/trove/cluster/models.py @@ -314,10 +314,10 @@ class Cluster(object): raise exception.BadRequest(_("Action %s not supported") % action) def grow(self, instances): - raise exception.BadRequest(_("Action 'grow' not supported")) + raise exception.BadRequest(_("Action 'grow' not supported")) def shrink(self, instance_ids): - raise exception.BadRequest(_("Action 'shrink' not supported")) + raise exception.BadRequest(_("Action 'shrink' not supported")) @staticmethod def load_instance(context, cluster_id, instance_id): @@ -341,23 +341,26 @@ def is_cluster_deleting(context, cluster_id): def validate_instance_flavors(context, instances, volume_enabled, ephemeral_enabled): - """Load and validate flavors for given instance definitions.""" - flavors = dict() - nova_client = remote.create_nova_client(context) + """Validate flavors for given instance definitions.""" + nova_cli_cache = dict() for instance in instances: + region_name = instance.get('region_name') flavor_id = instance['flavor_id'] - if flavor_id not in flavors: - try: - flavor = nova_client.flavors.get(flavor_id) - if (not volume_enabled and - (ephemeral_enabled and flavor.ephemeral == 0)): - raise exception.LocalStorageNotSpecified( - flavor=flavor_id) - flavors[flavor_id] = flavor - except nova_exceptions.NotFound: - raise exception.FlavorNotFound(uuid=flavor_id) + try: + if region_name in nova_cli_cache: + nova_client = nova_cli_cache[region_name] + else: + nova_client = remote.create_nova_client( + context, region_name) + nova_cli_cache[region_name] = nova_client - return flavors + flavor = nova_client.flavors.get(flavor_id) + if (not volume_enabled and + (ephemeral_enabled and flavor.ephemeral == 0)): + raise exception.LocalStorageNotSpecified( + flavor=flavor_id) + except nova_exceptions.NotFound: + raise exception.FlavorNotFound(uuid=flavor_id) def get_required_volume_size(instances, volume_enabled): diff --git a/trove/cluster/service.py b/trove/cluster/service.py index e1fb5ddc94..97a91b9acd 100644 --- a/trove/cluster/service.py +++ b/trove/cluster/service.py @@ -174,6 +174,7 @@ class ClusterController(wsgi.Controller): "volume_type": volume_type, "nics": nics, "availability_zone": availability_zone, + 'region_name': node.get('region_name'), "modules": modules}) locality = body['cluster'].get('locality') diff --git a/trove/common/apischema.py b/trove/common/apischema.py index 484e81762d..cecfc38a75 100644 --- a/trove/common/apischema.py +++ b/trove/common/apischema.py @@ -250,6 +250,7 @@ cluster = { "nics": nics, "availability_zone": non_empty_string, "modules": module_list, + "region_name": non_empty_string } } }, @@ -287,7 +288,8 @@ cluster = { "availability_zone": non_empty_string, "modules": module_list, "related_to": non_empty_string, - "type": non_empty_string + "type": non_empty_string, + "region_name": non_empty_string } } } @@ -349,6 +351,7 @@ instance = { }, "nics": nics, "modules": module_list, + "region_name": non_empty_string, "locality": non_empty_string } } diff --git a/trove/common/cfg.py b/trove/common/cfg.py index 908d0a59ad..332af5d171 100644 --- a/trove/common/cfg.py +++ b/trove/common/cfg.py @@ -92,8 +92,18 @@ common_opts = [ help='Service type to use when searching catalog.'), cfg.StrOpt('swift_endpoint_type', default='publicURL', help='Service endpoint type to use when searching catalog.'), + cfg.URIOpt('glance_url', help='URL ending in ``AUTH_``.'), + cfg.StrOpt('glance_service_type', default='image', + help='Service type to use when searching catalog.'), + cfg.StrOpt('glance_endpoint_type', default='publicURL', + help='Service endpoint type to use when searching catalog.'), cfg.URIOpt('trove_auth_url', default='http://0.0.0.0:5000/v2.0', help='Trove authentication URL.'), + cfg.StrOpt('trove_url', help='URL without the tenant segment.'), + cfg.StrOpt('trove_service_type', default='database', + help='Service type to use when searching catalog.'), + cfg.StrOpt('trove_endpoint_type', default='publicURL', + help='Service endpoint type to use when searching catalog.'), cfg.IPOpt('host', default='0.0.0.0', help='Host to listen for RPC messages.'), cfg.IntOpt('report_interval', default=30, @@ -328,11 +338,17 @@ common_opts = [ cfg.StrOpt('remote_swift_client', default='trove.common.remote.swift_client', help='Client to send Swift calls to.'), + cfg.StrOpt('remote_trove_client', + default='trove.common.trove_remote.trove_client', + help='Client to send Trove calls to.'), + cfg.StrOpt('remote_glance_client', + default='trove.common.glance_remote.glance_client', + help='Client to send Glance calls to.'), cfg.StrOpt('exists_notification_transformer', help='Transformer for exists notifications.'), cfg.IntOpt('exists_notification_interval', default=3600, help='Seconds to wait between pushing events.'), - cfg.IntOpt('quota_notification_interval', default=3600, + cfg.IntOpt('quota_notification_interval', help='Seconds to wait between pushing events.'), cfg.DictOpt('notification_service_id', default={'mysql': '2f3ff068-2bfb-4f70-9a9d-a6bb65bc084b', diff --git a/trove/common/glance_remote.py b/trove/common/glance_remote.py new file mode 100644 index 0000000000..0bcde97b1a --- /dev/null +++ b/trove/common/glance_remote.py @@ -0,0 +1,53 @@ +# Copyright 2016 Tesora Inc. +# All Rights Reserved. +# +# 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 keystoneauth1.identity import v3 +from keystoneauth1 import session as ka_session + +from oslo_utils.importutils import import_class + +from trove.common import cfg +from trove.common.remote import get_endpoint +from trove.common.remote import normalize_url + +from glanceclient import Client + +CONF = cfg.CONF + + +def glance_client(context, region_name=None): + + # We should allow glance to get the endpoint from the service + # catalog, but to do so we would need to be able to specify + # the endpoint_filter on the API calls, but glance + # doesn't currently allow that. As a result, we must + # specify the endpoint explicitly. + if CONF.glance_url: + endpoint_url = '%(url)s%(tenant)s' % { + 'url': normalize_url(CONF.glance_url), + 'tenant': context.tenant} + else: + endpoint_url = get_endpoint( + context.service_catalog, service_type=CONF.glance_service_type, + endpoint_region=region_name or CONF.os_region_name, + endpoint_type=CONF.glance_endpoint_type) + + auth = v3.Token(CONF.trove_auth_url, context.auth_token) + session = ka_session.Session(auth=auth) + + return Client('2', endpoint=endpoint_url, session=session) + + +create_glance_client = import_class(CONF.remote_glance_client) diff --git a/trove/common/models.py b/trove/common/models.py index 84fbcc9579..78e5e9a2c6 100644 --- a/trove/common/models.py +++ b/trove/common/models.py @@ -103,21 +103,28 @@ class NetworkRemoteModelBase(RemoteModelBase): network_driver = None @classmethod - def get_driver(cls, context): + def get_driver(cls, context, region_name): if not cls.network_driver: cls.network_driver = import_class(CONF.network_driver) - return cls.network_driver(context) + return cls.network_driver(context, region_name) class NovaRemoteModelBase(RemoteModelBase): @classmethod - def get_client(cls, context): - return remote.create_nova_client(context) + def get_client(cls, context, region_name): + return remote.create_nova_client(context, region_name) class SwiftRemoteModelBase(RemoteModelBase): + @classmethod + def get_client(cls, context, region_name): + return remote.create_swift_client(context, region_name) + + +class CinderRemoteModelBase(RemoteModelBase): + @classmethod def get_client(cls, context): - return remote.create_swift_client(context) + return remote.create_cinder_client(context) diff --git a/trove/common/notification.py b/trove/common/notification.py index be7c96bfac..aa53593920 100644 --- a/trove/common/notification.py +++ b/trove/common/notification.py @@ -436,7 +436,7 @@ class DBaaSInstanceCreate(DBaaSAPINotification): def required_start_traits(self): return ['name', 'flavor_id', 'datastore', 'datastore_version', - 'image_id', 'availability_zone'] + 'image_id', 'availability_zone', 'region_name'] def optional_start_traits(self): return ['databases', 'users', 'volume_size', 'restore_point', @@ -789,3 +789,14 @@ class DBaaSInstanceUpgrade(DBaaSAPINotification): @abc.abstractmethod def required_start_traits(self): return ['instance_id', 'datastore_version_id'] + + +class DBaaSInstanceMigrate(DBaaSAPINotification): + + @abc.abstractmethod + def event_type(self): + return 'migrate' + + @abc.abstractmethod + def required_start_traits(self): + return ['host'] diff --git a/trove/common/remote.py b/trove/common/remote.py index 70867bbab7..76b9335afa 100644 --- a/trove/common/remote.py +++ b/trove/common/remote.py @@ -87,7 +87,7 @@ def guest_client(context, id, manager=None): return clazz(context, id) -def nova_client(context): +def nova_client(context, region_name=None): if CONF.nova_compute_url: url = '%(nova_url)s%(tenant)s' % { 'nova_url': normalize_url(CONF.nova_compute_url), @@ -95,7 +95,7 @@ def nova_client(context): else: url = get_endpoint(context.service_catalog, service_type=CONF.nova_compute_service_type, - endpoint_region=CONF.os_region_name, + endpoint_region=region_name or CONF.os_region_name, endpoint_type=CONF.nova_compute_endpoint_type) client = Client(CONF.nova_client_version, context.user, context.auth_token, @@ -116,7 +116,7 @@ def create_admin_nova_client(context): return client -def cinder_client(context): +def cinder_client(context, region_name=None): if CONF.cinder_url: url = '%(cinder_url)s%(tenant)s' % { 'cinder_url': normalize_url(CONF.cinder_url), @@ -124,7 +124,7 @@ def cinder_client(context): else: url = get_endpoint(context.service_catalog, service_type=CONF.cinder_service_type, - endpoint_region=CONF.os_region_name, + endpoint_region=region_name or CONF.os_region_name, endpoint_type=CONF.cinder_endpoint_type) client = CinderClient.Client(context.user, context.auth_token, @@ -135,7 +135,7 @@ def cinder_client(context): return client -def heat_client(context): +def heat_client(context, region_name=None): if CONF.heat_url: url = '%(heat_url)s%(tenant)s' % { 'heat_url': normalize_url(CONF.heat_url), @@ -143,7 +143,7 @@ def heat_client(context): else: url = get_endpoint(context.service_catalog, service_type=CONF.heat_service_type, - endpoint_region=CONF.os_region_name, + endpoint_region=region_name or CONF.os_region_name, endpoint_type=CONF.heat_endpoint_type) client = HeatClient.Client(token=context.auth_token, @@ -152,7 +152,7 @@ def heat_client(context): return client -def swift_client(context): +def swift_client(context, region_name=None): if CONF.swift_url: # swift_url has a different format so doesn't need to be normalized url = '%(swift_url)s%(tenant)s' % {'swift_url': CONF.swift_url, @@ -160,7 +160,7 @@ def swift_client(context): else: url = get_endpoint(context.service_catalog, service_type=CONF.swift_service_type, - endpoint_region=CONF.os_region_name, + endpoint_region=region_name or CONF.os_region_name, endpoint_type=CONF.swift_endpoint_type) client = Connection(preauthurl=url, @@ -170,7 +170,7 @@ def swift_client(context): return client -def neutron_client(context): +def neutron_client(context, region_name=None): from neutronclient.v2_0 import client as NeutronClient if CONF.neutron_url: # neutron endpoint url / publicURL does not include tenant segment @@ -178,7 +178,7 @@ def neutron_client(context): else: url = get_endpoint(context.service_catalog, service_type=CONF.neutron_service_type, - endpoint_region=CONF.os_region_name, + endpoint_region=region_name or CONF.os_region_name, endpoint_type=CONF.neutron_endpoint_type) client = NeutronClient.Client(token=context.auth_token, diff --git a/trove/common/single_tenant_remote.py b/trove/common/single_tenant_remote.py index ede2930604..87f937564e 100644 --- a/trove/common/single_tenant_remote.py +++ b/trove/common/single_tenant_remote.py @@ -52,7 +52,7 @@ remote_neutron_client = \ PROXY_AUTH_URL = CONF.trove_auth_url -def nova_client_trove_admin(context=None): +def nova_client_trove_admin(context, region_name=None, compute_url=None): """ Returns a nova client object with the trove admin credentials :param context: original context from user request @@ -60,16 +60,19 @@ def nova_client_trove_admin(context=None): :return novaclient: novaclient with trove admin credentials :rtype: novaclient.v1_1.client.Client """ + + compute_url = compute_url or CONF.nova_compute_url + client = NovaClient(CONF.nova_proxy_admin_user, CONF.nova_proxy_admin_pass, CONF.nova_proxy_admin_tenant_name, auth_url=PROXY_AUTH_URL, service_type=CONF.nova_compute_service_type, - region_name=CONF.os_region_name) + region_name=region_name or CONF.os_region_name) - if CONF.nova_compute_url and CONF.nova_proxy_admin_tenant_id: + if compute_url and CONF.nova_proxy_admin_tenant_id: client.client.management_url = "%s/%s/" % ( - normalize_url(CONF.nova_compute_url), + normalize_url(compute_url), CONF.nova_proxy_admin_tenant_id) return client diff --git a/trove/common/strategies/cluster/experimental/cassandra/api.py b/trove/common/strategies/cluster/experimental/cassandra/api.py index 3a7cdfb861..41c3f229c5 100644 --- a/trove/common/strategies/cluster/experimental/cassandra/api.py +++ b/trove/common/strategies/cluster/experimental/cassandra/api.py @@ -155,8 +155,9 @@ class CassandraCluster(models.Cluster): availability_zone=instance_az, configuration_id=None, cluster_config=member_config, + modules=instance.get('modules'), locality=locality, - modules=instance.get('modules')) + region_name=instance.get('region_name')) new_instances.append(new_instance) diff --git a/trove/common/strategies/cluster/experimental/galera_common/api.py b/trove/common/strategies/cluster/experimental/galera_common/api.py index 1c0f6b719b..4e4c5bcea4 100644 --- a/trove/common/strategies/cluster/experimental/galera_common/api.py +++ b/trove/common/strategies/cluster/experimental/galera_common/api.py @@ -120,8 +120,9 @@ class GaleraCommonCluster(cluster_models.Cluster): nics=instance.get('nics', None), configuration_id=None, cluster_config=member_config, + modules=instance.get('modules'), locality=locality, - modules=instance.get('modules') + region_name=instance.get('region_name') ) for instance in instances] diff --git a/trove/common/strategies/cluster/experimental/mongodb/api.py b/trove/common/strategies/cluster/experimental/mongodb/api.py index a1ffba4773..4ea138f05b 100644 --- a/trove/common/strategies/cluster/experimental/mongodb/api.py +++ b/trove/common/strategies/cluster/experimental/mongodb/api.py @@ -93,6 +93,9 @@ class MongoDbCluster(models.Cluster): azs = [instance.get('availability_zone', None) for instance in instances] + regions = [instance.get('region_name', None) + for instance in instances] + db_info = models.DBCluster.create( name=name, tenant_id=context.tenant, datastore_version_id=datastore_version.id, @@ -129,8 +132,9 @@ class MongoDbCluster(models.Cluster): nics=nics[i], configuration_id=None, cluster_config=member_config, + modules=instances[i].get('modules'), locality=locality, - modules=instances[i].get('modules')) + region_name=regions[i]) for i in range(1, num_configsvr + 1): instance_name = "%s-%s-%s" % (name, "configsvr", str(i)) @@ -144,7 +148,8 @@ class MongoDbCluster(models.Cluster): nics=None, configuration_id=None, cluster_config=configsvr_config, - locality=locality) + locality=locality, + region_name=regions[i]) for i in range(1, num_mongos + 1): instance_name = "%s-%s-%s" % (name, "mongos", str(i)) @@ -158,7 +163,8 @@ class MongoDbCluster(models.Cluster): nics=None, configuration_id=None, cluster_config=mongos_config, - locality=locality) + locality=locality, + region_name=regions[i]) task_api.load(context, datastore_version.manager).create_cluster( db_info.id) diff --git a/trove/common/strategies/cluster/experimental/redis/api.py b/trove/common/strategies/cluster/experimental/redis/api.py index 3799f932f8..28186c3f75 100644 --- a/trove/common/strategies/cluster/experimental/redis/api.py +++ b/trove/common/strategies/cluster/experimental/redis/api.py @@ -88,8 +88,10 @@ class RedisCluster(models.Cluster): cluster_config={ "id": db_info.id, "instance_type": "member"}, + modules=instance.get('modules'), locality=locality, - modules=instance.get('modules') + region_name=instance.get( + 'region_name') ) for instance in instances] diff --git a/trove/common/strategies/cluster/experimental/vertica/api.py b/trove/common/strategies/cluster/experimental/vertica/api.py index 67cce94f5b..a5f7de237f 100644 --- a/trove/common/strategies/cluster/experimental/vertica/api.py +++ b/trove/common/strategies/cluster/experimental/vertica/api.py @@ -103,6 +103,9 @@ class VerticaCluster(models.Cluster): azs = [instance.get('availability_zone', None) for instance in instances] + regions = [instance.get('region_name', None) + for instance in instances] + # Creating member instances minstances = [] for i in range(0, num_instances): @@ -119,7 +122,8 @@ class VerticaCluster(models.Cluster): datastore_version, volume_size, None, nics=nics[i], availability_zone=azs[i], configuration_id=None, cluster_config=member_config, - locality=locality, modules=instances[i].get('modules')) + modules=instances[i].get('modules'), locality=locality, + region_name=regions[i]) ) return minstances diff --git a/trove/common/trove_remote.py b/trove/common/trove_remote.py new file mode 100644 index 0000000000..01b4141e7f --- /dev/null +++ b/trove/common/trove_remote.py @@ -0,0 +1,56 @@ +# Copyright 2016 Tesora Inc. +# All Rights Reserved. +# +# 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 oslo_utils.importutils import import_class + +from trove.common import cfg +from trove.common.remote import get_endpoint +from trove.common.remote import normalize_url + +from troveclient.v1 import client as TroveClient + +CONF = cfg.CONF + +PROXY_AUTH_URL = CONF.trove_auth_url + + +""" +NOTE(mwj, Apr 2016): +This module is separated from remote.py because remote.py is used +on the Trove guest, but the trove client is not installed on the guest, +so the imports here would fail. +""" + + +def trove_client(context, region_name=None): + if CONF.trove_url: + url = '%(url)s%(tenant)s' % { + 'url': normalize_url(CONF.trove_url), + 'tenant': context.tenant} + else: + url = get_endpoint(context.service_catalog, + service_type=CONF.trove_service_type, + endpoint_region=region_name or CONF.os_region_name, + endpoint_type=CONF.trove_endpoint_type) + + client = TroveClient.Client(context.user, context.auth_token, + project_id=context.tenant, + auth_url=PROXY_AUTH_URL) + client.client.auth_token = context.auth_token + client.client.management_url = url + return client + + +create_trove_client = import_class(CONF.remote_trove_client) diff --git a/trove/db/sqlalchemy/migrate_repo/versions/039_region.py b/trove/db/sqlalchemy/migrate_repo/versions/039_region.py new file mode 100644 index 0000000000..eda38f5681 --- /dev/null +++ b/trove/db/sqlalchemy/migrate_repo/versions/039_region.py @@ -0,0 +1,35 @@ +# Copyright 2016 Tesora Inc. +# All Rights Reserved. +# +# 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 oslo_log import log as logging +from sqlalchemy.schema import Column +from sqlalchemy.schema import MetaData + +from trove.common import cfg +from trove.db.sqlalchemy.migrate_repo.schema import String +from trove.db.sqlalchemy.migrate_repo.schema import Table + + +CONF = cfg.CONF +logger = logging.getLogger('trove.db.sqlalchemy.migrate_repo.schema') + +meta = MetaData() + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + instances = Table('instances', meta, autoload=True) + instances.create_column(Column('region_id', String(255))) + instances.update().values(region_id=CONF.os_region_name).execute() diff --git a/trove/extensions/mgmt/instances/models.py b/trove/extensions/mgmt/instances/models.py index c3912c1582..3c0a5bbc74 100644 --- a/trove/extensions/mgmt/instances/models.py +++ b/trove/extensions/mgmt/instances/models.py @@ -33,7 +33,7 @@ CONF = cfg.CONF def load_mgmt_instances(context, deleted=None, client=None, include_clustered=None): if not client: - client = remote.create_nova_client(context) + client = remote.create_nova_client(context, CONF.os_region_name) try: mgmt_servers = client.rdservers.list() except AttributeError: @@ -56,7 +56,7 @@ def load_mgmt_instance(cls, context, id, include_deleted): try: instance = load_instance(cls, context, id, needs_server=True, include_deleted=include_deleted) - client = remote.create_nova_client(context) + client = remote.create_nova_client(context, CONF.os_region_name) try: server = client.rdservers.get(instance.server_id) except AttributeError: @@ -169,7 +169,7 @@ def _load_servers(instances, find_server): server = find_server(db.id, db.compute_instance_id) instance.server = server except Exception as ex: - LOG.error(ex) + LOG.exception(ex) return instances diff --git a/trove/extensions/mgmt/instances/service.py b/trove/extensions/mgmt/instances/service.py index 280cb20d58..a1bd8c076d 100644 --- a/trove/extensions/mgmt/instances/service.py +++ b/trove/extensions/mgmt/instances/service.py @@ -22,6 +22,8 @@ import trove.common.apischema as apischema from trove.common.auth import admin_context from trove.common import exception from trove.common.i18n import _ +from trove.common import notification +from trove.common.notification import StartNotification from trove.common import wsgi from trove.extensions.mgmt.instances import models from trove.extensions.mgmt.instances import views @@ -63,7 +65,7 @@ class MgmtInstanceController(InstanceController): instances = models.load_mgmt_instances( context, deleted=deleted, include_clustered=include_clustered) except nova_exceptions.ClientException as e: - LOG.error(e) + LOG.exception(e) return wsgi.Result(str(e), 403) view_cls = views.MgmtInstancesView @@ -118,28 +120,32 @@ class MgmtInstanceController(InstanceController): raise exception.BadRequest(msg) if selected_action: - return selected_action(context, instance, body) + return selected_action(context, instance, req, body) else: raise exception.BadRequest(_("Invalid request body.")) - def _action_stop(self, context, instance, body): + def _action_stop(self, context, instance, req, body): LOG.debug("Stopping MySQL on instance %s." % instance.id) instance.stop_db() return wsgi.Result(None, 202) - def _action_reboot(self, context, instance, body): + def _action_reboot(self, context, instance, req, body): LOG.debug("Rebooting instance %s." % instance.id) instance.reboot() return wsgi.Result(None, 202) - def _action_migrate(self, context, instance, body): + def _action_migrate(self, context, instance, req, body): LOG.debug("Migrating instance %s." % instance.id) LOG.debug("body['migrate']= %s" % body['migrate']) host = body['migrate'].get('host', None) - instance.migrate(host) + + context.notification = notification.DBaaSInstanceMigrate(context, + request=req) + with StartNotification(context, host=host): + instance.migrate(host) return wsgi.Result(None, 202) - def _action_reset_task_status(self, context, instance, body): + def _action_reset_task_status(self, context, instance, req, body): LOG.debug("Setting Task-Status to NONE on instance %s." % instance.id) instance.reset_task_status() @@ -163,7 +169,7 @@ class MgmtInstanceController(InstanceController): try: instance_models.Instance.load(context=context, id=id) except exception.TroveError as e: - LOG.error(e) + LOG.exception(e) return wsgi.Result(str(e), 404) rhv = views.RootHistoryView(id) reh = mysql_models.RootHistory.load(context=context, instance_id=id) diff --git a/trove/extensions/mgmt/volume/models.py b/trove/extensions/mgmt/volume/models.py index 0f8b4eb8c5..a7abc90af4 100644 --- a/trove/extensions/mgmt/volume/models.py +++ b/trove/extensions/mgmt/volume/models.py @@ -41,8 +41,8 @@ class StorageDevice(object): class StorageDevices(object): @staticmethod - def load(context): - client = create_cinder_client(context) + def load(context, region_name): + client = create_cinder_client(context, region_name) rdstorages = client.rdstorage.list() for rdstorage in rdstorages: LOG.debug("rdstorage=" + str(rdstorage)) diff --git a/trove/extensions/mgmt/volume/service.py b/trove/extensions/mgmt/volume/service.py index c6b1b9924d..3f9b4733cc 100644 --- a/trove/extensions/mgmt/volume/service.py +++ b/trove/extensions/mgmt/volume/service.py @@ -17,11 +17,13 @@ from oslo_log import log as logging from trove.common.auth import admin_context +from trove.common import cfg from trove.common.i18n import _ from trove.common import wsgi from trove.extensions.mgmt.volume import models from trove.extensions.mgmt.volume import views +CONF = cfg.CONF LOG = logging.getLogger(__name__) @@ -34,5 +36,5 @@ class StorageController(wsgi.Controller): LOG.info(_("req : '%s'\n\n") % req) LOG.info(_("Indexing storage info for tenant '%s'") % tenant_id) context = req.environ[wsgi.CONTEXT_KEY] - storages = models.StorageDevices.load(context) + storages = models.StorageDevices.load(context, CONF.os_region_name) return wsgi.Result(views.StoragesView(storages).data(), 200) diff --git a/trove/extensions/security_group/models.py b/trove/extensions/security_group/models.py index 1d311319a1..892e8f65e6 100644 --- a/trove/extensions/security_group/models.py +++ b/trove/extensions/security_group/models.py @@ -49,11 +49,10 @@ class SecurityGroup(DatabaseModelBase): .get_instance_id_by_security_group_id(self.id) @classmethod - def create_sec_group(cls, name, description, context): + def create_sec_group(cls, name, description, context, region_name): try: - remote_sec_group = RemoteSecurityGroup.create(name, - description, - context) + remote_sec_group = RemoteSecurityGroup.create( + name, description, context, region_name) if not remote_sec_group: raise exception.SecurityGroupCreationError( @@ -71,11 +70,12 @@ class SecurityGroup(DatabaseModelBase): raise @classmethod - def create_for_instance(cls, instance_id, context): + def create_for_instance(cls, instance_id, context, region_name): # Create a new security group name = "%s_%s" % (CONF.trove_security_group_name_prefix, instance_id) description = _("Security Group for %s") % instance_id - sec_group = cls.create_sec_group(name, description, context) + sec_group = cls.create_sec_group(name, description, context, + region_name) # Currently this locked down by default, since we don't create any # default security group rules for the security group. @@ -101,14 +101,14 @@ class SecurityGroup(DatabaseModelBase): return SecurityGroupRule.find_all(group_id=self.id, deleted=False) - def delete(self, context): + def delete(self, context, region_name): try: sec_group_rules = self.get_rules() if sec_group_rules: for rule in sec_group_rules: - rule.delete(context) + rule.delete(context, region_name) - RemoteSecurityGroup.delete(self.id, context) + RemoteSecurityGroup.delete(self.id, context, region_name) super(SecurityGroup, self).delete() except exception.TroveError: @@ -116,7 +116,7 @@ class SecurityGroup(DatabaseModelBase): raise exception.TroveError("Failed to delete Security Group") @classmethod - def delete_for_instance(cls, instance_id, context): + def delete_for_instance(cls, instance_id, context, region_name): try: association = SecurityGroupInstanceAssociation.find_by( instance_id=instance_id, @@ -124,7 +124,7 @@ class SecurityGroup(DatabaseModelBase): if association: sec_group = association.get_security_group() if sec_group: - sec_group.delete(context) + sec_group.delete(context, region_name) association.delete() except (exception.ModelNotFoundError, exception.TroveError): @@ -140,7 +140,7 @@ class SecurityGroupRule(DatabaseModelBase): @classmethod def create_sec_group_rule(cls, sec_group, protocol, from_port, - to_port, cidr, context): + to_port, cidr, context, region_name): try: remote_rule_id = RemoteSecurityGroup.add_rule( sec_group_id=sec_group['id'], @@ -148,7 +148,8 @@ class SecurityGroupRule(DatabaseModelBase): from_port=from_port, to_port=to_port, cidr=cidr, - context=context) + context=context, + region_name=region_name) if not remote_rule_id: raise exception.SecurityGroupRuleCreationError( @@ -172,10 +173,10 @@ class SecurityGroupRule(DatabaseModelBase): tenant_id=tenant_id, deleted=False) - def delete(self, context): + def delete(self, context, region_name): try: # Delete Remote Security Group Rule - RemoteSecurityGroup.delete_rule(self.id, context) + RemoteSecurityGroup.delete_rule(self.id, context, region_name) super(SecurityGroupRule, self).delete() except exception.TroveError: LOG.exception(_('Failed to delete security group.')) @@ -210,42 +211,44 @@ class RemoteSecurityGroup(NetworkRemoteModelBase): _data_fields = ['id', 'name', 'description', 'rules'] - def __init__(self, security_group=None, id=None, context=None): + def __init__(self, security_group=None, id=None, context=None, + region_name=None): if id is None and security_group is None: msg = _("Security Group does not have id defined!") raise exception.InvalidModelError(msg) elif security_group is None: - driver = self.get_driver(context) + driver = self.get_driver(context, + region_name or CONF.os_region_name) self._data_object = driver.get_sec_group_by_id(group_id=id) else: self._data_object = security_group @classmethod - def create(cls, name, description, context): + def create(cls, name, description, context, region_name): """Creates a new Security Group.""" - driver = cls.get_driver(context) + driver = cls.get_driver(context, region_name) sec_group = driver.create_security_group( name=name, description=description) return RemoteSecurityGroup(security_group=sec_group) @classmethod - def delete(cls, sec_group_id, context): + def delete(cls, sec_group_id, context, region_name): """Deletes a Security Group.""" - driver = cls.get_driver(context) + driver = cls.get_driver(context, region_name) driver.delete_security_group(sec_group_id) @classmethod def add_rule(cls, sec_group_id, protocol, from_port, - to_port, cidr, context): + to_port, cidr, context, region_name): """Adds a new rule to an existing security group.""" - driver = cls.get_driver(context) + driver = cls.get_driver(context, region_name) sec_group_rule = driver.add_security_group_rule( sec_group_id, protocol, from_port, to_port, cidr) return sec_group_rule.id @classmethod - def delete_rule(cls, sec_group_rule_id, context): + def delete_rule(cls, sec_group_rule_id, context, region_name): """Deletes a rule from an existing security group.""" - driver = cls.get_driver(context) + driver = cls.get_driver(context, region_name) driver.delete_security_group_rule(sec_group_rule_id) diff --git a/trove/extensions/security_group/service.py b/trove/extensions/security_group/service.py index 2622accf69..5a8c8bb43f 100644 --- a/trove/extensions/security_group/service.py +++ b/trove/extensions/security_group/service.py @@ -77,7 +77,7 @@ class SecurityGroupRuleController(wsgi.Controller): "exist or does not belong to tenant %s") % tenant_id) raise exception.Forbidden("Unauthorized") - sec_group_rule.delete(context) + sec_group_rule.delete(context, CONF.os_region_name) sec_group.save() return wsgi.Result(None, 204) @@ -106,7 +106,8 @@ class SecurityGroupRuleController(wsgi.Controller): from_, to_ = utils.gen_ports(port_or_range) rule = models.SecurityGroupRule.create_sec_group_rule( sec_group, protocol, int(from_), int(to_), - body['security_group_rule']['cidr'], context) + body['security_group_rule']['cidr'], context, + CONF.os_region_name) rules.append(rule) except (ValueError, AttributeError) as e: raise exception.BadRequest(msg=str(e)) diff --git a/trove/instance/models.py b/trove/instance/models.py index 8b3668e655..49d6b15105 100644 --- a/trove/instance/models.py +++ b/trove/instance/models.py @@ -27,6 +27,7 @@ from oslo_log import log as logging from trove.backup.models import Backup from trove.common import cfg from trove.common import exception +from trove.common.glance_remote import create_glance_client from trove.common.i18n import _, _LE, _LI, _LW import trove.common.instance as tr_instance from trove.common.notification import StartNotification @@ -36,6 +37,7 @@ from trove.common.remote import create_guest_client from trove.common.remote import create_nova_client from trove.common import server_group as srv_grp from trove.common import template +from trove.common.trove_remote import create_trove_client from trove.common import utils from trove.configuration.models import Configuration from trove.datastore import models as datastore_models @@ -62,7 +64,7 @@ def filter_ips(ips, white_list_regex, black_list_regex): and not re.search(black_list_regex, ip)] -def load_server(context, instance_id, server_id): +def load_server(context, instance_id, server_id, region_name): """ Loads a server or raises an exception. :param context: request context used to access nova @@ -74,7 +76,7 @@ def load_server(context, instance_id, server_id): :type server_id: unicode :rtype: novaclient.v2.servers.Server """ - client = create_nova_client(context) + client = create_nova_client(context, region_name=region_name) try: server = client.servers.get(server_id) except nova_exceptions.NotFound: @@ -120,7 +122,7 @@ def load_simple_instance_server_status(context, db_info): db_info.server_status = "BUILD" db_info.addresses = {} else: - client = create_nova_client(context) + client = create_nova_client(context, db_info.region_id) try: server = client.servers.get(db_info.compute_instance_id) db_info.server_status = server.status @@ -427,6 +429,10 @@ class SimpleInstance(object): def shard_id(self): return self.db_info.shard_id + @property + def region_name(self): + return self.db_info.region_id + class DetailInstance(SimpleInstance): """A detailed view of an Instance. @@ -511,7 +517,8 @@ def load_instance(cls, context, id, needs_server=False, else: try: server = load_server(context, db_info.id, - db_info.compute_instance_id) + db_info.compute_instance_id, + region_name=db_info.region_id) # TODO(tim.simpson): Remove this hack when we have notifications! db_info.server_status = server.status db_info.addresses = server.addresses @@ -547,7 +554,7 @@ def load_guest_info(instance, context, id): instance.volume_used = volume_info['used'] instance.volume_total = volume_info['total'] except Exception as e: - LOG.error(e) + LOG.exception(e) return instance @@ -646,8 +653,8 @@ class BaseInstance(SimpleInstance): self.set_instance_fault_deleted() # Delete associated security group if CONF.trove_security_groups_support: - SecurityGroup.delete_for_instance(self.db_info.id, - self.context) + SecurityGroup.delete_for_instance(self.db_info.id, self.context, + self.db_info.region_id) @property def guest(self): @@ -658,7 +665,8 @@ class BaseInstance(SimpleInstance): @property def nova_client(self): if not self._nova_client: - self._nova_client = create_nova_client(self.context) + self._nova_client = create_nova_client( + self.context, region_name=self.db_info.region_id) return self._nova_client def update_db(self, **values): @@ -684,7 +692,8 @@ class BaseInstance(SimpleInstance): @property def volume_client(self): if not self._volume_client: - self._volume_client = create_cinder_client(self.context) + self._volume_client = create_cinder_client( + self.context, region_name=self.db_info.region_id) return self._volume_client def reset_task_status(self): @@ -773,13 +782,61 @@ class Instance(BuiltInstance): datastore_manager) return False + @classmethod + def _validate_remote_datastore(cls, context, region_name, flavor, + datastore, datastore_version): + remote_nova_client = create_nova_client(context, + region_name=region_name) + try: + remote_flavor = remote_nova_client.flavors.get(flavor.id) + if (flavor.ram != remote_flavor.ram or + flavor.vcpus != remote_flavor.vcpus): + raise exception.TroveError( + "Flavors differ between regions" + " %(local)s and %(remote)s." % + {'local': CONF.os_region_name, 'remote': region_name}) + except nova_exceptions.NotFound: + raise exception.TroveError( + "Flavors %(flavor)s not found in region %(remote)s." + % {'flavor': flavor.id, 'remote': region_name}) + + remote_trove_client = create_trove_client( + context, region_name=region_name) + try: + remote_ds_ver = remote_trove_client.datastore_versions.get( + datastore.name, datastore_version.name) + if datastore_version.name != remote_ds_ver.name: + raise exception.TroveError( + "Datastore versions differ between regions " + "%(local)s and %(remote)s." % + {'local': CONF.os_region_name, 'remote': region_name}) + except exception.NotFound: + raise exception.TroveError( + "Datastore Version %(dsv)s not found in region %(remote)s." + % {'dsv': datastore_version.name, 'remote': region_name}) + + glance_client = create_glance_client(context) + local_image = glance_client.images.get(datastore_version.image) + remote_glance_client = create_glance_client( + context, region_name=region_name) + remote_image = remote_glance_client.images.get( + remote_ds_ver.image) + if local_image.checksum != remote_image.checksum: + raise exception.TroveError( + "Images for Datastore %(ds)s do not match" + "between regions %(local)s and %(remote)s." % + {'ds': datastore.name, 'local': CONF.os_region_name, + 'remote': region_name}) + @classmethod def create(cls, context, name, flavor_id, image_id, databases, users, datastore, datastore_version, volume_size, backup_id, availability_zone=None, nics=None, configuration_id=None, slave_of_id=None, cluster_config=None, replica_count=None, volume_type=None, modules=None, - locality=None): + locality=None, region_name=None): + + region_name = region_name or CONF.os_region_name call_args = { 'name': name, @@ -788,6 +845,7 @@ class Instance(BuiltInstance): 'datastore_version': datastore_version.name, 'image_id': image_id, 'availability_zone': availability_zone, + 'region_name': region_name, } # All nova flavors are permitted for a datastore-version unless one @@ -812,6 +870,12 @@ class Instance(BuiltInstance): except nova_exceptions.NotFound: raise exception.FlavorNotFound(uuid=flavor_id) + # If a different region is specified for the instance, ensure + # that the flavor and image are the same in both regions + if region_name and region_name != CONF.os_region_name: + cls._validate_remote_datastore(context, region_name, flavor, + datastore, datastore_version) + deltas = {'instances': 1} volume_support = datastore_cfg.volume_support if volume_support: @@ -945,10 +1009,12 @@ class Instance(BuiltInstance): task_status=InstanceTasks.BUILDING, configuration_id=configuration_id, slave_of_id=slave_of_id, cluster_id=cluster_id, - shard_id=shard_id, type=instance_type) + shard_id=shard_id, type=instance_type, + region_id=region_name) LOG.debug("Tenant %(tenant)s created new Trove instance " - "%(db)s.", - {'tenant': context.tenant, 'db': db_info.id}) + "%(db)s in region %(region)s.", + {'tenant': context.tenant, 'db': db_info.id, + 'region': region_name}) instance_id = db_info.id cls.add_instance_modules(context, instance_id, modules) @@ -1009,8 +1075,7 @@ class Instance(BuiltInstance): context, instance_id, module.id, module.md5) def get_flavor(self): - client = create_nova_client(self.context) - return client.flavors.get(self.flavor_id) + return self.nova_client.flavors.get(self.flavor_id) def get_default_configuration_template(self): flavor = self.get_flavor() @@ -1036,13 +1101,12 @@ class Instance(BuiltInstance): raise exception.BadRequest(_("The new flavor id must be different " "than the current flavor id of '%s'.") % self.flavor_id) - client = create_nova_client(self.context) try: - new_flavor = client.flavors.get(new_flavor_id) + new_flavor = self.nova_client.flavors.get(new_flavor_id) except nova_exceptions.NotFound: raise exception.FlavorNotFound(uuid=new_flavor_id) - old_flavor = client.flavors.get(self.flavor_id) + old_flavor = self.nova_client.flavors.get(self.flavor_id) if self.volume_support: if new_flavor.ephemeral != 0: raise exception.LocalStorageNotSupported() @@ -1322,8 +1386,8 @@ class Instances(object): @staticmethod def load(context, include_clustered, instance_ids=None): - def load_simple_instance(context, db, status, **kwargs): - return SimpleInstance(context, db, status) + def load_simple_instance(context, db_info, status, **kwargs): + return SimpleInstance(context, db_info, status) if context is None: raise TypeError("Argument context not defined.") @@ -1375,7 +1439,14 @@ class Instances(object): db.addresses = {} else: try: - server = find_server(db.id, db.compute_instance_id) + if (not db.region_id + or db.region_id == CONF.os_region_name): + server = find_server(db.id, db.compute_instance_id) + else: + nova_client = create_nova_client( + context, region_name=db.region_id) + server = nova_client.servers.get( + db.compute_instance_id) db.server_status = server.status db.addresses = server.addresses except exception.ComputeInstanceNotFound: @@ -1402,13 +1473,12 @@ class Instances(object): class DBInstance(dbmodels.DatabaseModelBase): - """Defines the task being executed plus the start time.""" _data_fields = ['name', 'created', 'compute_instance_id', 'task_id', 'task_description', 'task_start_time', 'volume_id', 'deleted', 'tenant_id', 'datastore_version_id', 'configuration_id', 'slave_of_id', - 'cluster_id', 'shard_id', 'type'] + 'cluster_id', 'shard_id', 'type', 'region_id'] def __init__(self, task_status, **kwargs): """ diff --git a/trove/instance/service.py b/trove/instance/service.py index 7c819da19d..c70813fe1e 100644 --- a/trove/instance/service.py +++ b/trove/instance/service.py @@ -20,6 +20,7 @@ import webob.exc from trove.backup.models import Backup as backup_model from trove.backup import views as backup_views import trove.common.apischema as apischema +from trove.common import cfg from trove.common import exception from trove.common.i18n import _ from trove.common.i18n import _LI @@ -37,6 +38,7 @@ from trove.module import models as module_models from trove.module import views as module_views +CONF = cfg.CONF LOG = logging.getLogger(__name__) @@ -302,6 +304,7 @@ class InstanceController(wsgi.Controller): 'Cannot specify locality when adding replicas to existing ' 'master.') raise exception.BadRequest(msg=dupe_locality_msg) + region_name = body['instance'].get('region_name', CONF.os_region_name) instance = models.Instance.create(context, name, flavor_id, image_id, databases, users, @@ -312,7 +315,8 @@ class InstanceController(wsgi.Controller): replica_count=replica_count, volume_type=volume_type, modules=modules, - locality=locality) + locality=locality, + region_name=region_name) view = views.InstanceDetailView(instance, req=req) return wsgi.Result(view.data(), 200) diff --git a/trove/instance/views.py b/trove/instance/views.py index cb383dc505..73531eae23 100644 --- a/trove/instance/views.py +++ b/trove/instance/views.py @@ -37,6 +37,7 @@ class InstanceView(object): "flavor": self._build_flavor_info(), "datastore": {"type": self.instance.datastore.name, "version": self.instance.datastore_version.name}, + "region": self.instance.region_name } if self.instance.volume_support: instance_dict['volume'] = {'size': self.instance.volume_size} diff --git a/trove/network/neutron.py b/trove/network/neutron.py index 6f8e966dd4..c8c60d9c57 100644 --- a/trove/network/neutron.py +++ b/trove/network/neutron.py @@ -41,9 +41,9 @@ class NovaNetworkStruct(object): class NeutronDriver(base.NetworkDriver): - def __init__(self, context): + def __init__(self, context, region_name): try: - self.client = remote.create_neutron_client(context) + self.client = remote.create_neutron_client(context, region_name) except neutron_exceptions.NeutronClientException as e: raise exception.TroveError(str(e)) diff --git a/trove/network/nova.py b/trove/network/nova.py index 5f0623c5e3..a66a8be4d7 100644 --- a/trove/network/nova.py +++ b/trove/network/nova.py @@ -27,10 +27,10 @@ LOG = logging.getLogger(__name__) class NovaNetwork(base.NetworkDriver): - def __init__(self, context): + def __init__(self, context, region_name): try: self.client = remote.create_nova_client( - context) + context, region_name) except nova_exceptions.ClientException as e: raise exception.TroveError(str(e)) diff --git a/trove/taskmanager/manager.py b/trove/taskmanager/manager.py index a70872b723..24feae557e 100644 --- a/trove/taskmanager/manager.py +++ b/trove/taskmanager/manager.py @@ -428,13 +428,14 @@ class Manager(periodic_task.PeriodicTasks): mgmtmodels.publish_exist_events(self.exists_transformer, self.admin_context) - @periodic_task.periodic_task(spacing=CONF.quota_notification_interval) - def publish_quota_notifications(self, context): - nova_client = remote.create_nova_client(self.admin_context) - for tenant in nova_client.tenants.list(): - for quota in QUOTAS.get_all_quotas_by_tenant(tenant.id): - usage = QUOTAS.get_quota_usage(quota) - DBaaSQuotas(self.admin_context, quota, usage).notify() + if CONF.quota_notification_interval: + @periodic_task.periodic_task(spacing=CONF.quota_notification_interval) + def publish_quota_notifications(self, context): + nova_client = remote.create_nova_client(self.admin_context) + for tenant in nova_client.tenants.list(): + for quota in QUOTAS.get_all_quotas_by_tenant(tenant.id): + usage = QUOTAS.get_quota_usage(quota) + DBaaSQuotas(self.admin_context, quota, usage).notify() def __getattr__(self, name): """ diff --git a/trove/taskmanager/models.py b/trove/taskmanager/models.py index ed62b90fd1..fcca4f9688 100755 --- a/trove/taskmanager/models.py +++ b/trove/taskmanager/models.py @@ -565,7 +565,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): LOG.error(msg_create) # Make sure we log any unexpected errors from the create if not isinstance(e_create, TroveError): - LOG.error(e_create) + LOG.exception(e_create) msg_delete = ( _("An error occurred while deleting a bad " "replication snapshot from instance %(source)s.") % @@ -866,7 +866,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): def _create_volume(self, volume_size, volume_type, datastore_manager): LOG.debug("Begin _create_volume for id: %s" % self.id) - volume_client = create_cinder_client(self.context) + volume_client = create_cinder_client(self.context, self.region_name) volume_desc = ("datastore volume for %s" % self.id) volume_ref = volume_client.volumes.create( volume_size, name="datastore-%s" % self.id, @@ -1009,7 +1009,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): def _create_secgroup(self, datastore_manager): security_group = SecurityGroup.create_for_instance( - self.id, self.context) + self.id, self.context, self.region_name) tcp_ports = CONF.get(datastore_manager).tcp_ports udp_ports = CONF.get(datastore_manager).udp_ports icmp = CONF.get(datastore_manager).icmp @@ -1037,7 +1037,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): if protocol == 'icmp': SecurityGroupRule.create_sec_group_rule( s_group, 'icmp', None, None, - cidr, self.context) + cidr, self.context, self.region_name) else: for port_or_range in set(ports): try: @@ -1045,7 +1045,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin): from_, to_ = utils.gen_ports(port_or_range) SecurityGroupRule.create_sec_group_rule( s_group, protocol, int(from_), int(to_), - cidr, self.context) + cidr, self.context, self.region_name) except (ValueError, TroveError): set_error_and_raise([from_, to_]) @@ -1143,7 +1143,8 @@ class BuiltInstanceTasks(BuiltInstance, NotifyMixin, ConfigurationMixin): # If volume has been resized it must be manually removed in cinder try: if self.volume_id: - volume_client = create_cinder_client(self.context) + volume_client = create_cinder_client(self.context, + self.region_name) volume = volume_client.volumes.get(self.volume_id) if volume.status == "available": LOG.info(_("Deleting volume %(v)s for instance: %(i)s.") diff --git a/trove/tests/api/instances.py b/trove/tests/api/instances.py index 6cacbb911d..161bdd2a46 100644 --- a/trove/tests/api/instances.py +++ b/trove/tests/api/instances.py @@ -696,7 +696,8 @@ class CreateInstance(object): # Check these attrs only are returned in create response allowed_attrs = ['created', 'flavor', 'addresses', 'id', 'links', - 'name', 'status', 'updated', 'datastore', 'fault'] + 'name', 'status', 'updated', 'datastore', 'fault', + 'region'] if ROOT_ON_CREATE: allowed_attrs.append('password') if VOLUME_SUPPORT: @@ -1138,7 +1139,8 @@ class TestInstanceListing(object): @test def test_index_list(self): allowed_attrs = ['id', 'links', 'name', 'status', 'flavor', - 'datastore', 'ip', 'hostname', 'replica_of'] + 'datastore', 'ip', 'hostname', 'replica_of', + 'region'] if VOLUME_SUPPORT: allowed_attrs.append('volume') instances = dbaas.instances.list() @@ -1159,7 +1161,7 @@ class TestInstanceListing(object): def test_get_instance(self): allowed_attrs = ['created', 'databases', 'flavor', 'hostname', 'id', 'links', 'name', 'status', 'updated', 'ip', - 'datastore', 'fault'] + 'datastore', 'fault', 'region'] if VOLUME_SUPPORT: allowed_attrs.append('volume') else: @@ -1247,7 +1249,7 @@ class TestInstanceListing(object): 'flavor', 'guest_status', 'host', 'hostname', 'id', 'name', 'root_enabled_at', 'root_enabled_by', 'server_state_description', 'status', 'datastore', - 'updated', 'users', 'volume', 'fault'] + 'updated', 'users', 'volume', 'fault', 'region'] with CheckInstance(result._info) as check: check.contains_allowed_attrs( result._info, allowed_attrs, diff --git a/trove/tests/api/mgmt/instances.py b/trove/tests/api/mgmt/instances.py index f8fbf840ec..7961d46140 100644 --- a/trove/tests/api/mgmt/instances.py +++ b/trove/tests/api/mgmt/instances.py @@ -232,7 +232,9 @@ class MgmtInstancesIndex(object): 'task_description', 'tenant_id', 'updated', + 'region' ] + if CONFIG.trove_volume_support: expected_fields.append('volume') @@ -254,6 +256,7 @@ class MgmtInstancesIndex(object): Make sure that the deleted= filter works as expected, and no instances are excluded. """ + if not hasattr(self.client.management.index, 'deleted'): raise SkipTest("instance index must have a deleted " "label for this test") diff --git a/trove/tests/fakes/guestagent.py b/trove/tests/fakes/guestagent.py index 79e0a02d70..baf0bee9ca 100644 --- a/trove/tests/fakes/guestagent.py +++ b/trove/tests/fakes/guestagent.py @@ -361,6 +361,9 @@ class FakeGuest(object): def backup_required_for_replication(self): return True + def post_processing_required_for_replication(self): + return False + def module_list(self, context, include_contents=False): return [] diff --git a/trove/tests/fakes/nova.py b/trove/tests/fakes/nova.py index c406431531..ffa7a1df57 100644 --- a/trove/tests/fakes/nova.py +++ b/trove/tests/fakes/nova.py @@ -870,13 +870,13 @@ def get_client_data(context): return CLIENT_DATA[context] -def fake_create_nova_client(context): +def fake_create_nova_client(context, region_name=None): return get_client_data(context)['nova'] -def fake_create_nova_volume_client(context): +def fake_create_nova_volume_client(context, region_name=None): return get_client_data(context)['volume'] -def fake_create_cinder_client(context): +def fake_create_cinder_client(context, region_name=None): return get_client_data(context)['volume'] diff --git a/trove/tests/unittests/cluster/test_cluster_controller.py b/trove/tests/unittests/cluster/test_cluster_controller.py index 29541f2396..e4554f6641 100644 --- a/trove/tests/unittests/cluster/test_cluster_controller.py +++ b/trove/tests/unittests/cluster/test_cluster_controller.py @@ -177,6 +177,7 @@ class TestClusterController(TestCase): 'flavor_id': '1234', 'availability_zone': 'az', 'modules': None, + 'region_name': None, 'nics': [ {'net-id': 'e89aa5fd-6b0a-436d-a75c-1545d34d5331'} ] diff --git a/trove/tests/unittests/cluster/test_cluster_pxc_controller.py b/trove/tests/unittests/cluster/test_cluster_pxc_controller.py index 48ae39884d..51176aa2dc 100644 --- a/trove/tests/unittests/cluster/test_cluster_pxc_controller.py +++ b/trove/tests/unittests/cluster/test_cluster_pxc_controller.py @@ -142,6 +142,7 @@ class TestClusterController(trove_testtools.TestCase): 'flavor_id': '1234', 'availability_zone': 'az', 'modules': None, + 'region_name': None, 'nics': [ {'net-id': 'e89aa5fd-6b0a-436d-a75c-1545d34d5331'} ] diff --git a/trove/tests/unittests/cluster/test_cluster_redis_controller.py b/trove/tests/unittests/cluster/test_cluster_redis_controller.py index 00666abbb4..6fe3cfc753 100644 --- a/trove/tests/unittests/cluster/test_cluster_redis_controller.py +++ b/trove/tests/unittests/cluster/test_cluster_redis_controller.py @@ -157,6 +157,7 @@ class TestClusterController(trove_testtools.TestCase): "flavor_id": "1234", "availability_zone": "az", 'modules': None, + 'region_name': None, "nics": [ {"net-id": "e89aa5fd-6b0a-436d-a75c-1545d34d5331"} ] @@ -167,6 +168,7 @@ class TestClusterController(trove_testtools.TestCase): "flavor_id": "1234", "availability_zone": "az", 'modules': None, + 'region_name': None, "nics": [ {"net-id": "e89aa5fd-6b0a-436d-a75c-1545d34d5331"} ] @@ -177,6 +179,7 @@ class TestClusterController(trove_testtools.TestCase): "flavor_id": "1234", "availability_zone": "az", 'modules': None, + 'region_name': None, "nics": [ {"net-id": "e89aa5fd-6b0a-436d-a75c-1545d34d5331"} ] diff --git a/trove/tests/unittests/cluster/test_cluster_vertica_controller.py b/trove/tests/unittests/cluster/test_cluster_vertica_controller.py index e56a950f6f..720f5a2eb8 100644 --- a/trove/tests/unittests/cluster/test_cluster_vertica_controller.py +++ b/trove/tests/unittests/cluster/test_cluster_vertica_controller.py @@ -142,6 +142,7 @@ class TestClusterController(trove_testtools.TestCase): 'flavor_id': '1234', 'availability_zone': 'az', 'modules': None, + 'region_name': None, 'nics': [ {'net-id': 'e89aa5fd-6b0a-436d-a75c-1545d34d5331'} ] diff --git a/trove/tests/unittests/cluster/test_models.py b/trove/tests/unittests/cluster/test_models.py index a0f2c79b84..e1ed045a52 100644 --- a/trove/tests/unittests/cluster/test_models.py +++ b/trove/tests/unittests/cluster/test_models.py @@ -38,11 +38,15 @@ class TestModels(trove_testtools.TestCase): mock_flv.ephemeral = 0 test_instances = [{'flavor_id': 1, 'volume_size': 10}, - {'flavor_id': 1, 'volume_size': 1.5}, - {'flavor_id': 2, 'volume_size': 3}] + {'flavor_id': 1, 'volume_size': 1.5, + 'region_name': 'home'}, + {'flavor_id': 2, 'volume_size': 3, + 'region_name': 'work'}] models.validate_instance_flavors(Mock(), test_instances, True, True) - create_nove_cli_mock.assert_called_once_with(ANY) + create_nove_cli_mock.assert_has_calls([call(ANY, None), + call(ANY, 'home'), + call(ANY, 'work')]) self.assertRaises(exception.LocalStorageNotSpecified, models.validate_instance_flavors, diff --git a/trove/tests/unittests/common/test_remote.py b/trove/tests/unittests/common/test_remote.py index 02c813a5f7..7b0ffec6b2 100644 --- a/trove/tests/unittests/common/test_remote.py +++ b/trove/tests/unittests/common/test_remote.py @@ -24,6 +24,7 @@ from testtools import ExpectedException, matchers from trove.common import cfg from trove.common.context import TroveContext from trove.common import exception +from trove.common import glance_remote from trove.common import remote from trove.tests.fakes.swift import SwiftClientStub from trove.tests.unittests import trove_testtools @@ -574,6 +575,47 @@ class TestCreateSwiftClient(trove_testtools.TestCase): client.url) +class TestCreateGlanceClient(trove_testtools.TestCase): + def setUp(self): + super(TestCreateGlanceClient, self).setUp() + self.glance_public_url = 'http://publicURL/v2' + self.glancev3_public_url_region_two = 'http://publicURL-r2/v3' + self.service_catalog = [ + { + 'endpoints': [ + { + 'region': 'RegionOne', + 'publicURL': self.glance_public_url, + } + ], + 'type': 'image' + }, + { + 'endpoints': [ + { + 'region': 'RegionOne', + 'publicURL': 'http://publicURL-r1/v1', + }, + { + 'region': 'RegionTwo', + 'publicURL': self.glancev3_public_url_region_two, + } + ], + 'type': 'imagev3' + } + ] + + def test_create_with_no_conf_no_catalog(self): + self.assertRaises(exception.EmptyCatalog, + glance_remote.create_glance_client, + TroveContext()) + + def test_create(self): + client = glance_remote.create_glance_client( + TroveContext(service_catalog=self.service_catalog)) + self.assertIsNotNone(client) + + class TestEndpoints(trove_testtools.TestCase): """ Copied from glance/tests/unit/test_auth.py. diff --git a/trove/tests/unittests/network/test_neutron_driver.py b/trove/tests/unittests/network/test_neutron_driver.py index bda649b3c7..102e319d51 100644 --- a/trove/tests/unittests/network/test_neutron_driver.py +++ b/trove/tests/unittests/network/test_neutron_driver.py @@ -19,6 +19,7 @@ from mock import Mock, patch from neutronclient.common import exceptions as neutron_exceptions from neutronclient.v2_0 import client as NeutronClient +from trove.common import cfg from trove.common import exception from trove.common.models import NetworkRemoteModelBase from trove.common import remote @@ -28,6 +29,9 @@ from trove.network.neutron import NeutronDriver as driver from trove.tests.unittests import trove_testtools +CONF = cfg.CONF + + class NeutronDriverTest(trove_testtools.TestCase): def setUp(self): super(NeutronDriverTest, self).setUp() @@ -50,26 +54,30 @@ class NeutronDriverTest(trove_testtools.TestCase): def test_create_security_group(self): driver.create_security_group = Mock() RemoteSecurityGroup.create(name=Mock(), description=Mock(), - context=self.context) + context=self.context, + region_name=CONF.os_region_name) self.assertEqual(1, driver.create_security_group.call_count) def test_add_security_group_rule(self): driver.add_security_group_rule = Mock() RemoteSecurityGroup.add_rule(sec_group_id=Mock(), protocol=Mock(), from_port=Mock(), to_port=Mock(), - cidr=Mock(), context=self.context) + cidr=Mock(), context=self.context, + region_name=CONF.os_region_name) self.assertEqual(1, driver.add_security_group_rule.call_count) def test_delete_security_group_rule(self): driver.delete_security_group_rule = Mock() RemoteSecurityGroup.delete_rule(sec_group_rule_id=Mock(), - context=self.context) + context=self.context, + region_name=CONF.os_region_name) self.assertEqual(1, driver.delete_security_group_rule.call_count) def test_delete_security_group(self): driver.delete_security_group = Mock() RemoteSecurityGroup.delete(sec_group_id=Mock(), - context=self.context) + context=self.context, + region_name=CONF.os_region_name) self.assertEqual(1, driver.delete_security_group.call_count) @@ -81,7 +89,7 @@ class NeutronDriverExceptionTest(trove_testtools.TestCase): self.orig_NeutronClient = NeutronClient.Client self.orig_get_endpoint = remote.get_endpoint remote.get_endpoint = MagicMock(return_value="neutron_url") - mock_driver = neutron.NeutronDriver(self.context) + mock_driver = neutron.NeutronDriver(self.context, "regionOne") NetworkRemoteModelBase.get_driver = MagicMock( return_value=mock_driver) @@ -98,23 +106,27 @@ class NeutronDriverExceptionTest(trove_testtools.TestCase): def test_create_sg_with_exception(self, mock_logging): self.assertRaises(exception.SecurityGroupCreationError, RemoteSecurityGroup.create, - "sg_name", "sg_desc", self.context) + "sg_name", "sg_desc", self.context, + region_name=CONF.os_region_name) @patch('trove.network.neutron.LOG') def test_add_sg_rule_with_exception(self, mock_logging): self.assertRaises(exception.SecurityGroupRuleCreationError, RemoteSecurityGroup.add_rule, "12234", "tcp", "22", "22", - "0.0.0.0/8", self.context) + "0.0.0.0/8", self.context, + region_name=CONF.os_region_name) @patch('trove.network.neutron.LOG') def test_delete_sg_rule_with_exception(self, mock_logging): self.assertRaises(exception.SecurityGroupRuleDeletionError, RemoteSecurityGroup.delete_rule, - "12234", self.context) + "12234", self.context, + region_name=CONF.os_region_name) @patch('trove.network.neutron.LOG') def test_delete_sg_with_exception(self, mock_logging): self.assertRaises(exception.SecurityGroupDeletionError, RemoteSecurityGroup.delete, - "123445", self.context) + "123445", self.context, + region_name=CONF.os_region_name) diff --git a/trove/tests/unittests/secgroups/test_security_group.py b/trove/tests/unittests/secgroups/test_security_group.py index 459a303e4c..0e98693ed3 100644 --- a/trove/tests/unittests/secgroups/test_security_group.py +++ b/trove/tests/unittests/secgroups/test_security_group.py @@ -18,6 +18,7 @@ from mock import Mock from mock import patch from novaclient import exceptions as nova_exceptions +from trove.common import cfg from trove.common import exception import trove.common.remote from trove.extensions.security_group import models as sec_mod @@ -26,6 +27,9 @@ from trove.tests.fakes import nova from trove.tests.unittests import trove_testtools +CONF = cfg.CONF + + """ Unit tests for testing the exceptions raised by Security Groups """ @@ -49,7 +53,7 @@ class Security_Group_Exceptions_Test(trove_testtools.TestCase): self.FakeClient.security_group_rules.delete = fException trove.common.remote.create_nova_client = ( - lambda c: self._return_mocked_nova_client(c)) + lambda c, r: self._return_mocked_nova_client(c)) def tearDown(self): super(Security_Group_Exceptions_Test, self).tearDown() @@ -67,25 +71,29 @@ class Security_Group_Exceptions_Test(trove_testtools.TestCase): sec_mod.RemoteSecurityGroup.create, "TestName", "TestDescription", - self.context) + self.context, + region_name=CONF.os_region_name) @patch('trove.network.nova.LOG') def test_failed_to_delete_security_group(self, mock_logging): self.assertRaises(exception.SecurityGroupDeletionError, sec_mod.RemoteSecurityGroup.delete, - 1, self.context) + 1, self.context, + region_name=CONF.os_region_name) @patch('trove.network.nova.LOG') def test_failed_to_create_security_group_rule(self, mock_logging): self.assertRaises(exception.SecurityGroupRuleCreationError, sec_mod.RemoteSecurityGroup.add_rule, - 1, "tcp", 3306, 3306, "0.0.0.0/0", self.context) + 1, "tcp", 3306, 3306, "0.0.0.0/0", self.context, + region_name=CONF.os_region_name) @patch('trove.network.nova.LOG') def test_failed_to_delete_security_group_rule(self, mock_logging): self.assertRaises(exception.SecurityGroupRuleDeletionError, sec_mod.RemoteSecurityGroup.delete_rule, - 1, self.context) + 1, self.context, + region_name=CONF.os_region_name) class fake_RemoteSecGr(object): @@ -93,7 +101,7 @@ class fake_RemoteSecGr(object): self.id = uuid.uuid4() return {'id': self.id} - def delete(self, context): + def delete(self, context, region_name): pass @@ -135,7 +143,7 @@ class SecurityGroupDeleteTest(trove_testtools.TestCase): sec_mod.SecurityGroupInstanceAssociation.find_by = self.fException self.assertIsNone( sec_mod.SecurityGroup.delete_for_instance( - uuid.uuid4(), self.context)) + uuid.uuid4(), self.context, CONF.os_region_name)) def test_get_security_group_from_assoc_with_db_exception(self): @@ -156,7 +164,7 @@ class SecurityGroupDeleteTest(trove_testtools.TestCase): return_value=new_fake_RemoteSecGrAssoc()) self.assertIsNone( sec_mod.SecurityGroup.delete_for_instance( - i_id, self.context)) + i_id, self.context, CONF.os_region_name)) def test_delete_secgr_assoc_with_db_exception(self): @@ -171,4 +179,4 @@ class SecurityGroupDeleteTest(trove_testtools.TestCase): get_security_group(), 'delete')) self.assertIsNone( sec_mod.SecurityGroup.delete_for_instance( - i_id, self.context)) + i_id, self.context, CONF.os_region_name))