Merge "vm moves for host notification"

This commit is contained in:
Zuul 2023-01-28 13:19:42 +00:00 committed by Gerrit Code Review
commit e8db24c637
22 changed files with 1390 additions and 145 deletions

View File

@ -0,0 +1,100 @@
# Copyright(c) 2022 Inspur
#
# 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.
"""The VM Move API extension."""
from http import HTTPStatus
from webob import exc
from masakari.api.openstack import common
from masakari.api.openstack import extensions
from masakari.api.openstack import wsgi
from masakari import exception
from masakari.ha import api as vmove_api
from masakari.policies import vmoves as vmove_policies
ALIAS = "vmoves"
class VMovesController(wsgi.Controller):
"""The VM move API controller for the Instance HA."""
def __init__(self):
self.api = vmove_api.VMoveAPI()
@extensions.expected_errors((HTTPStatus.BAD_REQUEST, HTTPStatus.FORBIDDEN,
HTTPStatus.NOT_FOUND))
def index(self, req, notification_id):
"""Returns a list of vmoves."""
context = req.environ['masakari.context']
context.can(vmove_policies.VMOVES % 'index')
try:
filters = {}
limit, marker = common.get_limit_and_marker(req)
sort_keys, sort_dirs = common.get_sort_params(req.params)
if 'status' in req.params:
filters['status'] = req.params['status']
if 'type' in req.params:
filters['type'] = req.params['type']
vmoves = self.api.get_all(context,
notification_id,
filters=filters,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
limit=limit,
marker=marker)
except exception.MarkerNotFound as ex:
raise exc.HTTPBadRequest(explanation=ex.format_message())
except exception.Invalid as e:
raise exc.HTTPBadRequest(explanation=e.format_message())
except exception.NotificationNotFound as ex:
raise exc.HTTPNotFound(explanation=ex.format_message())
return {'vmoves': vmoves}
@extensions.expected_errors((HTTPStatus.FORBIDDEN, HTTPStatus.NOT_FOUND))
def show(self, req, notification_id, id):
"""Shows the details of one vmove."""
context = req.environ['masakari.context']
context.can(vmove_policies.VMOVES % 'detail')
try:
vmove = self.api.get_vmove(context, notification_id, id)
except exception.VMoveNotFound as e:
raise exc.HTTPNotFound(explanation=e.format_message())
return {'vmove': vmove}
class VMoves(extensions.V1APIExtensionBase):
"""vmoves controller"""
name = "vmoves"
alias = ALIAS
version = 1
def get_resources(self):
parent = {'member_name': 'notification',
'collection_name': 'notifications'}
resources = [
extensions.ResourceExtension(
'vmoves', VMovesController(), parent=parent,
member_name='vmove')]
return resources
def get_controller_extensions(self):
return []

View File

