Locality support for replication
In order to allow replication sets to be all on the same hypervisor (affinity) or all on different hypervisors (anti-affinity) a new argument (locality) needed to be added to the Trove create API. This changeset addresses the Trove server part of this feature. 'locality' can now be added to the ReST payload for a create command and it is passed along as a scheduler hint to Nova. The replication scenario tests were enhanced to test that 'affinity' works and 'anti-affinity' fails (since devstack sets up a single hypervisor by default). A check for the existance (and lack of) server-groups was added. This is to ensure that not only is the server-group created properly, but also that it has been deleted after all the related instances are gone. DocImpact: New functionality Partially implements: blueprint replication-cluster-locality Depends-On: I18f242983775526a7f1e2644302ebdc0dac025cf Change-Id: I7d924c25d832f9ff4386e9497bfd214f1b2b3503
This commit is contained in:
parent
9147f9dd6b
commit
187725fafb
@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- A locality flag was added to the trove ReST API to
|
||||
allow a user to specify whether new replicas should
|
||||
be on the same hypervisor (affinity) or on different
|
||||
hypervisors (anti-affinity).
|
@ -349,7 +349,8 @@ instance = {
|
||||
}
|
||||
},
|
||||
"nics": nics,
|
||||
"modules": module_list
|
||||
"modules": module_list,
|
||||
"locality": non_empty_string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
96
trove/common/server_group.py
Normal file
96
trove/common/server_group.py
Normal file
@ -0,0 +1,96 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
import six
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from trove.common import cfg
|
||||
from trove.common.i18n import _
|
||||
from trove.common.remote import create_nova_client
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServerGroup(object):
|
||||
|
||||
@classmethod
|
||||
def load(cls, context, compute_id):
|
||||
client = create_nova_client(context)
|
||||
server_group = None
|
||||
try:
|
||||
for sg in client.server_groups.list():
|
||||
if compute_id in sg.members:
|
||||
server_group = sg
|
||||
except Exception:
|
||||
LOG.exception(_("Could not load server group for compute %s") %
|
||||
compute_id)
|
||||
return server_group
|
||||
|
||||
@classmethod
|
||||
def create(cls, context, locality, name_suffix):
|
||||
client = create_nova_client(context)
|
||||
server_group_name = "%s_%s" % ('locality', name_suffix)
|
||||
server_group = client.server_groups.create(
|
||||
name=server_group_name, policies=[locality])
|
||||
LOG.debug("Created '%s' server group called %s (id: %s)." %
|
||||
(locality, server_group_name, server_group.id))
|
||||
|
||||
return server_group
|
||||
|
||||
@classmethod
|
||||
def delete(cls, context, server_group, force=False):
|
||||
# Only delete the server group if we're the last member in it, or if
|
||||
# it has no members
|
||||
if server_group:
|
||||
if force or len(server_group.members) <= 1:
|
||||
client = create_nova_client(context)
|
||||
client.server_groups.delete(server_group.id)
|
||||
LOG.debug("Deleted server group %s." % server_group.id)
|
||||
else:
|
||||
LOG.debug("Skipping delete of server group %s (members: %s)." %
|
||||
(server_group.id, server_group.members))
|
||||
|
||||
@classmethod
|
||||
def convert_to_hint(cls, server_group, hints=None):
|
||||
if server_group:
|
||||
hints = hints or {}
|
||||
hints["group"] = server_group.id
|
||||
return hints
|
||||
|
||||
@classmethod
|
||||
def build_scheduler_hint(cls, context, locality, name_suffix):
|
||||
scheduler_hint = None
|
||||
if locality:
|
||||
# Build the scheduler hint, but only if locality's a string
|
||||
if isinstance(locality, six.string_types):
|
||||
server_group = cls.create(
|
||||
context, locality, name_suffix)
|
||||
scheduler_hint = cls.convert_to_hint(
|
||||
server_group)
|
||||
else:
|
||||
# otherwise assume it's already in hint form (i.e. a dict)
|
||||
scheduler_hint = locality
|
||||
return scheduler_hint
|
||||
|
||||
@classmethod
|
||||
def get_locality(cls, server_group):
|
||||
locality = None
|
||||
if server_group:
|
||||
locality = server_group.policies[0]
|
||||
return locality
|
@ -33,6 +33,7 @@ from trove.common.remote import create_cinder_client
|
||||
from trove.common.remote import create_dns_client
|
||||
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 import utils
|
||||
from trove.configuration.models import Configuration
|
||||
@ -153,7 +154,7 @@ class SimpleInstance(object):
|
||||
"""
|
||||
|
||||
def __init__(self, context, db_info, datastore_status, root_password=None,
|
||||
ds_version=None, ds=None):
|
||||
ds_version=None, ds=None, locality=None):
|
||||
"""
|
||||
:type context: trove.common.context.TroveContext
|
||||
:type db_info: trove.instance.models.DBInstance
|
||||
@ -170,6 +171,7 @@ class SimpleInstance(object):
|
||||
if ds is None:
|
||||
self.ds = (datastore_models.Datastore.
|
||||
load(self.ds_version.datastore_id))
|
||||
self.locality = locality
|
||||
|
||||
self.slave_list = None
|
||||
|
||||
@ -495,7 +497,7 @@ def load_instance(cls, context, id, needs_server=False,
|
||||
return cls(context, db_info, server, service_status)
|
||||
|
||||
|
||||
def load_instance_with_guest(cls, context, id, cluster_id=None):
|
||||
def load_instance_with_info(cls, context, id, cluster_id=None):
|
||||
db_info = get_db_info(context, id, cluster_id)
|
||||
load_simple_instance_server_status(context, db_info)
|
||||
service_status = InstanceServiceStatus.find_by(instance_id=id)
|
||||
@ -503,6 +505,7 @@ def load_instance_with_guest(cls, context, id, cluster_id=None):
|
||||
{'instance_id': id, 'service_status': service_status.status})
|
||||
instance = cls(context, db_info, service_status)
|
||||
load_guest_info(instance, context, id)
|
||||
load_server_group_info(instance, context, db_info.compute_instance_id)
|
||||
return instance
|
||||
|
||||
|
||||
@ -518,6 +521,12 @@ def load_guest_info(instance, context, id):
|
||||
return instance
|
||||
|
||||
|
||||
def load_server_group_info(instance, context, compute_id):
|
||||
server_group = srv_grp.ServerGroup.load(context, compute_id)
|
||||
if server_group:
|
||||
instance.locality = srv_grp.ServerGroup.get_locality(server_group)
|
||||
|
||||
|
||||
class BaseInstance(SimpleInstance):
|
||||
"""Represents an instance.
|
||||
-----------
|
||||
@ -557,6 +566,8 @@ class BaseInstance(SimpleInstance):
|
||||
self._guest = None
|
||||
self._nova_client = None
|
||||
self._volume_client = None
|
||||
self._server_group = None
|
||||
self._server_group_loaded = False
|
||||
|
||||
def get_guest(self):
|
||||
return create_guest_client(self.context, self.db_info.id)
|
||||
@ -640,6 +651,15 @@ class BaseInstance(SimpleInstance):
|
||||
self.id)
|
||||
self.update_db(task_status=InstanceTasks.NONE)
|
||||
|
||||
@property
|
||||
def server_group(self):
|
||||
# The server group could be empty, so we need a flag to cache it
|
||||
if not self._server_group_loaded:
|
||||
self._server_group = srv_grp.ServerGroup.load(
|
||||
self.context, self.db_info.compute_instance_id)
|
||||
self._server_group_loaded = True
|
||||
return self._server_group
|
||||
|
||||
|
||||
class FreshInstance(BaseInstance):
|
||||
@classmethod
|
||||
@ -677,7 +697,8 @@ class Instance(BuiltInstance):
|
||||
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):
|
||||
replica_count=None, volume_type=None, modules=None,
|
||||
locality=None):
|
||||
|
||||
call_args = {
|
||||
'name': name,
|
||||
@ -792,6 +813,8 @@ class Instance(BuiltInstance):
|
||||
"create %(count)d instances.") % {'count': replica_count})
|
||||
multi_replica = slave_of_id and replica_count and replica_count > 1
|
||||
instance_count = replica_count if multi_replica else 1
|
||||
if locality:
|
||||
call_args['locality'] = locality
|
||||
|
||||
if not nics:
|
||||
nics = []
|
||||
@ -889,10 +912,11 @@ class Instance(BuiltInstance):
|
||||
datastore_version.manager, datastore_version.packages,
|
||||
volume_size, backup_id, availability_zone, root_password,
|
||||
nics, overrides, slave_of_id, cluster_config,
|
||||
volume_type=volume_type, modules=module_list)
|
||||
volume_type=volume_type, modules=module_list,
|
||||
locality=locality)
|
||||
|
||||
return SimpleInstance(context, db_info, service_status,
|
||||
root_password)
|
||||
root_password, locality=locality)
|
||||
|
||||
with StartNotification(context, **call_args):
|
||||
return run_with_quotas(context.tenant, deltas, _create_resources)
|
||||
|
@ -196,8 +196,8 @@ class InstanceController(wsgi.Controller):
|
||||
LOG.debug("req : '%s'\n\n", req)
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
server = models.load_instance_with_guest(models.DetailInstance,
|
||||
context, id)
|
||||
server = models.load_instance_with_info(models.DetailInstance,
|
||||
context, id)
|
||||
return wsgi.Result(views.InstanceDetailView(server,
|
||||
req=req).data(), 200)
|
||||
|
||||
@ -275,6 +275,21 @@ class InstanceController(wsgi.Controller):
|
||||
body['instance'].get('slave_of'))
|
||||
replica_count = body['instance'].get('replica_count')
|
||||
modules = body['instance'].get('modules')
|
||||
locality = body['instance'].get('locality')
|
||||
if locality:
|
||||
locality_domain = ['affinity', 'anti-affinity']
|
||||
locality_domain_msg = ("Invalid locality '%s'. "
|
||||
"Must be one of ['%s']" %
|
||||
(locality,
|
||||
"', '".join(locality_domain)))
|
||||
if locality not in locality_domain:
|
||||
raise exception.BadRequest(msg=locality_domain_msg)
|
||||
if slave_of_id:
|
||||
dupe_locality_msg = (
|
||||
'Cannot specify locality when adding replicas to existing '
|
||||
'master.')
|
||||
raise exception.BadRequest(msg=dupe_locality_msg)
|
||||
|
||||
instance = models.Instance.create(context, name, flavor_id,
|
||||
image_id, databases, users,
|
||||
datastore, datastore_version,
|
||||
@ -283,7 +298,8 @@ class InstanceController(wsgi.Controller):
|
||||
configuration, slave_of_id,
|
||||
replica_count=replica_count,
|
||||
volume_type=volume_type,
|
||||
modules=modules)
|
||||
modules=modules,
|
||||
locality=locality)
|
||||
|
||||
view = views.InstanceDetailView(instance, req=req)
|
||||
return wsgi.Result(view.data(), 200)
|
||||
|
@ -99,6 +99,9 @@ class InstanceDetailView(InstanceView):
|
||||
result['instance']['configuration'] = (self.
|
||||
_build_configuration_info())
|
||||
|
||||
if self.instance.locality:
|
||||
result['instance']['locality'] = self.instance.locality
|
||||
|
||||
if (isinstance(self.instance, models.DetailInstance) and
|
||||
self.instance.volume_used):
|
||||
used = self.instance.volume_used
|
||||
|
@ -152,7 +152,7 @@ class API(object):
|
||||
availability_zone=None, root_password=None,
|
||||
nics=None, overrides=None, slave_of_id=None,
|
||||
cluster_config=None, volume_type=None,
|
||||
modules=None):
|
||||
modules=None, locality=None):
|
||||
|
||||
LOG.debug("Making async call to create instance %s " % instance_id)
|
||||
self._cast("create_instance", self.version_cap,
|
||||
@ -172,7 +172,7 @@ class API(object):
|
||||
slave_of_id=slave_of_id,
|
||||
cluster_config=cluster_config,
|
||||
volume_type=volume_type,
|
||||
modules=modules)
|
||||
modules=modules, locality=locality)
|
||||
|
||||
def create_cluster(self, cluster_id):
|
||||
LOG.debug("Making async call to create cluster %s " % cluster_id)
|
||||
|
@ -28,6 +28,7 @@ from trove.common.i18n import _
|
||||
from trove.common.notification import DBaaSQuotas, EndNotification
|
||||
from trove.common import remote
|
||||
import trove.common.rpc.version as rpc_version
|
||||
from trove.common import server_group as srv_grp
|
||||
from trove.common.strategies.cluster import strategy
|
||||
import trove.extensions.mgmt.instances.models as mgmtmodels
|
||||
from trove.instance.tasks import InstanceTasks
|
||||
@ -288,6 +289,11 @@ class Manager(periodic_task.PeriodicTasks):
|
||||
replica_backup_created = False
|
||||
replicas = []
|
||||
|
||||
master_instance_tasks = BuiltInstanceTasks.load(context, slave_of_id)
|
||||
server_group = master_instance_tasks.server_group
|
||||
scheduler_hints = srv_grp.ServerGroup.convert_to_hint(server_group)
|
||||
LOG.debug("Using scheduler hints for locality: %s" % scheduler_hints)
|
||||
|
||||
try:
|
||||
for replica_index in range(0, len(ids)):
|
||||
try:
|
||||
@ -306,7 +312,7 @@ class Manager(periodic_task.PeriodicTasks):
|
||||
packages, volume_size, replica_backup_id,
|
||||
availability_zone, root_passwords[replica_index],
|
||||
nics, overrides, None, snapshot, volume_type,
|
||||
modules)
|
||||
modules, scheduler_hints)
|
||||
replicas.append(instance_tasks)
|
||||
except Exception:
|
||||
# if it's the first replica, then we shouldn't continue
|
||||
@ -327,7 +333,7 @@ class Manager(periodic_task.PeriodicTasks):
|
||||
image_id, databases, users, datastore_manager,
|
||||
packages, volume_size, backup_id, availability_zone,
|
||||
root_password, nics, overrides, slave_of_id,
|
||||
cluster_config, volume_type, modules):
|
||||
cluster_config, volume_type, modules, locality):
|
||||
if slave_of_id:
|
||||
self._create_replication_slave(context, instance_id, name,
|
||||
flavor, image_id, databases, users,
|
||||
@ -341,12 +347,16 @@ class Manager(periodic_task.PeriodicTasks):
|
||||
raise AttributeError(_(
|
||||
"Cannot create multiple non-replica instances."))
|
||||
instance_tasks = FreshInstanceTasks.load(context, instance_id)
|
||||
|
||||
scheduler_hints = srv_grp.ServerGroup.build_scheduler_hint(
|
||||
context, locality, instance_id)
|
||||
instance_tasks.create_instance(flavor, image_id, databases, users,
|
||||
datastore_manager, packages,
|
||||
volume_size, backup_id,
|
||||
availability_zone, root_password,
|
||||
nics, overrides, cluster_config,
|
||||
None, volume_type, modules)
|
||||
None, volume_type, modules,
|
||||
scheduler_hints)
|
||||
timeout = (CONF.restore_usage_timeout if backup_id
|
||||
else CONF.usage_timeout)
|
||||
instance_tasks.wait_for_instance(timeout, flavor)
|
||||
@ -355,7 +365,7 @@ class Manager(periodic_task.PeriodicTasks):
|
||||
image_id, databases, users, datastore_manager,
|
||||
packages, volume_size, backup_id, availability_zone,
|
||||
root_password, nics, overrides, slave_of_id,
|
||||
cluster_config, volume_type, modules):
|
||||
cluster_config, volume_type, modules, locality):
|
||||
with EndNotification(context,
|
||||
instance_id=(instance_id[0]
|
||||
if type(instance_id) is list
|
||||
@ -365,7 +375,8 @@ class Manager(periodic_task.PeriodicTasks):
|
||||
datastore_manager, packages, volume_size,
|
||||
backup_id, availability_zone,
|
||||
root_password, nics, overrides, slave_of_id,
|
||||
cluster_config, volume_type, modules)
|
||||
cluster_config, volume_type, modules,
|
||||
locality)
|
||||
|
||||
def update_overrides(self, context, instance_id, overrides):
|
||||
instance_tasks = models.BuiltInstanceTasks.load(context, instance_id)
|
||||
|
@ -52,6 +52,7 @@ import trove.common.remote as remote
|
||||
from trove.common.remote import create_cinder_client
|
||||
from trove.common.remote import create_dns_client
|
||||
from trove.common.remote import create_heat_client
|
||||
from trove.common import server_group as srv_grp
|
||||
from trove.common.strategies.cluster import strategy
|
||||
from trove.common import template
|
||||
from trove.common import utils
|
||||
@ -367,7 +368,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
|
||||
datastore_manager, packages, volume_size,
|
||||
backup_id, availability_zone, root_password, nics,
|
||||
overrides, cluster_config, snapshot, volume_type,
|
||||
modules):
|
||||
modules, scheduler_hints):
|
||||
# It is the caller's responsibility to ensure that
|
||||
# FreshInstanceTasks.wait_for_instance is called after
|
||||
# create_instance to ensure that the proper usage event gets sent
|
||||
@ -413,7 +414,8 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
|
||||
volume_size,
|
||||
availability_zone,
|
||||
nics,
|
||||
files)
|
||||
files,
|
||||
scheduler_hints)
|
||||
else:
|
||||
volume_info = self._create_server_volume_individually(
|
||||
flavor['id'],
|
||||
@ -424,7 +426,8 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
|
||||
availability_zone,
|
||||
nics,
|
||||
files,
|
||||
cinder_volume_type)
|
||||
cinder_volume_type,
|
||||
scheduler_hints)
|
||||
|
||||
config = self._render_config(flavor)
|
||||
|
||||
@ -626,7 +629,8 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
|
||||
|
||||
def _create_server_volume(self, flavor_id, image_id, security_groups,
|
||||
datastore_manager, volume_size,
|
||||
availability_zone, nics, files):
|
||||
availability_zone, nics, files,
|
||||
scheduler_hints):
|
||||
LOG.debug("Begin _create_server_volume for id: %s" % self.id)
|
||||
try:
|
||||
userdata = self._prepare_userdata(datastore_manager)
|
||||
@ -642,7 +646,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
|
||||
security_groups=security_groups,
|
||||
availability_zone=availability_zone,
|
||||
nics=nics, config_drive=config_drive,
|
||||
userdata=userdata)
|
||||
userdata=userdata, scheduler_hints=scheduler_hints)
|
||||
server_dict = server._info
|
||||
LOG.debug("Created new compute instance %(server_id)s "
|
||||
"for id: %(id)s\nServer response: %(response)s" %
|
||||
@ -778,7 +782,8 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
|
||||
def _create_server_volume_individually(self, flavor_id, image_id,
|
||||
security_groups, datastore_manager,
|
||||
volume_size, availability_zone,
|
||||
nics, files, volume_type):
|
||||
nics, files, volume_type,
|
||||
scheduler_hints):
|
||||
LOG.debug("Begin _create_server_volume_individually for id: %s" %
|
||||
self.id)
|
||||
server = None
|
||||
@ -790,7 +795,8 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
|
||||
server = self._create_server(flavor_id, image_id, security_groups,
|
||||
datastore_manager,
|
||||
block_device_mapping,
|
||||
availability_zone, nics, files)
|
||||
availability_zone, nics, files,
|
||||
scheduler_hints)
|
||||
server_id = server.id
|
||||
# Save server ID.
|
||||
self.update_db(compute_instance_id=server_id)
|
||||
@ -904,7 +910,8 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
|
||||
|
||||
def _create_server(self, flavor_id, image_id, security_groups,
|
||||
datastore_manager, block_device_mapping,
|
||||
availability_zone, nics, files={}):
|
||||
availability_zone, nics, files={},
|
||||
scheduler_hints=None):
|
||||
userdata = self._prepare_userdata(datastore_manager)
|
||||
name = self.hostname or self.name
|
||||
bdmap = block_device_mapping
|
||||
@ -914,7 +921,7 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
|
||||
name, image_id, flavor_id, files=files, userdata=userdata,
|
||||
security_groups=security_groups, block_device_mapping=bdmap,
|
||||
availability_zone=availability_zone, nics=nics,
|
||||
config_drive=config_drive)
|
||||
config_drive=config_drive, scheduler_hints=scheduler_hints)
|
||||
LOG.debug("Created new compute instance %(server_id)s "
|
||||
"for instance %(id)s" %
|
||||
{'server_id': server.id, 'id': self.id})
|
||||
@ -1073,6 +1080,11 @@ class BuiltInstanceTasks(BuiltInstance, NotifyMixin, ConfigurationMixin):
|
||||
except Exception as ex:
|
||||
LOG.exception(_("Error during dns entry of instance %(id)s: "
|
||||
"%(ex)s") % {'id': self.db_info.id, 'ex': ex})
|
||||
try:
|
||||
srv_grp.ServerGroup.delete(self.context, self.server_group)
|
||||
except Exception:
|
||||
LOG.exception(_("Error during delete server group for %s")
|
||||
% self.id)
|
||||
|
||||
# Poll until the server is gone.
|
||||
def server_is_finished():
|
||||
|
@ -268,7 +268,8 @@ class FakeServers(object):
|
||||
|
||||
def create(self, name, image_id, flavor_ref, files=None, userdata=None,
|
||||
block_device_mapping=None, volume=None, security_groups=None,
|
||||
availability_zone=None, nics=None, config_drive=False):
|
||||
availability_zone=None, nics=None, config_drive=False,
|
||||
scheduler_hints=None):
|
||||
id = "FAKE_%s" % uuid.uuid4()
|
||||
if volume:
|
||||
volume = self.volumes.create(volume['size'], volume['name'],
|
||||
@ -794,6 +795,43 @@ class FakeSecurityGroupRules(object):
|
||||
del self.securityGroupRules[id]
|
||||
|
||||
|
||||
class FakeServerGroup(object):
|
||||
|
||||
def __init__(self, name=None, policies=None, context=None):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.id = "FAKE_SRVGRP_%s" % uuid.uuid4()
|
||||
self.policies = policies or {}
|
||||
|
||||
def get_id(self):
|
||||
return self.id
|
||||
|
||||
def data(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'policies': self.policies
|
||||
}
|
||||
|
||||
|
||||
class FakeServerGroups(object):
|
||||
|
||||
def __init__(self, context=None):
|
||||
self.context = context
|
||||
self.server_groups = {}
|
||||
|
||||
def create(self, name=None, policies=None):
|
||||
server_group = FakeServerGroup(name, policies, context=self.context)
|
||||
self.server_groups[server_group.get_id()] = server_group
|
||||
return server_group
|
||||
|
||||
def delete(self, group_id):
|
||||
pass
|
||||
|
||||
def list(self):
|
||||
return self.server_groups
|
||||
|
||||
|
||||
class FakeClient(object):
|
||||
|
||||
def __init__(self, context):
|
||||
@ -808,6 +846,7 @@ class FakeClient(object):
|
||||
self.rdservers = FakeRdServers(self.servers)
|
||||
self.security_groups = FakeSecurityGroups(context)
|
||||
self.security_group_rules = FakeSecurityGroupRules(context)
|
||||
self.server_groups = FakeServerGroups(context)
|
||||
|
||||
def get_server_volumes(self, server_id):
|
||||
return self.servers.get_server_volumes(server_id)
|
||||
|
@ -44,10 +44,15 @@ class ReplicationGroup(TestGroup):
|
||||
|
||||
@test(depends_on=[add_data_for_replication])
|
||||
def verify_data_for_replication(self):
|
||||
"""Verify data exists on master."""
|
||||
"""Verify initial data exists on master."""
|
||||
self.test_runner.run_verify_data_for_replication()
|
||||
|
||||
@test(runs_after=[verify_data_for_replication])
|
||||
def create_non_affinity_master(self):
|
||||
"""Test creating a non-affinity master."""
|
||||
self.test_runner.run_create_non_affinity_master()
|
||||
|
||||
@test(runs_after=[create_non_affinity_master])
|
||||
def create_single_replica(self):
|
||||
"""Test creating a single replica."""
|
||||
self.test_runner.run_create_single_replica()
|
||||
@ -63,18 +68,50 @@ class ReplicationGroup(TestGroup):
|
||||
self.test_runner.run_verify_replica_data_after_single()
|
||||
|
||||
@test(runs_after=[verify_replica_data_after_single])
|
||||
def wait_for_non_affinity_master(self):
|
||||
"""Wait for non-affinity master to complete."""
|
||||
self.test_runner.run_wait_for_non_affinity_master()
|
||||
|
||||
@test(runs_after=[wait_for_non_affinity_master])
|
||||
def create_non_affinity_replica(self):
|
||||
"""Test creating a non-affinity replica."""
|
||||
self.test_runner.run_create_non_affinity_replica()
|
||||
|
||||
@test(runs_after=[create_non_affinity_replica])
|
||||
def create_multiple_replicas(self):
|
||||
"""Test creating multiple replicas."""
|
||||
self.test_runner.run_create_multiple_replicas()
|
||||
|
||||
@test(depends_on=[create_single_replica, create_multiple_replicas])
|
||||
@test(runs_after=[create_multiple_replicas])
|
||||
def wait_for_non_affinity_replica_fail(self):
|
||||
"""Wait for non-affinity replica to fail."""
|
||||
self.test_runner.run_wait_for_non_affinity_replica_fail()
|
||||
|
||||
@test(runs_after=[wait_for_non_affinity_replica_fail])
|
||||
def delete_non_affinity_repl(self):
|
||||
"""Test deleting non-affinity replica."""
|
||||
self.test_runner.run_delete_non_affinity_repl()
|
||||
|
||||
@test(runs_after=[delete_non_affinity_repl])
|
||||
def delete_non_affinity_master(self):
|
||||
"""Test deleting non-affinity master."""
|
||||
self.test_runner.run_delete_non_affinity_master()
|
||||
|
||||
@test(depends_on=[create_single_replica, create_multiple_replicas],
|
||||
runs_after=[delete_non_affinity_master])
|
||||
def verify_replica_data_orig(self):
|
||||
"""Verify original data was transferred to replicas."""
|
||||
self.test_runner.run_verify_replica_data_orig()
|
||||
|
||||
@test(depends_on=[create_single_replica, create_multiple_replicas],
|
||||
runs_after=[verify_replica_data_orig])
|
||||
def add_data_to_replicate(self):
|
||||
"""Add data to master to verify replication."""
|
||||
"""Add new data to master to verify replication."""
|
||||
self.test_runner.run_add_data_to_replicate()
|
||||
|
||||
@test(depends_on=[add_data_to_replicate])
|
||||
def verify_data_to_replicate(self):
|
||||
"""Verify data exists on master."""
|
||||
"""Verify new data exists on master."""
|
||||
self.test_runner.run_verify_data_to_replicate()
|
||||
|
||||
@test(depends_on=[create_single_replica, create_multiple_replicas,
|
||||
@ -87,13 +124,6 @@ class ReplicationGroup(TestGroup):
|
||||
@test(depends_on=[create_single_replica, create_multiple_replicas,
|
||||
add_data_to_replicate],
|
||||
runs_after=[wait_for_data_to_replicate])
|
||||
def verify_replica_data_orig(self):
|
||||
"""Verify original data was transferred to replicas."""
|
||||
self.test_runner.run_verify_replica_data_orig()
|
||||
|
||||
@test(depends_on=[create_single_replica, create_multiple_replicas,
|
||||
add_data_to_replicate],
|
||||
runs_after=[verify_replica_data_orig])
|
||||
def verify_replica_data_new(self):
|
||||
"""Verify new data was transferred to replicas."""
|
||||
self.test_runner.run_verify_replica_data_new()
|
||||
@ -128,8 +158,14 @@ class ReplicationGroup(TestGroup):
|
||||
"""Test promoting a replica to replica source (master)."""
|
||||
self.test_runner.run_promote_to_replica_source()
|
||||
|
||||
@test(depends_on=[promote_to_replica_source])
|
||||
def verify_replica_data_new_master(self):
|
||||
"""Verify data is still on new master."""
|
||||
self.test_runner.run_verify_replica_data_new_master()
|
||||
|
||||
@test(depends_on=[create_single_replica, create_multiple_replicas,
|
||||
promote_to_replica_source])
|
||||
promote_to_replica_source],
|
||||
runs_after=[verify_replica_data_new_master])
|
||||
def add_data_to_replicate2(self):
|
||||
"""Add data to new master to verify replication."""
|
||||
self.test_runner.run_add_data_to_replicate2()
|
||||
|
@ -45,7 +45,8 @@ class InstanceCreateRunner(TestRunner):
|
||||
instance_info = self.assert_instance_create(
|
||||
name, flavor, trove_volume_size, [], [], None, None,
|
||||
CONFIG.dbaas_datastore, CONFIG.dbaas_datastore_version,
|
||||
expected_states, expected_http_code, create_helper_user=True)
|
||||
expected_states, expected_http_code, create_helper_user=True,
|
||||
locality='affinity')
|
||||
|
||||
# Update the shared instance info.
|
||||
self.instance_info.id = instance_info.id
|
||||
@ -57,6 +58,8 @@ class InstanceCreateRunner(TestRunner):
|
||||
instance_info.dbaas_datastore_version)
|
||||
self.instance_info.dbaas_flavor_href = instance_info.dbaas_flavor_href
|
||||
self.instance_info.volume = instance_info.volume
|
||||
self.instance_info.srv_grp_id = self.assert_server_group_exists(
|
||||
self.instance_info.id)
|
||||
|
||||
def run_initial_configuration_create(self, expected_http_code=200):
|
||||
dynamic_config = self.test_helper.get_dynamic_group()
|
||||
@ -123,18 +126,18 @@ class InstanceCreateRunner(TestRunner):
|
||||
return self.auth_client.find_flavor_self_href(flavor)
|
||||
|
||||
def assert_instance_create(
|
||||
self, name, flavor, trove_volume_size,
|
||||
database_definitions, user_definitions,
|
||||
self, name, flavor, trove_volume_size,
|
||||
database_definitions, user_definitions,
|
||||
configuration_id, root_password, datastore, datastore_version,
|
||||
expected_states, expected_http_code, create_helper_user=False):
|
||||
expected_states, expected_http_code, create_helper_user=False,
|
||||
locality=None):
|
||||
"""This assert method executes a 'create' call and verifies the server
|
||||
response. It neither waits for the instance to become available
|
||||
nor it performs any other validations itself.
|
||||
It has been designed this way to increase test granularity
|
||||
(other tests may run while the instance is building) and also to allow
|
||||
its reuse in other runners .
|
||||
its reuse in other runners.
|
||||
"""
|
||||
|
||||
databases = database_definitions
|
||||
users = [{'name': item['name'], 'password': item['password']}
|
||||
for item in user_definitions]
|
||||
@ -199,7 +202,8 @@ class InstanceCreateRunner(TestRunner):
|
||||
configuration=configuration_id,
|
||||
availability_zone="nova",
|
||||
datastore=instance_info.dbaas_datastore,
|
||||
datastore_version=instance_info.dbaas_datastore_version)
|
||||
datastore_version=instance_info.dbaas_datastore_version,
|
||||
locality=locality)
|
||||
self.assert_instance_action(
|
||||
instance.id, expected_states[0:1], expected_http_code)
|
||||
|
||||
@ -227,6 +231,9 @@ class InstanceCreateRunner(TestRunner):
|
||||
instance._info['datastore']['version'],
|
||||
"Unexpected instance datastore version")
|
||||
self.assert_configuration_group(instance_info.id, configuration_id)
|
||||
if locality:
|
||||
self.assert_equal(locality, instance._info['locality'],
|
||||
"Unexpected locality")
|
||||
|
||||
return instance_info
|
||||
|
||||
|
@ -34,6 +34,7 @@ class InstanceDeleteRunner(TestRunner):
|
||||
|
||||
self.assert_instance_delete(self.instance_info.id, expected_states,
|
||||
expected_http_code)
|
||||
self.assert_server_group_gone(self.instance_info.srv_grp_id)
|
||||
|
||||
def assert_instance_delete(self, instance_id, expected_states,
|
||||
expected_http_code):
|
||||
|
@ -32,6 +32,10 @@ class ReplicationRunner(TestRunner):
|
||||
self.replica_1_host = None
|
||||
self.master_backup_count = None
|
||||
self.used_data_sets = set()
|
||||
self.non_affinity_master_id = None
|
||||
self.non_affinity_srv_grp_id = None
|
||||
self.non_affinity_repl_id = None
|
||||
self.locality = 'affinity'
|
||||
|
||||
def run_add_data_for_replication(self, data_type=DataType.small):
|
||||
self.assert_add_replication_data(data_type, self.master_host)
|
||||
@ -55,6 +59,16 @@ class ReplicationRunner(TestRunner):
|
||||
"""
|
||||
self.test_helper.verify_data(data_type, host)
|
||||
|
||||
def run_create_non_affinity_master(self, expected_http_code=200):
|
||||
self.non_affinity_master_id = self.auth_client.instances.create(
|
||||
self.instance_info.name + 'non-affinity',
|
||||
self.instance_info.dbaas_flavor_href,
|
||||
self.instance_info.volume,
|
||||
datastore=self.instance_info.dbaas_datastore,
|
||||
datastore_version=self.instance_info.dbaas_datastore_version,
|
||||
locality='anti-affinity').id
|
||||
self.assert_client_code(expected_http_code)
|
||||
|
||||
def run_create_single_replica(self, expected_states=['BUILD', 'ACTIVE'],
|
||||
expected_http_code=200):
|
||||
master_id = self.instance_info.id
|
||||
@ -81,6 +95,7 @@ class ReplicationRunner(TestRunner):
|
||||
expected_http_code)
|
||||
self._assert_is_master(master_id, [replica_id])
|
||||
self._assert_is_replica(replica_id, master_id)
|
||||
self._assert_locality(master_id)
|
||||
return replica_id
|
||||
|
||||
def _assert_is_master(self, instance_id, replica_ids):
|
||||
@ -103,12 +118,75 @@ class ReplicationRunner(TestRunner):
|
||||
'Unexpected replication master ID')
|
||||
self._validate_replica(instance_id)
|
||||
|
||||
def _assert_locality(self, instance_id):
|
||||
replica_ids = self._get_replica_set(instance_id)
|
||||
instance = self.get_instance(instance_id)
|
||||
self.assert_equal(self.locality, instance.locality,
|
||||
"Unexpected locality for instance '%s'" %
|
||||
instance_id)
|
||||
for replica_id in replica_ids:
|
||||
replica = self.get_instance(replica_id)
|
||||
self.assert_equal(self.locality, replica.locality,
|
||||
"Unexpected locality for instance '%s'" %
|
||||
replica_id)
|
||||
|
||||
def run_wait_for_non_affinity_master(self,
|
||||
expected_states=['BUILD', 'ACTIVE']):
|
||||
self._assert_instance_states(self.non_affinity_master_id,
|
||||
expected_states)
|
||||
self.non_affinity_srv_grp_id = self.assert_server_group_exists(
|
||||
self.non_affinity_master_id)
|
||||
|
||||
def run_create_non_affinity_replica(self, expected_http_code=200):
|
||||
self.non_affinity_repl_id = self.auth_client.instances.create(
|
||||
self.instance_info.name + 'non-affinity-repl',
|
||||
self.instance_info.dbaas_flavor_href,
|
||||
self.instance_info.volume,
|
||||
datastore=self.instance_info.dbaas_datastore,
|
||||
datastore_version=self.instance_info.dbaas_datastore_version,
|
||||
replica_of=self.non_affinity_master_id,
|
||||
replica_count=1).id
|
||||
self.assert_client_code(expected_http_code)
|
||||
|
||||
def run_create_multiple_replicas(self, expected_states=['BUILD', 'ACTIVE'],
|
||||
expected_http_code=200):
|
||||
master_id = self.instance_info.id
|
||||
self.replica_2_id = self.assert_replica_create(
|
||||
master_id, 'replica2', 2, expected_states, expected_http_code)
|
||||
|
||||
def run_wait_for_non_affinity_replica_fail(
|
||||
self, expected_states=['BUILD', 'FAILED']):
|
||||
self._assert_instance_states(self.non_affinity_repl_id,
|
||||
expected_states,
|
||||
fast_fail_status=['ACTIVE'])
|
||||
|
||||
def run_delete_non_affinity_repl(self,
|
||||
expected_last_state=['SHUTDOWN'],
|
||||
expected_http_code=202):
|
||||
self.assert_delete_instances(
|
||||
self.non_affinity_repl_id,
|
||||
expected_last_state=expected_last_state,
|
||||
expected_http_code=expected_http_code)
|
||||
|
||||
def assert_delete_instances(
|
||||
self, instance_ids, expected_last_state, expected_http_code):
|
||||
instance_ids = (instance_ids if utils.is_collection(instance_ids)
|
||||
else [instance_ids])
|
||||
for instance_id in instance_ids:
|
||||
self.auth_client.instances.delete(instance_id)
|
||||
self.assert_client_code(expected_http_code)
|
||||
|
||||
self.assert_all_gone(instance_ids, expected_last_state)
|
||||
|
||||
def run_delete_non_affinity_master(self,
|
||||
expected_last_state=['SHUTDOWN'],
|
||||
expected_http_code=202):
|
||||
self.assert_delete_instances(
|
||||
self.non_affinity_master_id,
|
||||
expected_last_state=expected_last_state,
|
||||
expected_http_code=expected_http_code)
|
||||
self.assert_server_group_gone(self.non_affinity_srv_grp_id)
|
||||
|
||||
def run_add_data_to_replicate(self):
|
||||
self.assert_add_replication_data(DataType.tiny, self.master_host)
|
||||
|
||||
@ -191,6 +269,12 @@ class ReplicationRunner(TestRunner):
|
||||
self.assert_instance_action(new_master_id, expected_states,
|
||||
expected_http_code)
|
||||
|
||||
def run_verify_replica_data_new_master(self):
|
||||
self.assert_verify_replication_data(
|
||||
DataType.small, self.replica_1_host)
|
||||
self.assert_verify_replication_data(
|
||||
DataType.tiny, self.replica_1_host)
|
||||
|
||||
def run_add_data_to_replicate2(self):
|
||||
self.assert_add_replication_data(DataType.tiny2, self.replica_1_host)
|
||||
|
||||
@ -266,16 +350,6 @@ class ReplicationRunner(TestRunner):
|
||||
self.replica_1_id, expected_last_state=expected_last_state,
|
||||
expected_http_code=expected_http_code)
|
||||
|
||||
def assert_delete_instances(
|
||||
self, instance_ids, expected_last_state, expected_http_code):
|
||||
instance_ids = (instance_ids if utils.is_collection(instance_ids)
|
||||
else [instance_ids])
|
||||
for instance_id in instance_ids:
|
||||
self.auth_client.instances.delete(instance_id)
|
||||
self.assert_client_code(expected_http_code)
|
||||
|
||||
self.assert_all_gone(instance_ids, expected_last_state)
|
||||
|
||||
def run_delete_all_replicas(self, expected_last_state=['SHUTDOWN'],
|
||||
expected_http_code=202):
|
||||
self.assert_delete_all_replicas(
|
||||
|
@ -30,6 +30,7 @@ from trove.common.utils import poll_until, build_polling_task
|
||||
from trove.tests.config import CONFIG
|
||||
from trove.tests.util.check import AttrCheck
|
||||
from trove.tests.util import create_dbaas_client
|
||||
from trove.tests.util import create_nova_client
|
||||
from trove.tests.util.users import Requirements
|
||||
|
||||
CONF = cfg.CONF
|
||||
@ -213,7 +214,9 @@ class TestRunner(object):
|
||||
self._unauth_client = None
|
||||
self._admin_client = None
|
||||
self._swift_client = None
|
||||
self._nova_client = None
|
||||
self._test_helper = None
|
||||
self._servers = {}
|
||||
|
||||
@classmethod
|
||||
def fail(cls, message):
|
||||
@ -338,6 +341,12 @@ class TestRunner(object):
|
||||
auth_version='2.0',
|
||||
os_options=os_options)
|
||||
|
||||
@property
|
||||
def nova_client(self):
|
||||
if not self._nova_client:
|
||||
self._nova_client = create_nova_client(self.instance_info.user)
|
||||
return self._nova_client
|
||||
|
||||
def get_client_tenant(self, client):
|
||||
tenant_name = client.real_client.client.tenant
|
||||
service_url = client.real_client.client.service_url
|
||||
@ -518,6 +527,50 @@ class TestRunner(object):
|
||||
% (instance_id, instance.status))
|
||||
return instance.status == status
|
||||
|
||||
def get_server(self, instance_id):
|
||||
server = None
|
||||
if instance_id in self._servers:
|
||||
server = self._servers[instance_id]
|
||||
else:
|
||||
instance = self.get_instance(instance_id)
|
||||
self.report.log("Getting server for instance: %s" % instance)
|
||||
for nova_server in self.nova_client.servers.list():
|
||||
if str(nova_server.name) == instance.name:
|
||||
server = nova_server
|
||||
break
|
||||
if server:
|
||||
self._servers[instance_id] = server
|
||||
return server
|
||||
|
||||
def assert_server_group_exists(self, instance_id):
|
||||
"""Check that the Nova instance associated with instance_id
|
||||
belongs to a server group, and return the id.
|
||||
"""
|
||||
server = self.get_server(instance_id)
|
||||
self.assert_is_not_none(server, "Could not find Nova server for '%s'" %
|
||||
instance_id)
|
||||
server_group = None
|
||||
server_groups = self.nova_client.server_groups.list()
|
||||
for sg in server_groups:
|
||||
if server.id in sg.members:
|
||||
server_group = sg
|
||||
break
|
||||
if server_group is None:
|
||||
self.fail("Could not find server group for Nova instance %s" %
|
||||
server.id)
|
||||
return server_group.id
|
||||
|
||||
def assert_server_group_gone(self, srv_grp_id):
|
||||
"""Ensure that the server group is no longer present."""
|
||||
server_group = None
|
||||
server_groups = self.nova_client.server_groups.list()
|
||||
for sg in server_groups:
|
||||
if sg.id == srv_grp_id:
|
||||
server_group = sg
|
||||
break
|
||||
if server_group:
|
||||
self.fail("Found left-over server group: %s" % server_group)
|
||||
|
||||
def get_instance(self, instance_id):
|
||||
return self.auth_client.instances.get(instance_id)
|
||||
|
||||
|
111
trove/tests/unittests/common/test_server_group.py
Normal file
111
trove/tests/unittests/common/test_server_group.py
Normal file
@ -0,0 +1,111 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
import copy
|
||||
from mock import Mock, patch
|
||||
|
||||
from trove.common import server_group as srv_grp
|
||||
from trove.tests.unittests import trove_testtools
|
||||
|
||||
|
||||
class TestServerGroup(trove_testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestServerGroup, self).setUp()
|
||||
self.ServerGroup = srv_grp.ServerGroup()
|
||||
self.context = trove_testtools.TroveTestContext(self)
|
||||
self.sg_id = 'sg-1234'
|
||||
self.locality = 'affinity'
|
||||
self.expected_hints = {'group': self.sg_id}
|
||||
self.server_group = Mock()
|
||||
self.server_group.id = self.sg_id
|
||||
self.server_group.policies = [self.locality]
|
||||
self.server_group.members = ['id-1', 'id-2']
|
||||
self.empty_server_group = copy.copy(self.server_group)
|
||||
self.empty_server_group.members = ['id-1']
|
||||
|
||||
@patch.object(srv_grp, 'create_nova_client')
|
||||
def test_create(self, mock_client):
|
||||
mock_create = Mock(return_value=self.server_group)
|
||||
mock_client.return_value.server_groups.create = mock_create
|
||||
server_group = self.ServerGroup.create(
|
||||
self.context, self.locality, "name_suffix")
|
||||
mock_create.assert_called_with(name="locality_name_suffix",
|
||||
policies=[self.locality])
|
||||
self.assertEqual(self.server_group, server_group)
|
||||
|
||||
@patch.object(srv_grp, 'create_nova_client')
|
||||
def test_delete(self, mock_client):
|
||||
mock_delete = Mock()
|
||||
mock_client.return_value.server_groups.delete = mock_delete
|
||||
self.ServerGroup.delete(self.context, self.empty_server_group)
|
||||
mock_delete.assert_called_with(self.sg_id)
|
||||
|
||||
@patch.object(srv_grp, 'create_nova_client')
|
||||
def test_delete_non_empty(self, mock_client):
|
||||
mock_delete = Mock()
|
||||
mock_client.return_value.server_groups.delete = mock_delete
|
||||
srv_grp.ServerGroup.delete(self.context, self.server_group)
|
||||
mock_delete.assert_not_called()
|
||||
|
||||
@patch.object(srv_grp, 'create_nova_client')
|
||||
def test_delete_force(self, mock_client):
|
||||
mock_delete = Mock()
|
||||
mock_client.return_value.server_groups.delete = mock_delete
|
||||
self.ServerGroup.delete(self.context, self.server_group, force=True)
|
||||
mock_delete.assert_called_with(self.sg_id)
|
||||
|
||||
def test_convert_to_hint(self):
|
||||
hint = srv_grp.ServerGroup.convert_to_hint(self.server_group)
|
||||
self.assertEqual(self.expected_hints, hint, "Unexpected hint")
|
||||
|
||||
def test_convert_to_hints(self):
|
||||
hints = {'hint': 'myhint'}
|
||||
hints = srv_grp.ServerGroup.convert_to_hint(self.server_group, hints)
|
||||
self.expected_hints.update(hints)
|
||||
self.assertEqual(self.expected_hints, hints, "Unexpected hints")
|
||||
|
||||
def test_convert_to_hint_none(self):
|
||||
self.assertIsNone(srv_grp.ServerGroup.convert_to_hint(None))
|
||||
|
||||
@patch.object(srv_grp, 'create_nova_client')
|
||||
def test_build_scheduler_hint(self, mock_client):
|
||||
mock_create = Mock(return_value=self.server_group)
|
||||
mock_client.return_value.server_groups.create = mock_create
|
||||
expected_hint = {'get_back': 'same_dict'}
|
||||
scheduler_hint = self.ServerGroup.build_scheduler_hint(
|
||||
self.context, expected_hint, "name_suffix")
|
||||
self.assertEqual(expected_hint, scheduler_hint, "Unexpected hint")
|
||||
|
||||
@patch.object(srv_grp, 'create_nova_client')
|
||||
def test_build_scheduler_hint_from_locality(self, mock_client):
|
||||
mock_create = Mock(return_value=self.server_group)
|
||||
mock_client.return_value.server_groups.create = mock_create
|
||||
expected_hint = {'group': 'sg-1234'}
|
||||
scheduler_hint = self.ServerGroup.build_scheduler_hint(
|
||||
self.context, self.locality, "name_suffix")
|
||||
self.assertEqual(expected_hint, scheduler_hint, "Unexpected hint")
|
||||
|
||||
def test_build_scheduler_hint_none(self):
|
||||
self.assertIsNone(srv_grp.ServerGroup.build_scheduler_hint(
|
||||
self.context, None, None))
|
||||
|
||||
def test_get_locality(self):
|
||||
locality = srv_grp.ServerGroup.get_locality(self.server_group)
|
||||
self.assertEqual(self.locality, locality, "Unexpected locality")
|
||||
|
||||
def test_get_locality_none(self):
|
||||
self.assertIsNone(srv_grp.ServerGroup.get_locality(None))
|
@ -27,6 +27,7 @@ class TestInstanceController(trove_testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestInstanceController, self).setUp()
|
||||
self.controller = InstanceController()
|
||||
self.locality = 'affinity'
|
||||
self.instance = {
|
||||
"instance": {
|
||||
"volume": {"size": "1"},
|
||||
@ -46,7 +47,8 @@ class TestInstanceController(trove_testtools.TestCase):
|
||||
{
|
||||
"name": "db2"
|
||||
}
|
||||
]
|
||||
],
|
||||
"locality": self.locality
|
||||
}
|
||||
}
|
||||
self.context = trove_testtools.TroveTestContext(self)
|
||||
@ -149,6 +151,20 @@ class TestInstanceController(trove_testtools.TestCase):
|
||||
self.assertIn("'$#$%^^' does not match '^.*[0-9a-zA-Z]+.*$'",
|
||||
errors[0].message)
|
||||
|
||||
def test_validate_create_invalid_locality(self):
|
||||
body = self.instance
|
||||
body['instance']['locality'] = "$%^"
|
||||
schema = self.controller.get_schema('create', body)
|
||||
validator = jsonschema.Draft4Validator(schema)
|
||||
self.assertFalse(validator.is_valid(body))
|
||||
errors = sorted(validator.iter_errors(body), key=lambda e: e.path)
|
||||
error_messages = [error.message for error in errors]
|
||||
error_paths = [error.path.pop() for error in errors]
|
||||
self.assertEqual(1, len(errors))
|
||||
self.assertIn("'$%^' does not match '^.*[0-9a-zA-Z]+.*$'",
|
||||
error_messages)
|
||||
self.assertIn("locality", error_paths)
|
||||
|
||||
def test_validate_restart(self):
|
||||
body = {"restart": {}}
|
||||
schema = self.controller.get_schema('action', body)
|
||||
|
@ -43,7 +43,8 @@ class SimpleInstanceTest(trove_testtools.TestCase):
|
||||
InstanceTasks.BUILDING, name="TestInstance")
|
||||
self.instance = SimpleInstance(
|
||||
None, db_info, InstanceServiceStatus(
|
||||
ServiceStatuses.BUILDING), ds_version=Mock(), ds=Mock())
|
||||
ServiceStatuses.BUILDING), ds_version=Mock(), ds=Mock(),
|
||||
locality='affinity')
|
||||
db_info.addresses = {"private": [{"addr": "123.123.123.123"}],
|
||||
"internal": [{"addr": "10.123.123.123"}],
|
||||
"public": [{"addr": "15.123.123.123"}]}
|
||||
@ -102,6 +103,9 @@ class SimpleInstanceTest(trove_testtools.TestCase):
|
||||
self.assertTrue('123.123.123.123' in ip)
|
||||
self.assertTrue('15.123.123.123' in ip)
|
||||
|
||||
def test_locality(self):
|
||||
self.assertEqual('affinity', self.instance.locality)
|
||||
|
||||
|
||||
class CreateInstanceTest(trove_testtools.TestCase):
|
||||
|
||||
@ -172,6 +176,7 @@ class CreateInstanceTest(trove_testtools.TestCase):
|
||||
self.check = backup_models.DBBackup.check_swift_object_exist
|
||||
backup_models.DBBackup.check_swift_object_exist = Mock(
|
||||
return_value=True)
|
||||
self.locality = 'affinity'
|
||||
super(CreateInstanceTest, self).setUp()
|
||||
|
||||
@patch.object(task_api.API, 'get_client', Mock(return_value=Mock()))
|
||||
@ -213,6 +218,19 @@ class CreateInstanceTest(trove_testtools.TestCase):
|
||||
self.az, self.nics, self.configuration)
|
||||
self.assertIsNotNone(instance)
|
||||
|
||||
def test_can_instantiate_with_locality(self):
|
||||
# make sure the backup will fit
|
||||
self.backup.size = 0.2
|
||||
self.backup.save()
|
||||
instance = models.Instance.create(
|
||||
self.context, self.name, self.flavor_id,
|
||||
self.image_id, self.databases, self.users,
|
||||
self.datastore, self.datastore_version,
|
||||
self.volume_size, self.backup_id,
|
||||
self.az, self.nics, self.configuration,
|
||||
locality=self.locality)
|
||||
self.assertIsNotNone(instance)
|
||||
|
||||
|
||||
class TestReplication(trove_testtools.TestCase):
|
||||
|
||||
|
@ -62,6 +62,7 @@ class InstanceDetailViewTest(trove_testtools.TestCase):
|
||||
self.instance.get_visible_ip_addresses = lambda: ["1.2.3.4"]
|
||||
self.instance.slave_of_id = None
|
||||
self.instance.slaves = []
|
||||
self.instance.locality = 'affinity'
|
||||
|
||||
def tearDown(self):
|
||||
super(InstanceDetailViewTest, self).tearDown()
|
||||
@ -90,3 +91,10 @@ class InstanceDetailViewTest(trove_testtools.TestCase):
|
||||
result['instance']['datastore']['version'])
|
||||
self.assertNotIn('hostname', result['instance'])
|
||||
self.assertEqual([self.ip], result['instance']['ip'])
|
||||
|
||||
def test_locality(self):
|
||||
self.instance.hostname = None
|
||||
view = InstanceDetailView(self.instance, Mock())
|
||||
result = view.data()
|
||||
self.assertEqual(self.instance.locality,
|
||||
result['instance']['locality'])
|
||||
|
@ -48,6 +48,25 @@ class ApiTest(trove_testtools.TestCase):
|
||||
self.api.client.prepare = Mock(return_value=self.call_context)
|
||||
self.call_context.cast = Mock()
|
||||
|
||||
@patch.object(task_api.API, '_transform_obj', Mock(return_value='flv-id'))
|
||||
def test_create_instance(self):
|
||||
flavor = Mock()
|
||||
self.api.create_instance(
|
||||
'inst-id', 'inst-name', flavor, 'img-id', {'name': 'db1'},
|
||||
{'name': 'usr1'}, 'mysql', None, 1, backup_id='bk-id',
|
||||
availability_zone='az', root_password='pwd', nics=['nic-id'],
|
||||
overrides={}, slave_of_id='slv-id', cluster_config={},
|
||||
volume_type='type', modules=['mod-id'], locality='affinity')
|
||||
self._verify_rpc_prepare_before_cast()
|
||||
self._verify_cast(
|
||||
'create_instance', availability_zone='az', backup_id='bk-id',
|
||||
cluster_config={}, databases={'name': 'db1'},
|
||||
datastore_manager='mysql', flavor='flv-id', image_id='img-id',
|
||||
instance_id='inst-id', locality='affinity', modules=['mod-id'],
|
||||
name='inst-name', nics=['nic-id'], overrides={}, packages=None,
|
||||
root_password='pwd', slave_of_id='slv-id', users={'name': 'usr1'},
|
||||
volume_size=1, volume_type='type')
|
||||
|
||||
def test_detach_replica(self):
|
||||
self.api.detach_replica('some-instance-id')
|
||||
|
||||
|
@ -15,14 +15,15 @@
|
||||
# under the License.
|
||||
|
||||
from mock import Mock, patch, PropertyMock
|
||||
from proboscis.asserts import assert_equal
|
||||
|
||||
from trove.backup.models import Backup
|
||||
from trove.common.exception import TroveError, ReplicationSlaveAttachError
|
||||
from trove.common import server_group as srv_grp
|
||||
from trove.instance.tasks import InstanceTasks
|
||||
from trove.taskmanager.manager import Manager
|
||||
from trove.taskmanager import models
|
||||
from trove.taskmanager import service
|
||||
from trove.common.exception import TroveError, ReplicationSlaveAttachError
|
||||
from proboscis.asserts import assert_equal
|
||||
from trove.tests.unittests import trove_testtools
|
||||
|
||||
|
||||
@ -189,7 +190,8 @@ class TestManager(trove_testtools.TestCase):
|
||||
self.context, 'some-inst-id')
|
||||
|
||||
@patch.object(Backup, 'delete')
|
||||
def test_create_replication_slave(self, mock_backup_delete):
|
||||
@patch.object(models.BuiltInstanceTasks, 'load')
|
||||
def test_create_replication_slave(self, mock_load, mock_backup_delete):
|
||||
mock_tasks = Mock()
|
||||
mock_snapshot = {'dataset': {'snapshot_id': 'test-id'}}
|
||||
mock_tasks.get_replication_master_snapshot = Mock(
|
||||
@ -203,7 +205,7 @@ class TestManager(trove_testtools.TestCase):
|
||||
'temp-backup-id', None,
|
||||
'some_password', None, Mock(),
|
||||
'some-master-id', None, None,
|
||||
None)
|
||||
None, None)
|
||||
mock_tasks.get_replication_master_snapshot.assert_called_with(
|
||||
self.context, 'some-master-id', mock_flavor, 'temp-backup-id',
|
||||
replica_number=1)
|
||||
@ -211,15 +213,16 @@ class TestManager(trove_testtools.TestCase):
|
||||
|
||||
@patch.object(models.FreshInstanceTasks, 'load')
|
||||
@patch.object(Backup, 'delete')
|
||||
@patch.object(models.BuiltInstanceTasks, 'load')
|
||||
@patch('trove.taskmanager.manager.LOG')
|
||||
def test_exception_create_replication_slave(self, mock_logging,
|
||||
def test_exception_create_replication_slave(self, mock_logging, mock_tasks,
|
||||
mock_delete, mock_load):
|
||||
mock_load.return_value.create_instance = Mock(side_effect=TroveError)
|
||||
self.assertRaises(TroveError, self.manager.create_instance,
|
||||
self.context, ['id1', 'id2'], Mock(), Mock(),
|
||||
Mock(), None, None, 'mysql', 'mysql-server', 2,
|
||||
'temp-backup-id', None, 'some_password', None,
|
||||
Mock(), 'some-master-id', None, None, None)
|
||||
Mock(), 'some-master-id', None, None, None, None)
|
||||
|
||||
def test_AttributeError_create_instance(self):
|
||||
self.assertRaisesRegexp(
|
||||
@ -227,20 +230,23 @@ class TestManager(trove_testtools.TestCase):
|
||||
self.manager.create_instance, self.context, ['id1', 'id2'],
|
||||
Mock(), Mock(), Mock(), None, None, 'mysql', 'mysql-server', 2,
|
||||
'temp-backup-id', None, 'some_password', None, Mock(), None, None,
|
||||
None, None)
|
||||
None, None, None)
|
||||
|
||||
def test_create_instance(self):
|
||||
mock_tasks = Mock()
|
||||
mock_flavor = Mock()
|
||||
mock_override = Mock()
|
||||
mock_csg = Mock()
|
||||
type(mock_csg.return_value).id = PropertyMock(
|
||||
return_value='sg-id')
|
||||
with patch.object(models.FreshInstanceTasks, 'load',
|
||||
return_value=mock_tasks):
|
||||
self.manager.create_instance(self.context, 'id1', 'inst1',
|
||||
mock_flavor, 'mysql-image-id', None,
|
||||
None, 'mysql', 'mysql-server', 2,
|
||||
'temp-backup-id', None, 'password',
|
||||
None, mock_override, None, None, None,
|
||||
None)
|
||||
with patch.object(srv_grp.ServerGroup, 'create', mock_csg):
|
||||
self.manager.create_instance(
|
||||
self.context, 'id1', 'inst1', mock_flavor,
|
||||
'mysql-image-id', None, None, 'mysql', 'mysql-server', 2,
|
||||
'temp-backup-id', None, 'password', None, mock_override,
|
||||
None, None, None, None, 'affinity')
|
||||
mock_tasks.create_instance.assert_called_with(mock_flavor,
|
||||
'mysql-image-id', None,
|
||||
None, 'mysql',
|
||||
@ -248,7 +254,8 @@ class TestManager(trove_testtools.TestCase):
|
||||
'temp-backup-id', None,
|
||||
'password', None,
|
||||
mock_override,
|
||||
None, None, None, None)
|
||||
None, None, None, None,
|
||||
{'group': 'sg-id'})
|
||||
mock_tasks.wait_for_instance.assert_called_with(36000, mock_flavor)
|
||||
|
||||
def test_create_cluster(self):
|
||||
|
@ -81,7 +81,8 @@ class fake_Server:
|
||||
class fake_ServerManager:
|
||||
def create(self, name, image_id, flavor_id, files, userdata,
|
||||
security_groups, block_device_mapping, availability_zone=None,
|
||||
nics=None, config_drive=False):
|
||||
nics=None, config_drive=False,
|
||||
scheduler_hints=None):
|
||||
server = fake_Server()
|
||||
server.id = "server_id"
|
||||
server.name = name
|
||||
@ -380,7 +381,7 @@ class FreshInstanceTasksTest(trove_testtools.TestCase):
|
||||
'Error creating security group for instance',
|
||||
self.freshinstancetasks.create_instance, mock_flavor,
|
||||
'mysql-image-id', None, None, 'mysql', 'mysql-server', 2,
|
||||
None, None, None, None, Mock(), None, None, None, None)
|
||||
None, None, None, None, Mock(), None, None, None, None, None)
|
||||
|
||||
@patch.object(BaseInstance, 'update_db')
|
||||
@patch.object(backup_models.Backup, 'get_by_id')
|
||||
@ -402,7 +403,7 @@ class FreshInstanceTasksTest(trove_testtools.TestCase):
|
||||
self.freshinstancetasks.create_instance, mock_flavor,
|
||||
'mysql-image-id', None, None, 'mysql', 'mysql-server',
|
||||
2, Mock(), None, 'root_password', None, Mock(), None, None, None,
|
||||
None)
|
||||
None, None)
|
||||
|
||||
@patch.object(BaseInstance, 'update_db')
|
||||
@patch.object(taskmanager_models.FreshInstanceTasks, '_create_dns_entry')
|
||||
@ -417,6 +418,8 @@ class FreshInstanceTasksTest(trove_testtools.TestCase):
|
||||
mock_guest_prepare,
|
||||
mock_build_volume_info,
|
||||
mock_create_secgroup,
|
||||
mock_create_server,
|
||||
mock_get_injected_files,
|
||||
*args):
|
||||
mock_flavor = {'id': 8, 'ram': 768, 'name': 'bigger_flavor'}
|
||||
config_content = {'config_contents': 'some junk'}
|
||||
@ -428,13 +431,18 @@ class FreshInstanceTasksTest(trove_testtools.TestCase):
|
||||
'mysql-server', 2,
|
||||
None, None, None, None,
|
||||
overrides, None, None,
|
||||
'volume_type', None)
|
||||
'volume_type', None,
|
||||
{'group': 'sg-id'})
|
||||
mock_create_secgroup.assert_called_with('mysql')
|
||||
mock_build_volume_info.assert_called_with('mysql', volume_size=2,
|
||||
volume_type='volume_type')
|
||||
mock_guest_prepare.assert_called_with(
|
||||
768, mock_build_volume_info(), 'mysql-server', None, None, None,
|
||||
config_content, None, overrides, None, None, None)
|
||||
mock_create_server.assert_called_with(
|
||||
8, 'mysql-image-id', mock_create_secgroup(),
|
||||
'mysql', mock_build_volume_info()['block_device'], None,
|
||||
None, mock_get_injected_files(), {'group': 'sg-id'})
|
||||
|
||||
@patch.object(trove.guestagent.api.API, 'attach_replication_slave')
|
||||
@patch.object(rpc, 'get_client')
|
||||
|
Loading…
x
Reference in New Issue
Block a user