config/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/upgrade.py

439 lines
17 KiB
Python
Executable File

#
# Copyright (c) 2015-2017 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# coding=utf-8
#
import pecan
from pecan import rest, expose
import os
import six
import socket
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from sysinv.api.controllers.v1 import base
from sysinv.api.controllers.v1 import collection
from sysinv.api.controllers.v1 import link
from sysinv.api.controllers.v1 import types
from sysinv.api.controllers.v1 import utils
from sysinv.api.controllers.v1 import vim_api
from sysinv.common import exception
from sysinv.common import utils as cutils
from sysinv.common import constants
from sysinv import objects
from sysinv.openstack.common import log
from sysinv.openstack.common.gettextutils import _
LOG = log.getLogger(__name__)
class UpgradePatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/state']
class Upgrade(base.APIBase):
"""API representation of a Software Upgrade instance.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of a upgrade
"""
id = int
"Unique ID for this entry"
uuid = types.uuid
"Unique UUID for this entry"
state = wtypes.text
"Software upgrade state."
from_load = int
"The load id that software upgrading from"
to_load = int
"The load id that software upgrading to"
links = [link.Link]
"A list containing a self link and associated upgrade links"
from_release = wtypes.text
"The load version that software upgrading from"
to_release = wtypes.text
"The load version that software upgrading to"
def __init__(self, **kwargs):
self.fields = objects.software_upgrade.fields.keys()
for k in self.fields:
if not hasattr(self, k):
continue
setattr(self, k, kwargs.get(k, wtypes.Unset))
@classmethod
def convert_with_links(cls, rpc_upgrade, expand=True):
upgrade = Upgrade(**rpc_upgrade.as_dict())
if not expand:
upgrade.unset_fields_except(['uuid', 'state', 'from_release',
'to_release'])
upgrade.links = [link.Link.make_link('self', pecan.request.host_url,
'upgrades', upgrade.uuid),
link.Link.make_link('bookmark',
pecan.request.host_url,
'upgrades', upgrade.uuid,
bookmark=True)
]
return upgrade
class UpgradeCollection(collection.Collection):
"""API representation of a collection of software upgrades."""
upgrades = [Upgrade]
"A list containing Software Upgrade objects"
def __init__(self, **kwargs):
self._type = 'upgrades'
@classmethod
def convert_with_links(cls, rpc_upgrade, limit, url=None, expand=False,
**kwargs):
collection = UpgradeCollection()
collection.upgrades = [Upgrade.convert_with_links(p, expand)
for p in rpc_upgrade]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
LOCK_NAME = 'UpgradeController'
class UpgradeController(rest.RestController):
"""REST controller for Software Upgrades."""
_custom_actions = {
'check_reinstall': ['GET'],
'in_upgrade': ['GET'],
}
def __init__(self, parent=None, **kwargs):
self._parent = parent
def _get_upgrade_collection(self, marker=None, limit=None,
sort_key=None, sort_dir=None,
expand=False, resource_url=None):
limit = utils.validate_limit(limit)
sort_dir = utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.software_upgrade.get_by_uuid(
pecan.request.context, marker)
upgrades = pecan.request.dbapi.software_upgrade_get_list(
limit=limit, marker=marker_obj,
sort_key=sort_key, sort_dir=sort_dir)
return UpgradeCollection.convert_with_links(
upgrades, limit, url=resource_url, expand=expand,
sort_key=sort_key, sort_dir=sort_dir)
def _get_updates(self, patch):
"""Retrieve the updated attributes from the patch request."""
updates = {}
for p in patch:
attribute = p['path'] if p['path'][0] != '/' else p['path'][1:]
updates[attribute] = p['value']
return updates
@expose('json')
def check_reinstall(self):
reinstall_necessary = False
try:
upgrade = pecan.request.dbapi.software_upgrade_get_one()
except exception.NotFound:
pass
else:
controller_0 = pecan.request.dbapi.ihost_get_by_hostname(
constants.CONTROLLER_0_HOSTNAME)
host_upgrade = pecan.request.dbapi.host_upgrade_get_by_host(
controller_0.id)
if host_upgrade.target_load == upgrade.to_load or \
host_upgrade.software_load == upgrade.to_load:
reinstall_necessary = True
return {'reinstall_necessary': reinstall_necessary}
@wsme_pecan.wsexpose(UpgradeCollection, types.uuid, int, wtypes.text,
wtypes.text)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of upgrades."""
return self._get_upgrade_collection(marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(Upgrade, types.uuid)
def get_one(self, uuid):
"""Retrieve information about the given upgrade."""
rpc_upgrade = objects.software_upgrade.get_by_uuid(
pecan.request.context, uuid)
return Upgrade.convert_with_links(rpc_upgrade)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(Upgrade, body=six.text_type)
def post(self, body):
"""Create a new Software Upgrade instance and start upgrade."""
# Only start the upgrade from controller-0
if socket.gethostname() != constants.CONTROLLER_0_HOSTNAME:
raise wsme.exc.ClientSideError(_(
"upgrade-start rejected: An upgrade can only be started "
"when %s is active." % constants.CONTROLLER_0_HOSTNAME))
# There must not already be an upgrade in progress
try:
pecan.request.dbapi.software_upgrade_get_one()
except exception.NotFound:
pass
else:
raise wsme.exc.ClientSideError(_(
"upgrade-start rejected: An upgrade is already in progress."))
# Determine the from_load and to_load
loads = pecan.request.dbapi.load_get_list()
from_load = cutils.get_active_load(loads)
from_version = from_load.software_version
to_load = cutils.get_imported_load(loads)
to_version = to_load.software_version
controller_0 = pecan.request.dbapi.ihost_get_by_hostname(
constants.CONTROLLER_0_HOSTNAME)
force = body.get('force', False) is True
try:
# Set the upgrade flag in VIM
# This prevents VM changes during the upgrade and health checks
if utils.get_system_mode() != constants.SYSTEM_MODE_SIMPLEX:
vim_api.set_vim_upgrade_state(controller_0, True)
except Exception as e:
LOG.exception(e)
raise wsme.exc.ClientSideError(_(
"upgrade-start rejected: Unable to set VIM upgrade state"))
success, output = pecan.request.rpcapi.get_system_health(
pecan.request.context, force=force, upgrade=True)
if not success:
LOG.info("Health audit failure during upgrade start. Health "
"query results: %s" % output)
if os.path.exists(constants.SYSINV_RUNNING_IN_LAB) and force:
LOG.info("Running in lab, ignoring health errors.")
else:
vim_api.set_vim_upgrade_state(controller_0, False)
raise wsme.exc.ClientSideError(_(
"upgrade-start rejected: System is not in a valid state "
"for upgrades. Run system health-query-upgrade for more "
"details."))
# Create upgrade record. Must do this before the prepare_upgrade so
# the upgrade record exists when the database is dumped.
create_values = {'from_load': from_load.id,
'to_load': to_load.id,
'state': constants.UPGRADE_STARTING}
new_upgrade = None
try:
new_upgrade = pecan.request.dbapi.software_upgrade_create(
create_values)
except Exception as ex:
vim_api.set_vim_upgrade_state(controller_0, False)
LOG.exception(ex)
raise
# Prepare for upgrade
LOG.info("Starting upgrade from release: %s to release: %s" %
(from_version, to_version))
try:
pecan.request.rpcapi.start_upgrade(pecan.request.context,
new_upgrade)
except Exception as ex:
vim_api.set_vim_upgrade_state(controller_0, False)
pecan.request.dbapi.software_upgrade_destroy(new_upgrade.uuid)
LOG.exception(ex)
raise
return Upgrade.convert_with_links(new_upgrade)
@cutils.synchronized(LOCK_NAME)
@wsme.validate([UpgradePatchType])
@wsme_pecan.wsexpose(Upgrade, body=[UpgradePatchType])
def patch(self, patch):
"""Updates attributes of Software Upgrade."""
updates = self._get_updates(patch)
# Get the current upgrade
try:
upgrade = pecan.request.dbapi.software_upgrade_get_one()
except exception.NotFound:
raise wsme.exc.ClientSideError(_(
"operation rejected: An upgrade is not in progress."))
to_load = pecan.request.dbapi.load_get(upgrade.to_load)
to_version = to_load.software_version
if updates['state'] == constants.UPGRADE_ABORTING:
# Make sure upgrade wasn't already aborted
if upgrade.state in [constants.UPGRADE_ABORTING,
constants.UPGRADE_ABORTING_ROLLBACK]:
raise wsme.exc.ClientSideError(_(
"upgrade-abort rejected: Upgrade already aborted "))
# Abort the upgrade
rpc_upgrade = pecan.request.rpcapi.abort_upgrade(
pecan.request.context, upgrade)
return Upgrade.convert_with_links(rpc_upgrade)
# if an activation is requested, make sure we are not already in
# activating state or have already activated
elif updates['state'] == constants.UPGRADE_ACTIVATION_REQUESTED:
if upgrade.state in [constants.UPGRADE_ACTIVATING,
constants.UPGRADE_ACTIVATION_COMPLETE]:
raise wsme.exc.ClientSideError(_(
"upgrade-activate rejected: "
"Upgrade already activating or activated."))
hosts = pecan.request.dbapi.ihost_get_list()
# All hosts must be unlocked and enabled, and running the new
# release
for host in hosts:
if host['administrative'] != constants.ADMIN_UNLOCKED or \
host['operational'] != constants.OPERATIONAL_ENABLED:
raise wsme.exc.ClientSideError(_(
"upgrade-activate rejected: All hosts must be unlocked"
" and enabled before the upgrade can be activated."))
for host in hosts:
host_upgrade = objects.host_upgrade.get_by_host_id(
pecan.request.context, host.id)
if (host_upgrade.target_load != to_load.id or
host_upgrade.software_load != to_load.id):
raise wsme.exc.ClientSideError(_(
"upgrade-activate rejected: All hosts must be "
"upgraded before the upgrade can be activated."))
# we need to make sure the state is updated before calling the rpc
rpc_upgrade = pecan.request.dbapi.software_upgrade_update(
upgrade.uuid, updates)
pecan.request.rpcapi.activate_upgrade(pecan.request.context,
upgrade)
# make sure the to/from loads are in the correct state
pecan.request.dbapi.set_upgrade_loads_state(
upgrade,
constants.ACTIVE_LOAD_STATE,
constants.IMPORTED_LOAD_STATE)
LOG.info("Setting SW_VERSION to release: %s" % to_version)
system = pecan.request.dbapi.isystem_get_one()
pecan.request.dbapi.isystem_update(
system.uuid, {'software_version': to_version})
return Upgrade.convert_with_links(rpc_upgrade)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(Upgrade)
def delete(self):
"""Complete upgrade and delete Software Upgrade instance."""
# There must be an upgrade in progress
try:
upgrade = pecan.request.dbapi.software_upgrade_get_one()
except exception.NotFound:
raise wsme.exc.ClientSideError(_(
"upgrade-complete rejected: An upgrade is not in progress."))
# Only complete the upgrade from controller-0. This is to ensure that
# we can clean up all the upgrades related files, some of which are
# local to controller-0.
if socket.gethostname() != constants.CONTROLLER_0_HOSTNAME:
raise wsme.exc.ClientSideError(_(
"upgrade-complete rejected: An upgrade can only be completed "
"when %s is active." % constants.CONTROLLER_0_HOSTNAME))
from_load = pecan.request.dbapi.load_get(upgrade.from_load)
if upgrade.state == constants.UPGRADE_ACTIVATION_COMPLETE:
# Complete the upgrade
current_abort_state = upgrade.state
upgrade = pecan.request.dbapi.software_upgrade_update(
upgrade.uuid, {'state': constants.UPGRADE_COMPLETING})
try:
pecan.request.rpcapi.complete_upgrade(
pecan.request.context, upgrade, current_abort_state)
except Exception as ex:
LOG.exception(ex)
pecan.request.dbapi.software_upgrade_update(
upgrade.uuid,
{'state': constants.UPGRADE_ACTIVATION_COMPLETE})
raise
elif upgrade.state in [constants.UPGRADE_ABORTING,
constants.UPGRADE_ABORTING_ROLLBACK]:
# All hosts must be running the old release
hosts = pecan.request.dbapi.ihost_get_list()
for host in hosts:
host_upgrade = objects.host_upgrade.get_by_host_id(
pecan.request.context, host.id)
if (host_upgrade.target_load != from_load.id or
host_upgrade.software_load != from_load.id):
raise wsme.exc.ClientSideError(_(
"upgrade-abort rejected: All hosts must be downgraded "
"before the upgrade can be aborted."))
current_abort_state = upgrade.state
upgrade = pecan.request.dbapi.software_upgrade_update(
upgrade.uuid, {'state': constants.UPGRADE_ABORT_COMPLETING})
try:
pecan.request.rpcapi.complete_upgrade(
pecan.request.context, upgrade, current_abort_state)
except Exception as ex:
LOG.exception(ex)
pecan.request.dbapi.software_upgrade_update(
upgrade.uuid, {'state': current_abort_state})
raise
else:
raise wsme.exc.ClientSideError(_(
"upgrade-complete rejected: An upgrade can only be completed "
"when in the %s or %s state." %
(constants.UPGRADE_ACTIVATION_COMPLETE,
constants.UPGRADE_ABORTING)))
return Upgrade.convert_with_links(upgrade)
@wsme_pecan.wsexpose(wtypes.text, six.text_type)
def in_upgrade(self, uuid):
# uuid is added here for potential future use
try:
upgrade = pecan.request.dbapi.software_upgrade_get_one()
# We will wipe all the disks in the case of a host reinstall
# during a downgrade.
if upgrade.state in [constants.UPGRADE_ABORTING_ROLLBACK]:
LOG.info("in_upgrade status. Aborting upgrade, host reinstall")
return False
except exception.NotFound:
return False
return True