@ -363,6 +363,84 @@ def notification_delete(context, notification_uuid):
return IMPL.notification_delete(context, notification_uuid)
# vmoves related db apis
def vmoves_get_all_by_filters(
context, filters=None, sort_keys=None, sort_dirs=None,
limit=None, marker=None):
"""Get all vm moves that match the filters.
:param context: context to query under
:param filters: filters for the query in the form of key/value
:param sort_keys: list of attributes by which results should be sorted,
paired with corresponding item in sort_dirs
:param sort_dirs: list of directions in which results should be sorted,
paired with corresponding item in sort_keys
:param limit: maximum number of items to return
:param marker: the last item of the previous page, used to determine the
next page of results to return
:returns: list of dictionary-like objects containing all vm moves
"""
return IMPL.vmoves_get_all_by_filters(context, filters=filters,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
limit=limit,
marker=marker)
def vmove_get_by_uuid(context, vmove_uuid):
"""Get one vm move information by uuid.
:param context: context to query under
:param uuid: uuid of the vm move
:returns: dictionary-like object containing one vm move
:raises: exception.VMoveNotFound if the vm move with given
'uuid' doesn't exist
"""
return IMPL.vmove_get_by_uuid(context, vmove_uuid)
def vmove_create(context, values):
"""Create one vm move.
:param context: context to query under
:param values: dictionary of the vm move attributes to create
:returns: dictionary-like object containing created one vm move
"""
return IMPL.vmove_create(context, values)
def vmove_update(context, uuid, values):
"""Update one vm move information in the database.
:param context: context to query under
:param uuid: uuid of the vm move to be updated
:param values: dictionary of the vm move attributes to be updated
:returns: dictionary-like object containing updated one vm move
:raises: exception.VMoveNotFound if the vm move with given
'uuid' doesn't exist
"""
return IMPL.vmove_update(context, uuid, values)
def vmove_delete(context, uuid):
"""Delete one vm move.
:param context: context to query under
:param uuid: uuid of the vm move to be delete
:raises exception.VMoveNotFound if the vm move not exist.
"""
return IMPL.vmove_delete(context, uuid)
def purge_deleted_rows(context, age_in_days, max_rows):
"""Purge the soft deleted rows.

View File

@ -618,6 +618,109 @@ def notification_delete(context, notification_uuid):
raise exception.NotificationNotFound(id=notification_uuid)
# db apis for vm moves
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@main_context_manager.reader
def vmoves_get_all_by_filters(
context, filters=None, sort_keys=None,
sort_dirs=None, limit=None, marker=None):
if limit == 0:
return []
sort_keys, sort_dirs = _process_sort_params(sort_keys,
sort_dirs)
filters = filters or {}
query = model_query(context, models.VMove)
if 'notification_uuid' in filters:
query = query.filter(models.VMove.notification_uuid == filters[
'notification_uuid'])
if 'type' in filters:
query = query.filter(models.VMove.type == filters[
'type'])
if 'status' in filters:
status = filters['status']
if isinstance(status, (list, tuple, set, frozenset)):
column_attr = getattr(models.VMove, 'status')
query = query.filter(column_attr.in_(status))
else:
query = query.filter(models.VMove.status == status)
marker_row = None
if marker is not None:
marker_row = model_query(context,
models.VMove
).filter_by(id=marker).first()
if not marker_row:
raise exception.MarkerNotFound(marker=marker)
try:
query = sqlalchemyutils.paginate_query(query, models.VMove,
limit,
sort_keys,
marker=marker_row,
sort_dirs=sort_dirs)
except db_exc.InvalidSortKey as err:
raise exception.InvalidSortKey(err)
return query.all()
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@main_context_manager.reader
def vmove_get_by_uuid(context, uuid):
return _vmove_get_by_uuid(context, uuid)
def _vmove_get_by_uuid(context, uuid):
query = model_query(context, models.VMove).filter_by(uuid=uuid)
result = query.first()
if not result:
raise exception.VMoveNotFound(id=uuid)
return result
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@main_context_manager.writer
def vmove_create(context, values):
vm_move = models.VMove()
vm_move.update(values)
vm_move.save(session=context.session)
return _vmove_get_by_uuid(context, vm_move.uuid)
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@main_context_manager.writer
def vmove_update(context, uuid, values):
vm_move = _vmove_get_by_uuid(context, uuid)
vm_move.update(values)
vm_move.save(session=context.session)
return _vmove_get_by_uuid(context, vm_move.uuid)
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@main_context_manager.writer
def vmove_delete(context, vmove_uuid):
count = model_query(context, models.VMove
).filter_by(uuid=vmove_uuid
).soft_delete(synchronize_session=False)
if count == 0:
raise exception.VMoveNotFound(id=vmove_uuid)
class DeleteFromSelect(sa_sql.expression.UpdateBase):
def __init__(self, table, select, column):
self.table = table

View File

@ -0,0 +1,52 @@
# Copyright(c) 2022 Inspur
#
# 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 migrate.changeset import UniqueConstraint
from sqlalchemy import Column, MetaData, Table
from sqlalchemy import Integer, DateTime, String, Text
def define_vm_moves_table(meta):
vm_moves = Table('vmoves',
meta,
Column('created_at', DateTime),
Column('updated_at', DateTime),
Column('deleted_at', DateTime),
Column('deleted', Integer),
Column('id', Integer, primary_key=True, nullable=False),
Column('uuid', String(36), nullable=False),
Column('notification_uuid', String(36), nullable=False),
Column('instance_uuid', String(36), nullable=False),
Column('instance_name', String(255), nullable=False),
Column('source_host', String(255), nullable=True),
Column('dest_host', String(255), nullable=True),
Column('start_time', DateTime, nullable=True),
Column('end_time', DateTime, nullable=True),
Column('type', String(36), nullable=True),
Column('status', String(36), nullable=True),
Column('message', Text, nullable=True),
UniqueConstraint('uuid', name='uniq_vmove0uuid'),
mysql_engine='InnoDB',
mysql_charset='utf8',
extend_existing=True)
return vm_moves
def upgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
table = define_vm_moves_table(meta)
table.create()

View File

@ -141,3 +141,25 @@ class Notification(BASE, MasakariAPIBase, models.SoftDeleteMixin):
'ignored', 'finished', name='notification_status'),
nullable=False)
source_host_uuid = Column(String(36), nullable=False)
class VMove(BASE, MasakariAPIBase, models.SoftDeleteMixin):
"""Represents one vm move."""
__tablename__ = 'vmoves'
__table_args__ = (
schema.UniqueConstraint('uuid',
name='uniq_vmove0uuid'),
)
id = Column(Integer, primary_key=True, autoincrement=True)
uuid = Column(String(36), nullable=False)
notification_uuid = Column(String(36), nullable=False)
instance_uuid = Column(String(36), nullable=False)
instance_name = Column(String(255), nullable=False)
source_host = Column(String(255), nullable=True)
dest_host = Column(String(255), nullable=True)
start_time = Column(DateTime, nullable=True)
end_time = Column(DateTime, nullable=True)
type = Column(String(36), nullable=True)
status = Column(String(255), nullable=True)
message = Column(Text)

View File

@ -22,12 +22,15 @@ from oslo_log import log as logging
from oslo_service import loopingcall
from oslo_utils import excutils
from oslo_utils import strutils
from oslo_utils import timeutils
from taskflow.patterns import linear_flow
from taskflow import retry
import masakari.conf
from masakari.engine.drivers.taskflow import base
from masakari import exception
from masakari import objects
from masakari.objects import fields
from masakari import utils
@ -61,15 +64,14 @@ class DisableComputeServiceTask(base.MasakariTask):
class PrepareHAEnabledInstancesTask(base.MasakariTask):
"""Get all HA_Enabled instances."""
default_provides = set(["instance_list"])
def __init__(self, context, novaclient, **kwargs):
kwargs['requires'] = ["host_name"]
kwargs['requires'] = ["host_name", "notification_uuid"]
super(PrepareHAEnabledInstancesTask, self).__init__(context,
novaclient,
**kwargs)
def execute(self, host_name):
def execute(self, host_name, notification_uuid):
def _filter_instances(instance_list):
ha_enabled_instances = []
non_ha_enabled_instances = []
@ -131,21 +133,28 @@ class PrepareHAEnabledInstancesTask(base.MasakariTask):
LOG.info(msg)
raise exception.SkipHostRecoveryException(message=msg)
# persist vm moves
for instance in instance_list:
vmove = objects.VMove(context=self.context)
vmove.instance_uuid = instance.id
vmove.instance_name = instance.name
vmove.notification_uuid = notification_uuid
vmove.source_host = host_name
vmove.status = fields.VMoveStatus.PENDING
vmove.type = fields.VMoveType.EVACUATION
vmove.create()
# List of instance UUID
instance_list = [instance.id for instance in instance_list]
msg = "Instances to be evacuated are: '%s'" % ','.join(instance_list)
self.update_details(msg, 1.0)
return {
"instance_list": instance_list,
}
class EvacuateInstancesTask(base.MasakariTask):
def __init__(self, context, novaclient, **kwargs):
kwargs['requires'] = ["host_name", "instance_list"]
kwargs['requires'] = ["host_name", "notification_uuid"]
self.update_host_method = kwargs['update_host_method']
super(EvacuateInstancesTask, self).__init__(context, novaclient,
**kwargs)
@ -185,9 +194,27 @@ class EvacuateInstancesTask(base.MasakariTask):
finally:
periodic_call_stopped.stop()
def _evacuate_and_confirm(self, context, instance, host_name,
failed_evacuation_instances,
def _evacuate_and_confirm(self, context, vmove,
reserved_host=None):
def _update_vmove(vmove, status=None, start_time=None,
end_time=None, dest_host=None,
message=None):
if status:
vmove.status = status
if start_time:
vmove.start_time = start_time
if end_time:
vmove.end_time = end_time
if dest_host:
vmove.dest_host = dest_host
if message:
vmove.message = message
vmove.save()
instance_uuid = vmove.instance_uuid
instance = self.novaclient.get_server(context, instance_uuid)
# Before locking the instance check whether it is already locked
# by user, if yes don't lock the instance
instance_already_locked = self.novaclient.get_server(
@ -208,7 +235,7 @@ class EvacuateInstancesTask(base.MasakariTask):
raise exception.InstanceEvacuateFailed(
instance_uuid=instance.id)
if instance_host != host_name:
if instance_host != vmove.source_host:
if ((old_vm_state == 'error' and
new_vm_state == 'active') or
old_vm_state == new_vm_state):
@ -265,7 +292,11 @@ class EvacuateInstancesTask(base.MasakariTask):
if vm_state == 'active':
stop_instance = False
# evacuate the instance
# start to evacuate the instance
_update_vmove(
vmove,
status=fields.VMoveStatus.ONGOING,
start_time=timeutils.utcnow())
self.novaclient.evacuate_instance(context, instance.id,
target=reserved_host)
@ -279,29 +310,46 @@ class EvacuateInstancesTask(base.MasakariTask):
if vm_state == 'error':
self.novaclient.reset_instance_state(
context, instance.id)
instance = self.novaclient.get_server(context, instance_uuid)
dest_host = getattr(
instance, "OS-EXT-SRV-ATTR:hypervisor_hostname")
_update_vmove(
vmove,
status=fields.VMoveStatus.SUCCEEDED,
dest_host=dest_host)
except etimeout.Timeout:
# Instance is not stop in the expected time_limit.
failed_evacuation_instances.append(instance.id)
msg = "Failed reason: timeout."
_update_vmove(
vmove,
status=fields.VMoveStatus.FAILED,
message=msg)
except Exception as e:
# Exception is raised while resetting instance state or
# evacuating the instance itself.
LOG.warning(str(e))
failed_evacuation_instances.append(instance.id)
_update_vmove(
vmove,
status=fields.VMoveStatus.FAILED,
message=str(e))
finally:
_update_vmove(vmove, end_time=timeutils.utcnow())
if not instance_already_locked:
# Unlock the server after evacuation and confirmation
self.novaclient.unlock_server(context, instance.id)
def execute(self, host_name, instance_list, reserved_host=None):
def execute(self, host_name, notification_uuid, reserved_host=None):
all_vmoves = objects.VMoveList.get_all_vmoves(
self.context, notification_uuid, status=fields.VMoveStatus.PENDING)
instance_list = [i.instance_uuid for i in all_vmoves]
msg = ("Start evacuation of instances from failed host '%(host_name)s'"
", instance uuids are: '%(instance_list)s'") % {
'host_name': host_name, 'instance_list': ','.join(instance_list)}
self.update_details(msg)
def _do_evacuate(context, host_name, instance_list,
def _do_evacuate(context, host_name,
reserved_host=None):
failed_evacuation_instances = []
if reserved_host:
msg = "Enabling reserved host: '%s'" % reserved_host
self.update_details(msg, 0.1)
@ -338,40 +386,41 @@ class EvacuateInstancesTask(base.MasakariTask):
context, reserved_host, enable=True)
# Set reserved property of reserved_host to False
self.update_host_method(self.context, reserved_host)
self.update_host_method(context, reserved_host)
thread_pool = greenpool.GreenPool(
CONF.host_failure_recovery_threads)
for instance_id in instance_list:
msg = "Evacuation of instance started: '%s'" % instance_id
self.update_details(msg, 0.5)
instance = self.novaclient.get_server(self.context,
instance_id)
thread_pool.spawn_n(self._evacuate_and_confirm, context,
instance, host_name,
failed_evacuation_instances,
reserved_host)
nonlocal all_vmoves
for vmove in all_vmoves:
msg = ("Evacuation of instance started: '%s'"
% vmove.instance_uuid)
self.update_details(msg, 0.5)
thread_pool.spawn_n(self._evacuate_and_confirm, self.context,
vmove, reserved_host)
thread_pool.waitall()
evacuated_instances = list(set(instance_list).difference(set(
failed_evacuation_instances)))
all_vmoves = objects.VMoveList.get_all_vmoves(
self.context, notification_uuid)
if evacuated_instances:
evacuated_instances.sort()
succeeded_vmoves = [i.instance_uuid for i in all_vmoves
if i.status == fields.VMoveStatus.SUCCEEDED]
if succeeded_vmoves:
succeeded_vmoves.sort()
msg = ("Successfully evacuate instances '%(instance_list)s' "
"from host '%(host_name)s'") % {
'instance_list': ','.join(evacuated_instances),
'instance_list': ','.join(succeeded_vmoves),
'host_name': host_name}
self.update_details(msg, 0.7)
if failed_evacuation_instances:
failed_vmoves = [i.instance_uuid for i in
all_vmoves if i.status == fields.VMoveStatus.FAILED]
if failed_vmoves:
msg = ("Failed to evacuate instances "
"'%(failed_evacuation_instances)s' from host "
"'%(instance_list)s' from host "
"'%(host_name)s'") % {
'failed_evacuation_instances':
','.join(failed_evacuation_instances),
'instance_list': ','.join(failed_vmoves),
'host_name': host_name}
self.update_details(msg, 0.7)
raise exception.HostRecoveryFailureException(
@ -383,18 +432,19 @@ class EvacuateInstancesTask(base.MasakariTask):
lock_name = reserved_host if reserved_host else None
@utils.synchronized(lock_name)
def do_evacuate_with_reserved_host(context, host_name, instance_list,
reserved_host):
_do_evacuate(self.context, host_name, instance_list,
def do_evacuate_with_reserved_host(context, host_name,
notification_uuid, reserved_host):
_do_evacuate(context, host_name,
reserved_host=reserved_host)
if lock_name:
do_evacuate_with_reserved_host(self.context, host_name,
instance_list, reserved_host)
notification_uuid,
reserved_host)
else:
# No need to acquire lock on reserved_host when recovery_method is
# 'auto' as the selection of compute host will be decided by nova.
_do_evacuate(self.context, host_name, instance_list)
_do_evacuate(self.context, host_name)
def get_auto_flow(context, novaclient, process_what):

View File

@ -281,6 +281,14 @@ class ComputeNotFoundByName(NotFound):
"be found.")
class VMoveNotFound(NotFound):
msg_fmt = _("No vm move with id %(id)s.")
class NotificationWithoutVMoves(Invalid):
msg_fmt = _("This notification %(id)s without vm moves.")
class FailoverSegmentExists(MasakariException):
msg_fmt = _("Failover segment with name %(name)s already exists.")

View File

@ -393,3 +393,36 @@ class NotificationAPI(object):
get_notification_recovery_workflow_details(
context, notification))
return notification
class VMoveAPI(object):
"""The vmoves API to manage vmoves"""
def _is_valid_notification(self, context, notification_uuid):
notification = objects.Notification.get_by_uuid(
context, notification_uuid)
if notification.type != fields.NotificationType.COMPUTE_HOST:
raise exception.NotificationWithoutVMoves(id=notification_uuid)
def get_all(self, context, notification_uuid, filters=None,
sort_keys=None, sort_dirs=None, limit=None, marker=None):
"""Get all vmoves by filters"""
self._is_valid_notification(context, notification_uuid)
filters['notification_uuid'] = notification_uuid
vmoves = objects.VMoveList.get_all(
context, filters, sort_keys, sort_dirs, limit, marker)
return vmoves
def get_vmove(self, context, notification_uuid, vmove_uuid):
"""Get one vmove."""
self._is_valid_notification(context, notification_uuid)
if uuidutils.is_uuid_like(vmove_uuid):
LOG.debug("Fetching vmove by uuid %s", vmove_uuid)
vmove = objects.VMove.get_by_uuid(context, vmove_uuid)
else:
LOG.debug("Failed to fetch vmove by uuid %s", vmove_uuid)
raise exception.VMoveNotFound(id=vmove_uuid)
return vmove

View File

@ -21,3 +21,4 @@ def register_all():
__import__('masakari.objects.host')
__import__('masakari.objects.notification')
__import__('masakari.objects.segment')
__import__('masakari.objects.vmove')

View File

@ -278,3 +278,61 @@ class EventNotificationPriorityField(BaseEnumField):
class EventNotificationPhaseField(BaseEnumField):
AUTO_TYPE = EventNotificationPhase()
class VMoveType(Enum):
"""Represents possible types for VMoves."""
EVACUATION = "evacuation"
MIGRATION = "migration"
LIVE_MIGRATION = "live_migration"
ALL = (EVACUATION, MIGRATION, LIVE_MIGRATION)
def __init__(self):
super(VMoveType,
self).__init__(valid_values=VMoveType.ALL)
@classmethod
def index(cls, value):
"""Return an index into the Enum given a value."""
return cls.ALL.index(value)
@classmethod
def from_index(cls, index):
"""Return the Enum value at a given index."""
return cls.ALL[index]
class VMoveTypeField(BaseEnumField):
AUTO_TYPE = VMoveType()
class VMoveStatus(Enum):
"""Represents possible statuses for VMoves."""
PENDING = "pending"
ONGOING = "ongoing"
IGNORED = "ignored"
FAILED = "failed"
SUCCEEDED = "succeeded"
ALL = (PENDING, ONGOING, IGNORED, FAILED, SUCCEEDED)
def __init__(self):
super(VMoveStatus,
self).__init__(valid_values=VMoveStatus.ALL)
@classmethod
def index(cls, value):
"""Return an index into the Enum given a value."""
return cls.ALL.index(value)
@classmethod
def from_index(cls, index):
"""Return the Enum value at a given index."""
return cls.ALL[index]
class VMoveStatusField(BaseEnumField):
AUTO_TYPE = VMoveStatus()

115
masakari/objects/vmove.py Normal file
View File

@ -0,0 +1,115 @@
# Copyright(c) 2022 Inspur
#
# 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 oslo_utils import uuidutils
from masakari import db
from masakari import exception
from masakari import objects
from masakari.objects import base
from masakari.objects import fields
LOG = logging.getLogger(__name__)
@base.MasakariObjectRegistry.register
class VMove(base.MasakariPersistentObject, base.MasakariObject,
base.MasakariObjectDictCompat):
VERSION = '1.0'
fields = {
'id': fields.IntegerField(),
'uuid': fields.UUIDField(),
'notification_uuid': fields.UUIDField(),
'instance_uuid': fields.UUIDField(),
'instance_name': fields.StringField(),
'source_host': fields.StringField(nullable=True),
'dest_host': fields.StringField(nullable=True),
'start_time': fields.DateTimeField(nullable=True),
'end_time': fields.DateTimeField(nullable=True),
'type': fields.VMoveTypeField(nullable=True),
'status': fields.VMoveStatusField(nullable=True),
'message': fields.StringField(nullable=True),
}
@staticmethod
def _from_db_object(context, vmove, db_vmove):
for key in vmove.fields:
setattr(vmove, key, db_vmove[key])
vmove._context = context
vmove.obj_reset_changes()
return vmove
@base.remotable_classmethod
def get_by_uuid(cls, context, uuid):
db_inst = db.vmove_get_by_uuid(context, uuid)
return cls._from_db_object(context, cls(), db_inst)
@base.remotable
def create(self):
if self.obj_attr_is_set('id'):
raise exception.ObjectActionError(action='create',
reason='already created')
updates = self.masakari_obj_get_changes()
if 'uuid' not in updates:
updates['uuid'] = uuidutils.generate_uuid()
vmove = db.vmove_create(self._context, updates)
self._from_db_object(self._context, self, vmove)
@base.remotable
def save(self):
updates = self.masakari_obj_get_changes()
updates.pop('id', None)
vmove = db.vmove_update(self._context, self.uuid, updates)
self._from_db_object(self._context, self, vmove)
@base.MasakariObjectRegistry.register
class VMoveList(base.ObjectListBase, base.MasakariObject):
VERSION = '1.0'
fields = {
'objects': fields.ListOfObjectsField('VMove'),
}
@base.remotable_classmethod
def get_all(cls, ctxt, filters=None, sort_keys=None,
sort_dirs=None, limit=None, marker=None):
groups = db.vmoves_get_all_by_filters(ctxt, filters=filters,
sort_keys=sort_keys,
sort_dirs=sort_dirs,
limit=limit,
marker=marker)
return base.obj_make_list(ctxt, cls(ctxt), objects.VMove,
groups)
@base.remotable_classmethod
def get_all_vmoves(cls, ctxt, notification_uuid, status=None):
filters = {
'notification_uuid': notification_uuid
}
if status:
filters['status'] = status
groups = db.vmoves_get_all_by_filters(ctxt, filters=filters)
return base.obj_make_list(ctxt, cls(ctxt), objects.VMove,
groups)

View File

@ -22,6 +22,7 @@ from masakari.policies import hosts
from masakari.policies import notifications
from masakari.policies import segments
from masakari.policies import versions
from masakari.policies import vmoves
def list_rules():
@ -31,5 +32,6 @@ def list_rules():
hosts.list_rules(),
notifications.list_rules(),
segments.list_rules(),
versions.list_rules()
versions.list_rules(),
vmoves.list_rules()
)

View File

@ -0,0 +1,54 @@
# Copyright(c) 2022 Inspur
#
# 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_policy import policy
from masakari.policies import base
VMOVES = 'os_masakari_api:vmoves:%s'
rules = [
policy.DocumentedRuleDefault(
name=VMOVES % 'index',
check_str=base.RULE_ADMIN_API,
description="Lists IDs, notification_id, instance_id, source_host, "
"dest_host, status and type for all VM moves.",
operations=[
{
'method': 'GET',
'path': '/notifications/{notification_id}/vmoves'
}
]),
policy.DocumentedRuleDefault(
name=VMOVES % 'detail',
check_str=base.RULE_ADMIN_API,
description="Shows details for one VM move.",
operations=[
{
'method': 'GET',
'path': '/notifications/{notification_id}/vmoves/'
'{vmove_id}'
}
]),
policy.RuleDefault(
name=VMOVES % 'discoverable',
check_str=base.RULE_ADMIN_API,
description="VM moves API extensions to change the API.",
),
]
def list_rules():
return rules

View File

@ -0,0 +1,205 @@
# Copyright(c) 2022 Inspur
#
# 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.
"""Tests for the vmoves api."""
from unittest import mock
import ddt
from webob import exc
from masakari.api.openstack.ha import vmoves
from masakari import exception
from masakari.ha import api as ha_api
from masakari.objects import base as obj_base
from masakari.objects import notification as notification_obj
from masakari.objects import vmove as vmove_obj
from masakari import test
from masakari.tests.unit.api.openstack import fakes
from masakari.tests.unit import fakes as fakes_data
from masakari.tests import uuidsentinel
def _make_vmove_obj(vmove_dict):
return vmove_obj.VMove(**vmove_dict)
def _make_vmoves_list(vmove_list):
return vmove_obj.VMove(objects=[
_make_vmove_obj(a) for a in vmove_list])
@ddt.ddt
class VMoveTestCase(test.TestCase):
"""Test Case for vmove api."""
bad_request = exception.ValidationError
def _set_up(self):
self.controller = vmoves.VMovesController()
self.req = fakes.HTTPRequest.blank(
'/v1/notifications/%s/vmoves' % (
uuidsentinel.fake_notification1),
use_admin_context=True)
self.context = self.req.environ['masakari.context']
def setUp(self):
super(VMoveTestCase, self).setUp()
self._set_up()
self.host_type_notification = fakes_data.create_fake_notification(
id=1,
type="COMPUTE_HOST",
source_host_uuid=uuidsentinel.fake_host_1,
status="running",
notification_uuid=uuidsentinel.fake_host_type_notification,
payload={'event': 'STOPPED',
'host_status': 'NORMAL',
'cluster_status': 'ONLINE'}
)
self.vm_type_notification = fakes_data.create_fake_notification(
id=1,
type="VM",
source_host_uuid=uuidsentinel.fake_host_2,
status="running",
notification_uuid=uuidsentinel.fake_vm_type_notification,
payload={'event': 'STOPPED',
'host_status': 'NORMAL',
'cluster_status': 'ONLINE'}
)
self.vmove_1 = fakes_data.create_fake_vmove(
id=1,
uuid=uuidsentinel.fake_vmove_1,
notification_uuid=self.host_type_notification.notification_uuid,
instance_uuid=uuidsentinel.fake_instance_1,
instance_name='vm-1',
source_host='node01',
dest_host='node02',
start_time='2022-11-22 14:50:22',
end_time="2022-11-22 14:50:35",
type="evacuation",
status='succeeded',
message=None
)
self.vmove_2 = fakes_data.create_fake_vmove(
id=1,
uuid=uuidsentinel.fake_vmove_1,
notification_uuid=self.host_type_notification.notification_uuid,
instance_uuid=uuidsentinel.fake_instance_1,
instance_name='vm-1',
source_host='node01',
dest_host='node02',
start_time="2022-11-22 14:50:23",
end_time="2022-11-22 14:50:38",
type="evacuation",
status='succeeded',
message=None
)
self.vmove_list = [self.vmove_1, self.vmove_2]
self.vmove_list_obj = _make_vmoves_list(self.vmove_list)
@property
def app(self):
return fakes.wsgi_app_v1(init_only='vmoves')
def _assert_vmove_data(self, expected, actual):
self.assertTrue(obj_base.obj_equal_prims(expected, actual),
"The vmove objects were not equal")
@mock.patch.object(notification_obj.Notification, 'get_by_uuid')
@mock.patch.object(ha_api.VMoveAPI, 'get_all')
def test_index(self, mock_get_all, mock_notification):
mock_notification.return_value = mock.Mock()
mock_get_all.return_value = self.vmove_list
result = self.controller.index(
self.req, uuidsentinel.fake_host_type_notification)
result = result['vmoves']
self._assert_vmove_data(self.vmove_list_obj,
_make_vmoves_list(result))
@ddt.data('sort_key', 'sort_dir')
@mock.patch.object(notification_obj.Notification, 'get_by_uuid',
return_value=mock.Mock())
def test_index_invalid(self, sort_by, mock_notification):
req = fakes.HTTPRequest.blank(
'/v1/notifications/%s/vmoves?%s=abcd' % (
uuidsentinel.fake_notification, sort_by),
use_admin_context=True)
self.assertRaises(exc.HTTPBadRequest, self.controller.index, req,
uuidsentinel.fake_notification1)
@mock.patch.object(notification_obj.Notification, 'get_by_uuid')
@mock.patch.object(ha_api.VMoveAPI, 'get_all')
def test_index_with_valid_notification(self, mock_get_all,
mock_notification):
mock_notification.return_value = mock.Mock()
mock_get_all.side_effect = exception.NotificationWithoutVMoves
req = fakes.HTTPRequest.blank('/v1/notifications/%s/vmoves' % (
uuidsentinel.fake_vm_type_notification), use_admin_context=True)
self.assertRaises(exc.HTTPBadRequest, self.controller.index, req,
uuidsentinel.fake_notification1)
@mock.patch.object(ha_api.VMoveAPI, 'get_vmove')
def test_show(self, mock_get_vmove):
mock_get_vmove.return_value = self.vmove_1
result = self.controller.show(self.req,
uuidsentinel.fake_notification1,
uuidsentinel.fake_vmove_1)
vmove = result['vmove']
self._assert_vmove_data(self.vmove_1,
_make_vmove_obj(vmove))
@mock.patch.object(ha_api.VMoveAPI, 'get_vmove')
def test_show_with_non_existing_id(self, mock_get_vmove):
mock_get_vmove.side_effect = exception.VMoveNotFound(id="2")
self.assertRaises(exc.HTTPNotFound,
self.controller.show, self.req,
uuidsentinel.fake_notification1, "2")
class VMoveTestCasePolicyNotAuthorized(test.NoDBTestCase):
"""Test Case for vmove non admin."""
def _set_up(self):
self.controller = vmoves.VMovesController()
self.req = fakes.HTTPRequest.blank(
'/v1/notifications/%s/vmoves' % (
uuidsentinel.fake_notification1))
self.context = self.req.environ['masakari.context']
def setUp(self):
super(VMoveTestCasePolicyNotAuthorized, self).setUp()
self._set_up()
def _check_rule(self, exc, rule_name):
self.assertEqual(
"Policy doesn't allow %s to be performed." % rule_name,
exc.format_message())
def test_index_no_admin(self):
rule_name = "os_masakari_api:vmoves:index"
self.policy.set_rules({rule_name: "project:non_fake"})
exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.index,
self.req, uuidsentinel.fake_notification1)
self._check_rule(exc, rule_name)
def test_show_no_admin(self):
rule_name = "os_masakari_api:vmoves:detail"
self.policy.set_rules({rule_name: "project:non_fake"})
exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.show,
self.req, uuidsentinel.fake_notification1,
uuidsentinel.fake_vmove_1)
self._check_rule(exc, rule_name)

View File

@ -501,3 +501,121 @@ class NotificationsTestCase(test.TestCase, ModelsObjectComparatorMixin):
self.assertRaises(exception.InvalidSortKey,
db.notifications_get_all_by_filters,
context=self.ctxt, sort_keys=['invalid_sort_key'])
class VMoveTestCase(test.TestCase, ModelsObjectComparatorMixin):
def setUp(self):
super(VMoveTestCase, self).setUp()
self.ctxt = context.get_admin_context()
def _get_fake_values(self):
return {
'uuid': uuidsentinel.vmove,
'notification_uuid': uuidsentinel.notification,
'instance_uuid': uuidsentinel.instance_uuid,
'instance_name': 'fake_instance',
'source_host': 'fake_host1',
'dest_host': 'fake_host2',
'start_time': None,
'end_time': None,
'type': 'evacuation',
'status': 'pending',
'message': None
}
def _get_fake_values_list(self):
return [
{'uuid': uuidsentinel.vmove_1,
'notification_uuid': uuidsentinel.notification,
'instance_uuid': uuidsentinel.instance_uuid_1,
'instance_name': 'fake_instance1',
'source_host': 'fake_host1',
'dest_host': None,
'start_time': None,
'end_time': None,
'type': 'evacuation',
'status': 'pending',
'message': None},
{'uuid': uuidsentinel.vmove_2,
'notification_uuid': uuidsentinel.notification,
'instance_uuid': uuidsentinel.instance_uuid_2,
'instance_name': 'fake_instance2',
'source_host': 'fake_host1',
'dest_host': None,
'start_time': None,
'end_time': None,
'type': 'evacuation',
'status': 'pending',
'message': None},
{'uuid': uuidsentinel.vmove_3,
'notification_uuid': uuidsentinel.notification,
'instance_uuid': uuidsentinel.instance_uuid_3,
'instance_name': 'fake_instance3',
'source_host': 'fake_host1',
'dest_host': None,
'start_time': None,
'end_time': None,
'type': 'evacuation',
'status': 'pending',
'message': None}]
def _create_vmove(self, values):
return db.vmove_create(self.ctxt, values)
def _test_get_vmove(self, method, filter):
vmoves = [self._create_vmove(p)
for p in self._get_fake_values_list()]
for vmove in vmoves:
real_vmove = method(self.ctxt, vmove[filter])
self._assertEqualObjects(vmove, real_vmove)
def test_vmove_create(self):
vmove = self._create_vmove(self._get_fake_values())
self.assertIsNotNone(vmove['uuid'])
ignored_keys = ['deleted', 'created_at', 'updated_at', 'deleted_at',
'id']
self._assertEqualObjects(vmove, self._get_fake_values(),
ignored_keys)
def test_vmove_get_by_uuid(self):
self._test_get_vmove(db.vmove_get_by_uuid, 'uuid')
def test_vmove_update(self):
update = {'dest_host': 'fake_host2', 'status': 'succeeded'}
updated = {'uuid': uuidsentinel.vmove,
'notification_uuid': uuidsentinel.notification,
'instance_uuid': uuidsentinel.instance_uuid,
'instance_name': 'fake_instance',
'source_host': 'fake_host1',
'dest_host': 'fake_host2',
'start_time': None,
'end_time': None,
'type': 'evacuation',
'status': 'succeeded',
'message': None}
ignored_keys = ['deleted', 'created_at', 'updated_at', 'deleted_at',
'id']
self._create_vmove(self._get_fake_values())
db.vmove_update(self.ctxt, uuidsentinel.vmove, update)
vmove_updated = db.vmove_get_by_uuid(
self.ctxt, uuidsentinel.vmove)
self._assertEqualObjects(updated, vmove_updated, ignored_keys)
def test_vmove_not_found(self):
self._create_vmove(self._get_fake_values())
self.assertRaises(exception.VMoveNotFound,
db.vmove_get_by_uuid, self.ctxt,
500)
def test_invalid_marker(self):
[self._create_vmove(p) for p in self._get_fake_values_list()]
self.assertRaises(exception.MarkerNotFound,
db.vmoves_get_all_by_filters,
context=self.ctxt, marker=6)
def test_invalid_sort_key(self):
[self._create_vmove(p) for p in self._get_fake_values_list()]
self.assertRaises(exception.InvalidSortKey,
db.vmoves_get_all_by_filters,
context=self.ctxt, sort_keys=['invalid_sort_key'])

View File

@ -217,6 +217,19 @@ class MasakariMigrationsCheckers(test_migrations.WalkVersionsMixin):
def _check_007(self, engine, data):
self.assertColumnExists(engine, 'failover_segments', 'enabled')
def _check_008(self, engine, data):
self.assertColumnExists(engine, 'vmoves', 'uuid')
self.assertColumnExists(engine, 'vmoves', 'notification_uuid')
self.assertColumnExists(engine, 'vmoves', 'instance_uuid')
self.assertColumnExists(engine, 'vmoves', 'instance_name')
self.assertColumnExists(engine, 'vmoves', 'source_host')
self.assertColumnExists(engine, 'vmoves', 'dest_host')
self.assertColumnExists(engine, 'vmoves', 'start_time')
self.assertColumnExists(engine, 'vmoves', 'end_time')
self.assertColumnExists(engine, 'vmoves', 'type')
self.assertColumnExists(engine, 'vmoves', 'status')
self.assertColumnExists(engine, 'vmoves', 'message')
class TestMasakariMigrationsSQLite(
MasakariMigrationsCheckers,

View File

@ -27,8 +27,12 @@ from masakari import context
from masakari.engine.drivers.taskflow import host_failure
from masakari.engine import manager
from masakari import exception
from masakari import objects
from masakari.objects import fields
from masakari.objects import vmove as vmove_obj
from masakari import test
from masakari.tests.unit import fakes
from masakari.tests import uuidsentinel
CONF = conf.CONF
@ -50,13 +54,19 @@ class HostFailureTestCase(test.TestCase):
self.override_config("evacuate_all_instances",
False, "host_failure")
self.instance_host = "fake-host"
self.notification_uuid = uuidsentinel.fake_notification
self.novaclient = nova.API()
self.fake_client = fakes.FakeNovaClient()
self.disabled_reason = CONF.host_failure.service_disable_reason
def _verify_instance_evacuated(self, old_instance_list):
for server in old_instance_list:
instance = self.novaclient.get_server(self.ctxt, server)
def _verify_instance_evacuated(self):
all_vmoves = objects.VMoveList.get_all_vmoves(
self.ctxt, self.notification_uuid)
for vmove in all_vmoves:
instance = self.novaclient.get_server(self.ctxt,
vmove.instance_uuid)
if getattr(instance, 'OS-EXT-STS:vm_state') in \
['active', 'stopped', 'error']:
@ -92,10 +102,15 @@ class HostFailureTestCase(test.TestCase):
def _test_instance_list(self, instances_evacuation_count):
task = host_failure.PrepareHAEnabledInstancesTask(self.ctxt,
self.novaclient)
instances = task.execute(self.instance_host)
instance_uuid_list = []
for instance_id in instances['instance_list']:
instance = self.novaclient.get_server(self.ctxt, instance_id)
task.execute(self.instance_host, self.notification_uuid)
all_vmoves = objects.VMoveList.get_all_vmoves(
self.ctxt,
self.notification_uuid)
for vmove in all_vmoves:
instance = self.novaclient.get_server(self.ctxt,
vmove.instance_uuid)
if CONF.host_failure.ignore_instances_in_error_state:
self.assertNotEqual("error",
getattr(instance, "OS-EXT-STS:vm_state"))
@ -104,34 +119,25 @@ class HostFailureTestCase(test.TestCase):
.ha_enabled_instance_metadata_key)
self.assertTrue(instance.metadata.get(ha_enabled_key, False))
instance_uuid_list.append(instance.id)
self.assertEqual(instances_evacuation_count, len(all_vmoves))
self.assertEqual(instances_evacuation_count,
len(instances['instance_list']))
return {
"instance_list": instance_uuid_list,
}
def _evacuate_instances(self, instance_list, mock_enable_disable,
reserved_host=None):
def _evacuate_instances(self, mock_enable_disable, reserved_host=None):
task = host_failure.EvacuateInstancesTask(
self.ctxt, self.novaclient,
update_host_method=manager.update_host_method)
old_instance_list = copy.deepcopy(instance_list['instance_list'])
if reserved_host:
task.execute(self.instance_host,
instance_list['instance_list'],
self.notification_uuid,
reserved_host=reserved_host)
self.assertTrue(mock_enable_disable.called)
else:
task.execute(
self.instance_host, instance_list['instance_list'])
self.instance_host, self.notification_uuid)
# make sure instance is active and has different host
self._verify_instance_evacuated(old_instance_list)
self._verify_instance_evacuated()
@mock.patch.object(nova.API, "get_server")
@mock.patch.object(nova.API, "evacuate_instance")
@ -150,20 +156,24 @@ class HostFailureTestCase(test.TestCase):
return fake_server
failed_evacuation_instances = []
fake_instance = self.fake_client.servers.create(
id="1", host=self.instance_host, ha_enabled=True)
vmove = vmove_obj.VMove(context=self.ctxt)
vmove.instance_uuid = fake_instance.id
vmove.instance_name = fake_instance.name
vmove.notification_uuid = self.notification_uuid
vmove.source_host = self.instance_host
vmove.status = fields.VMoveStatus.PENDING
vmove.type = fields.VMoveType.EVACUATION
vmove.create()
_mock_get.side_effect = [
get_fake_server(fake_instance, 'active'),
get_fake_server(fake_instance, 'error'),
]
task._evacuate_and_confirm(self.ctxt, fake_instance,
self.instance_host,
failed_evacuation_instances)
self.assertIn(fake_instance.id, failed_evacuation_instances)
expected_log = 'Failed to evacuate instance %s' % fake_instance.id
_mock_log.warning.assert_called_once_with(expected_log)
task._evacuate_and_confirm(self.ctxt, vmove)
self.assertEqual(fields.VMoveStatus.FAILED, vmove.status)
@mock.patch('masakari.compute.nova.novaclient')
@mock.patch('masakari.engine.drivers.taskflow.base.MasakariTask.'
@ -184,10 +194,10 @@ class HostFailureTestCase(test.TestCase):
self._test_disable_compute_service(mock_enable_disable)
# execute PrepareHAEnabledInstancesTask
instance_list = self._test_instance_list(2)
self._test_instance_list(2)
# execute EvacuateInstancesTask
self._evacuate_instances(instance_list, mock_enable_disable)
self._evacuate_instances(mock_enable_disable)
# verify progress details
_mock_notify.assert_has_calls([
@ -202,9 +212,9 @@ class HostFailureTestCase(test.TestCase):
"considered for evacuation. Total count is: '2'", 0.8),
mock.call("Instances to be evacuated are: '1,2'", 1.0),
mock.call("Start evacuation of instances from failed host "
"'fake-host', instance uuids are: '1,2'"),
mock.call("Evacuation of instance started: '1'", 0.5),
"'fake-host', instance uuids are: '2,1'"),
mock.call("Evacuation of instance started: '2'", 0.5),
mock.call("Evacuation of instance started: '1'", 0.5),
mock.call("Successfully evacuate instances '1,2' from host "
"'fake-host'", 0.7),
mock.call('Evacuation process completed!', 1.0)
@ -236,10 +246,10 @@ class HostFailureTestCase(test.TestCase):
self._test_disable_compute_service(mock_enable_disable)
# execute PrepareHAEnabledInstancesTask
instance_list = self._test_instance_list(1)
self._test_instance_list(1)
# execute EvacuateInstancesTask
self._evacuate_instances(instance_list, mock_enable_disable)
self._evacuate_instances(mock_enable_disable)
# verify progress details
_mock_notify.assert_has_calls([
@ -283,12 +293,12 @@ class HostFailureTestCase(test.TestCase):
self._test_disable_compute_service(mock_enable_disable)
# execute PrepareHAEnabledInstancesTask
instance_list = self._test_instance_list(2)
self._test_instance_list(2)
# execute EvacuateInstancesTask
with mock.patch.object(manager, "update_host_method") as mock_save:
self._evacuate_instances(
instance_list, mock_enable_disable,
mock_enable_disable,
reserved_host=reserved_host.name)
self.assertEqual(1, mock_save.call_count)
self.assertIn(reserved_host.name,
@ -299,22 +309,22 @@ class HostFailureTestCase(test.TestCase):
mock.call("Disabling compute service on host: 'fake-host'"),
mock.call("Disabled compute service on host: 'fake-host'", 1.0),
mock.call('Preparing instances for evacuation'),
mock.call("Total instances running on failed host 'fake-host' is 2"
"", 0.3),
mock.call("Total instances running on failed host 'fake-host' is "
"2", 0.3),
mock.call("Total HA Enabled instances count: '1'", 0.6),
mock.call("Total Non-HA Enabled instances count: '1'", 0.7),
mock.call("All instances (HA Enabled/Non-HA Enabled) should be "
"considered for evacuation. Total count is: '2'", 0.8),
mock.call("Instances to be evacuated are: '1,2'", 1.0),
mock.call("Start evacuation of instances from failed host "
"'fake-host', instance uuids are: '1,2'"),
"'fake-host', instance uuids are: '2,1'"),
mock.call("Enabling reserved host: 'fake-reserved-host'", 0.1),
mock.call('Add host fake-reserved-host to aggregate fake_agg',
0.2),
mock.call('Added host fake-reserved-host to aggregate fake_agg',
0.3),
mock.call("Evacuation of instance started: '1'", 0.5),
mock.call("Evacuation of instance started: '2'", 0.5),
mock.call("Evacuation of instance started: '1'", 0.5),
mock.call("Successfully evacuate instances '1,2' from host "
"'fake-host'", 0.7),
mock.call('Evacuation process completed!', 1.0)
@ -348,12 +358,12 @@ class HostFailureTestCase(test.TestCase):
self._test_disable_compute_service(mock_enable_disable)
# execute PrepareHAEnabledInstancesTask
instance_list = self._test_instance_list(2)
self._test_instance_list(2)
# execute EvacuateInstancesTask
with mock.patch.object(manager, "update_host_method") as mock_save:
self._evacuate_instances(
instance_list, mock_enable_disable,
mock_enable_disable,
reserved_host=reserved_host.name)
self.assertEqual(1, mock_save.call_count)
self.assertIn(reserved_host.name,
@ -374,7 +384,7 @@ class HostFailureTestCase(test.TestCase):
"considered for evacuation. Total count is: '2'", 0.8),
mock.call("Instances to be evacuated are: '1,2'", 1.0),
mock.call("Start evacuation of instances from failed host "
"'fake-host', instance uuids are: '1,2'"),
"'fake-host', instance uuids are: '2,1'"),
mock.call("Enabling reserved host: 'fake-reserved-host'", 0.1),
mock.call('Add host fake-reserved-host to aggregate fake_agg_1',
0.2),
@ -384,8 +394,8 @@ class HostFailureTestCase(test.TestCase):
0.2),
mock.call('Added host fake-reserved-host to aggregate fake_agg_2',
0.3),
mock.call("Evacuation of instance started: '1'", 0.5),
mock.call("Evacuation of instance started: '2'", 0.5),
mock.call("Evacuation of instance started: '1'", 0.5),
mock.call("Successfully evacuate instances '1,2' from host "
"'fake-host'", 0.7),
mock.call('Evacuation process completed!', 1.0)
@ -421,12 +431,12 @@ class HostFailureTestCase(test.TestCase):
self._test_disable_compute_service(mock_enable_disable)
# execute PrepareHAEnabledInstancesTask
instance_list = self._test_instance_list(1)
self._test_instance_list(1)
# execute EvacuateInstancesTask
with mock.patch.object(manager, "update_host_method") as mock_save:
self._evacuate_instances(
instance_list, mock_enable_disable,
mock_enable_disable,
reserved_host=reserved_host.name)
self.assertEqual(1, mock_save.call_count)
self.assertIn(reserved_host.name,
@ -476,23 +486,17 @@ class HostFailureTestCase(test.TestCase):
power_state=power_state,
ha_enabled=True)
instance_uuid_list = []
for instance in self.fake_client.servers.list():
instance_uuid_list.append(instance.id)
instance_list = {
"instance_list": instance_uuid_list,
}
self._test_instance_list(2)
# execute EvacuateInstancesTask
self._evacuate_instances(instance_list, mock_enable_disable)
self._evacuate_instances(mock_enable_disable)
# verify progress details
_mock_notify.assert_has_calls([
mock.call("Start evacuation of instances from failed host "
"'fake-host', instance uuids are: '1,2'"),
mock.call("Evacuation of instance started: '1'", 0.5),
"'fake-host', instance uuids are: '2,1'"),
mock.call("Evacuation of instance started: '2'", 0.5),
mock.call("Evacuation of instance started: '1'", 0.5),
mock.call("Successfully evacuate instances '1,2' from host "
"'fake-host'", 0.7),
mock.call('Evacuation process completed!', 1.0)
@ -519,10 +523,10 @@ class HostFailureTestCase(test.TestCase):
ha_enabled=True)
# execute PrepareHAEnabledInstancesTask
instance_list = self._test_instance_list(1)
self._test_instance_list(1)
# execute EvacuateInstancesTask
self._evacuate_instances(instance_list, mock_enable_disable)
self._evacuate_instances(mock_enable_disable)
# verify progress details
_mock_notify.assert_has_calls([
@ -565,7 +569,7 @@ class HostFailureTestCase(test.TestCase):
task = host_failure.PrepareHAEnabledInstancesTask(self.ctxt,
self.novaclient)
self.assertRaises(exception.SkipHostRecoveryException, task.execute,
self.instance_host)
self.instance_host, self.notification_uuid)
# verify progress details
_mock_notify.assert_has_calls([
@ -598,7 +602,7 @@ class HostFailureTestCase(test.TestCase):
task = host_failure.PrepareHAEnabledInstancesTask(self.ctxt,
self.novaclient)
self.assertRaises(exception.SkipHostRecoveryException, task.execute,
self.instance_host)
self.instance_host, self.notification_uuid)
# verify progress details
_mock_notify.assert_has_calls([
@ -628,13 +632,7 @@ class HostFailureTestCase(test.TestCase):
host=self.instance_host,
ha_enabled=True)
instance_uuid_list = []
for instance in self.fake_client.servers.list():
instance_uuid_list.append(instance.id)
instance_list = {
"instance_list": instance_uuid_list,
}
self._test_instance_list(1)
def fake_get_server(context, host):
# assume that while evacuating instance goes into error state
@ -646,7 +644,7 @@ class HostFailureTestCase(test.TestCase):
# execute EvacuateInstancesTask
self.assertRaises(
exception.HostRecoveryFailureException,
self._evacuate_instances, instance_list, mock_enable_disable)
self._evacuate_instances, mock_enable_disable)
# verify progress details
_mock_notify.assert_has_calls([
@ -674,7 +672,7 @@ class HostFailureTestCase(test.TestCase):
task = host_failure.PrepareHAEnabledInstancesTask(self.ctxt,
self.novaclient)
self.assertRaises(exception.SkipHostRecoveryException, task.execute,
self.instance_host)
self.instance_host, self.notification_uuid)
# verify progress details
_mock_notify.assert_has_calls([
@ -715,34 +713,19 @@ class HostFailureTestCase(test.TestCase):
task_state='fake_task_state',
power_state=None,
ha_enabled=True)
instance_uuid_list = []
for instance in self.fake_client.servers.list():
instance_uuid_list.append(instance.id)
instance_list = {
"instance_list": instance_uuid_list,
}
self._test_instance_list(3)
# execute EvacuateInstancesTask
self._evacuate_instances(instance_list, mock_enable_disable)
reset_calls = [('1', 'active'),
('2', 'stopped'),
('3', 'error'),
('3', 'stopped')]
stop_calls = ['2', '3']
self.assertEqual(reset_calls,
self.fake_client.servers.reset_state_calls)
self.assertEqual(stop_calls,
self.fake_client.servers.stop_calls)
self._evacuate_instances(mock_enable_disable)
# verify progress details
_mock_notify.assert_has_calls([
mock.call("Start evacuation of instances from failed host "
"'fake-host', instance uuids are: '1,2,3'"),
mock.call("Evacuation of instance started: '1'", 0.5),
mock.call("Evacuation of instance started: '2'", 0.5),
"'fake-host', instance uuids are: '3,2,1'"),
mock.call("Evacuation of instance started: '3'", 0.5),
mock.call("Evacuation of instance started: '2'", 0.5),
mock.call("Evacuation of instance started: '1'", 0.5),
mock.call("Successfully evacuate instances '1,2,3' from host "
"'fake-host'", 0.7),
mock.call('Evacuation process completed!', 1.0)
@ -773,12 +756,12 @@ class HostFailureTestCase(test.TestCase):
self._test_disable_compute_service(mock_enable_disable)
# execute PrepareHAEnabledInstancesTask
instance_list = self._test_instance_list(2)
self._test_instance_list(2)
# execute EvacuateInstancesTask
with mock.patch.object(manager, "update_host_method") as mock_save:
self._evacuate_instances(
instance_list, mock_enable_disable,
mock_enable_disable,
reserved_host=reserved_host.name)
self.assertEqual(1, mock_save.call_count)
self.assertIn(reserved_host.name,
@ -797,14 +780,14 @@ class HostFailureTestCase(test.TestCase):
"considered for evacuation. Total count is: '2'", 0.8),
mock.call("Instances to be evacuated are: '1,2'", 1.0),
mock.call("Start evacuation of instances from failed host "
"'fake-host', instance uuids are: '1,2'"),
"'fake-host', instance uuids are: '2,1'"),
mock.call("Enabling reserved host: 'fake-reserved-host'", 0.1),
mock.call('Add host fake-reserved-host to aggregate fake_agg',
0.2),
mock.call('Added host fake-reserved-host to aggregate fake_agg',
0.3),
mock.call("Evacuation of instance started: '1'", 0.5),
mock.call("Evacuation of instance started: '2'", 0.5),
mock.call("Evacuation of instance started: '1'", 0.5),
mock.call("Successfully evacuate instances '1,2' from host "
"'fake-host'", 0.7),
mock.call('Evacuation process completed!', 1.0)
@ -822,16 +805,11 @@ class HostFailureTestCase(test.TestCase):
task_state=None,
power_state=None,
ha_enabled=True)
instance_uuid_list = []
for instance in self.fake_client.servers.list():
instance_uuid_list.append(instance.id)
instance_list = {
"instance_list": instance_uuid_list,
}
self._test_instance_list(1)
# execute EvacuateInstancesTask
self._evacuate_instances(instance_list, mock_enable_disable)
self._evacuate_instances(mock_enable_disable)
# If vm_state=stopped and task_state=None, reset_state and stop API
# will not be called.

View File

@ -45,6 +45,7 @@ class FakeNovaClient(object):
self.id = id
self.uuid = uuid or uuidutils.generate_uuid()
self.host = host
self.name = 'fake_instance'
setattr(self, 'OS-EXT-SRV-ATTR:hypervisor_hostname', host)
setattr(self, 'OS-EXT-STS:vm_state', vm_state)
setattr(self, 'OS-EXT-STS:task_state', task_state)
@ -226,3 +227,31 @@ def create_fake_notification_progress_details(
return objects.NotificationProgressDetails(
name=name, uuid=uuid, progress=progress, state=state,
progress_details=progress_details)
def create_fake_vmove(
id=1,
uuid=uuidsentinel.fake_vmove,
notification_uuid=uuidsentinel.fake_notification,
instance_uuid=uuidsentinel.fake_instance,
instance_name='fake_instance',
source_host='fake_host_1',
dest_host='fake_host_2',
start_time=None,
end_time=None,
type='evacuation',
status='pending',
message=None):
return objects.VMove(
id=id,
uuid=uuid,
notification_uuid=notification_uuid,
instance_uuid=instance_uuid,
instance_name=instance_name,
source_host=source_host,
dest_host=dest_host,
start_time=start_time,
end_time=end_time,
type=type,
status=status,
message=message)

View File

@ -32,6 +32,7 @@ from masakari.objects import fields
from masakari.objects import host as host_obj
from masakari.objects import notification as notification_obj
from masakari.objects import segment as segment_obj
from masakari.objects import vmove as vmove_obj
from masakari import test
from masakari.tests.unit.api.openstack import fakes
from masakari.tests.unit import fakes as fakes_data
@ -40,6 +41,10 @@ from masakari.tests import uuidsentinel
NOW = timeutils.utcnow().replace(microsecond=0)
def _make_vmove_obj(vmove_dict):
return vmove_obj.VMove(**vmove_dict)
def _make_segment_obj(segment_dict):
return segment_obj.FailoverSegment(**segment_dict)
@ -938,3 +943,102 @@ class NotificationAPITestCase(test.NoDBTestCase):
self.assertRaises(exception.InvalidInput,
self.notification_api.get_all,
self.context, self.req)
class VMoveAPITestCase(test.NoDBTestCase):
"""Test Case for vmove api."""
def setUp(self):
super(VMoveAPITestCase, self).setUp()
self.vmove_api = ha_api.VMoveAPI()
self.req = fakes.HTTPRequest.blank(
'/v1/notifications/%s/vmoves' % (
uuidsentinel.fake_notification),
use_admin_context=True)
self.context = self.req.environ['masakari.context']
self.notification = fakes_data.create_fake_notification(
id=1,
type="COMPUTE_HOST",
source_host_uuid=uuidsentinel.fake_host_1,
status="running",
notification_uuid=uuidsentinel.fake_notification,
payload={'event': 'STOPPED',
'host_status': 'NORMAL',
'cluster_status': 'ONLINE'}
)
self.vmove = fakes_data.create_fake_vmove(
id=1,
uuid=uuidsentinel.fake_vmove,
notification_uuid=self.notification.notification_uuid,
instance_uuid=uuidsentinel.fake_instance_1,
instance_name='vm1',
source_host='host_1',
dest_host='host_2',
start_time='2022-11-22 14:50:22',
end_time='2022-11-22 14:50:22',
type='evacuation',
status='succeeded',
message=None
)
def _assert_vmove_data(self, expected, actual):
self.assertTrue(obj_base.obj_equal_prims(expected, actual),
"The vmove objects were not equal")
@mock.patch.object(vmove_obj.VMoveList, 'get_all')
@mock.patch.object(notification_obj.Notification, 'get_by_uuid')
def test_get_all(self, mock_get, mock_get_all):
mock_get.return_value = self.notification
fake_vmove_list = [self.vmove]
mock_get_all.return_value = fake_vmove_list
result = self.vmove_api.get_all(self.context,
uuidsentinel.fake_notification,
filters={},
sort_keys=['created_at'],
sort_dirs=['desc'],
limit=None,
marker=None)
for i in range(len(result)):
self._assert_vmove_data(
fake_vmove_list[i], _make_vmove_obj(result[i]))
@mock.patch.object(vmove_obj.VMoveList, 'get_all')
@mock.patch.object(notification_obj.Notification, 'get_by_uuid')
def test_get_all_with_invalid_notification(self, mock_get, mock_get_all):
vm_type_notification = fakes_data.create_fake_notification(
id=1,
type="VM",
source_host_uuid=uuidsentinel.fake_host_2,
status="running",
notification_uuid=uuidsentinel.fake_vm_type_notification,
payload={'event': 'STOPPED',
'host_status': 'NORMAL',
'cluster_status': 'ONLINE'}
)
mock_get.return_value = vm_type_notification
self.assertRaises(exception.NotificationWithoutVMoves,
self.vmove_api.get_all, self.context,
uuidsentinel.fake_vm_type_notification)
@mock.patch.object(vmove_obj.VMove, 'get_by_uuid')
@mock.patch.object(notification_obj.Notification, 'get_by_uuid')
def test_get_vmove(self, mock_get, mock_get_vmove):
mock_get.return_value = self.notification
mock_get_vmove.return_value = self.vmove
result = self.vmove_api.get_vmove(
self.context,
uuidsentinel.fake_notification,
uuidsentinel.fake_vmove)
self._assert_vmove_data(self.vmove, result)
@mock.patch.object(vmove_obj.VMove, 'get_by_uuid')
@mock.patch.object(notification_obj.Notification, 'get_by_uuid')
def test_get_vmove_not_found(self, mock_get_notification, mock_get_vmove):
mock_get_notification.return_value = self.notification
self.assertRaises(exception.VMoveNotFound,
self.vmove_api.get_vmove, self.context,
uuidsentinel.fake_notification,
"123")

View File

@ -674,7 +674,9 @@ object_data = {
'MyOwnedObject': '1.0-fec853730bd02d54cc32771dd67f08a0',
'SegmentApiNotification': '1.0-1187e93f564c5cca692db76a66cda2a6',
'SegmentApiPayload': '1.1-e34e1c772e16e9ad492067ee98607b1d',
'SegmentApiPayloadBase': '1.1-6a1db76f3e825f92196fc1a11508d886'
'SegmentApiPayloadBase': '1.1-6a1db76f3e825f92196fc1a11508d886',
'VMove': '1.0-5c4d8667b5612b8a49adc065f8961aa2',
'VMoveList': '1.0-63fff36dee683c7a1555798cb233ad3f'
}

View File

@ -0,0 +1,120 @@
# Copyright(c) 2022 Inspur
#
# 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 unittest import mock
from oslo_utils import timeutils
from masakari import exception
from masakari.objects import vmove
from masakari.tests.unit.objects import test_objects
from masakari.tests import uuidsentinel
NOW = timeutils.utcnow().replace(microsecond=0)
fake_vmove = {
'created_at': NOW,
'updated_at': None,
'deleted_at': None,
'deleted': False,
'id': 1,
'uuid': uuidsentinel.fake_vmove,
'notification_uuid': uuidsentinel.fake_notification,
'instance_uuid': uuidsentinel.fake_instance,
'instance_name': 'fake_vm',
'source_host': 'fake_host1',
'dest_host': None,
'start_time': None,
'end_time': None,
'status': 'pending',
'type': 'evacuation',
'message': None
}
class TestVMoveObject(test_objects._LocalTest):
@mock.patch('masakari.db.vmove_get_by_uuid')
def test_get_by_uuid(self, mock_api_get):
mock_api_get.return_value = fake_vmove
vmove_obj = vmove.VMove.get_by_uuid(
self.context, uuidsentinel.fake_vmove)
self.compare_obj(vmove_obj, fake_vmove)
mock_api_get.assert_called_once_with(self.context,
uuidsentinel.fake_vmove)
def _vmove_create_attributes(self):
vmove_obj = vmove.VMove(context=self.context)
vmove_obj.uuid = uuidsentinel.fake_vmove
vmove_obj.notification_uuid = uuidsentinel.fake_notification
vmove_obj.instance_uuid = uuidsentinel.fake_instance
vmove_obj.instance_name = 'fake_vm1'
vmove_obj.source_host = 'fake_host1'
vmove_obj.status = 'pending'
vmove_obj.type = 'evacuation'
return vmove_obj
@mock.patch('masakari.db.vmove_create')
def test_create(self, mock_vmove_create):
mock_vmove_create.return_value = fake_vmove
vmove_obj = self._vmove_create_attributes()
vmove_obj.create()
self.compare_obj(vmove_obj, fake_vmove)
mock_vmove_create.assert_called_once_with(self.context, {
'uuid': uuidsentinel.fake_vmove,
'notification_uuid': uuidsentinel.fake_notification,
'instance_uuid': uuidsentinel.fake_instance,
'instance_name': 'fake_vm1',
'source_host': 'fake_host1',
'status': 'pending',
'type': 'evacuation'
})
@mock.patch('masakari.db.vmoves_get_all_by_filters')
def test_get_limit_and_marker_invalid_marker(self, mock_api_get):
vmove_uuid = uuidsentinel.fake_vmove
mock_api_get.side_effect = (exception.
MarkerNotFound(marker=vmove_uuid))
self.assertRaises(exception.MarkerNotFound,
vmove.VMoveList.get_all,
self.context, limit=5, marker=vmove_uuid)
@mock.patch('masakari.db.vmove_update')
def test_save(self, mock_vmove_update):
mock_vmove_update.return_value = fake_vmove
vmove_obj = self._vmove_create_attributes()
vmove_obj.uuid = uuidsentinel.fake_vmove
vmove_obj.save()
self.compare_obj(vmove_obj, fake_vmove)
(mock_vmove_update.assert_called_once_with(
self.context, uuidsentinel.fake_vmove,
{'uuid': uuidsentinel.fake_vmove,
'notification_uuid': uuidsentinel.fake_notification,
'instance_uuid': uuidsentinel.fake_instance,
'instance_name': 'fake_vm1',
'source_host': 'fake_host1',
'status': 'pending',
'type': 'evacuation'}))

View File

@ -61,7 +61,7 @@ masakari.api.v1.extensions =
segments = masakari.api.openstack.ha.segments:Segments
hosts = masakari.api.openstack.ha.hosts:Hosts
notifications = masakari.api.openstack.ha.notifications:Notifications
vmoves = masakari.api.openstack.ha.vmoves:VMoves
masakari.driver =
taskflow_driver = masakari.engine.drivers.taskflow:TaskFlowDriver