Add inactive param for import-load on conductor

This change added a parameter called 'inactive' to the load-import workflow on the conductor to allow the import of a previous release (ISO).

Test Plan:
PASS: (AIO-SX) failed to import the current version
PASS: (AIO-SX) failed to import the current version with
active param

PASS: (AIO-SX) import the new version
PASS: (AIO-SX) import new version with local param

PASS: (AIO-SX) failed to import the previous release
PASS: (AIO-SX) failed to import the previous release
with inactive param

PASS: DC (--os-region-name SystemController) success to import
currently version with active param
PASS: DC (--os-region-name SystemController) failed to import
currently version

PASS: DC (--os-region-name SystemController) import new version
PASS: DC (--os-region-name SystemController) import new version
with local param

PASS: DC (--os-region-name SystemController) import previous
release with inactive param
PASS: DC (--os-region-name SystemController) failed to import
previous release

PASS: DC (--os-region-name SystemController) extracted ISO files
to the controller (/var/www/pages/feed/rel-version)

PASS: DC (--os-region-name SystemController) failed import
previous release with inactive and active params
PASS: DC (--os-region-name SystemController) import previous
release (centos) with inactive param in a debian.

Story: 2010611
Task: 47509

Signed-off-by: Guilherme Schons <guilherme.dossantosschons@windriver.com>
Change-Id: I7c9a398a22ac91ea401aa93d6d8f35fa5401e9b2
This commit is contained in:
Guilherme Schons 2023-02-24 11:46:51 -03:00
parent dccb65aacb
commit 667804fad0
8 changed files with 421 additions and 16 deletions

View File

@ -843,6 +843,7 @@ ACTION_UPDATE_JOURNAL = "update"
MNT_DIR = '/tmp/mnt'
ACTIVE_LOAD_STATE = 'active'
INACTIVE_LOAD_STATE = 'inactive'
IMPORTING_LOAD_STATE = 'importing'
IMPORTED_LOAD_STATE = 'imported'
IMPORTED_METADATA_LOAD_STATE = 'imported-metadata'
@ -860,6 +861,10 @@ LOAD_SIGNATURE = 'path_to_sig'
IMPORT_LOAD_FILES = [LOAD_ISO, LOAD_SIGNATURE]
LOAD_FILES_STAGING_DIR = '/scratch/tmp_load'
STAGING_LOAD_FILES_REMOVAL_WAIT_TIME = 30
CURRENT_METADATA_FILE_PATH = '/usr/rootdirs/opt/upgrades/metadata.xml'
ACTIVE_LOAD_IMPORT = 'active'
INACTIVE_LOAD_IMPORT = 'inactive'
# Ceph
CEPH_HEALTH_OK = 'HEALTH_OK'

View File

@ -1299,6 +1299,7 @@ def validate_load_for_delete(load):
valid_delete_states = [
constants.IMPORTED_LOAD_STATE,
constants.IMPORTED_METADATA_LOAD_STATE,
constants.INACTIVE_LOAD_STATE,
constants.ERROR_LOAD_STATE,
constants.DELETING_LOAD_STATE
]

View File

@ -51,6 +51,7 @@ from contextlib import contextmanager
from datetime import datetime
from datetime import timedelta
from distutils.util import strtobool
from distutils.version import LooseVersion
from copy import deepcopy
import tsconfig.tsconfig as tsc
@ -11825,6 +11826,47 @@ class ConductorManager(service.PeriodicService):
LOG.error("Host: %s not found in database" % ihost_id)
return None
def _get_current_supported_upgrade_versions(self):
supported_versions = []
try:
metadata_file = open(constants.CURRENT_METADATA_FILE_PATH, 'r')
root = ElementTree.fromstring(metadata_file.read())
metadata_file.close()
except Exception:
raise exception.SysinvException(_(
"Unable to read metadata file from current version"))
supported_upgrades_elm = root.find('supported_upgrades')
if not supported_upgrades_elm:
raise exception.SysinvException(
_("Invalid Metadata XML from current version"))
upgrade_paths = supported_upgrades_elm.findall('upgrade')
for upgrade_element in upgrade_paths:
valid_from_version = upgrade_element.findtext('version')
versions = valid_from_version.split(",")
supported_versions.extend(versions)
return supported_versions
def _create_symlink_install_uuid(self, current_version):
"""
If the current version is Debian and the imported load
is Centos, the install_uuid path is different. It's
necessary to create a symlink for import.sh to find it.
"""
centos_feed_path = '/www/pages/feed/rel-%s' % current_version
os.makedirs(centos_feed_path, exist_ok=True)
src = '/var/www/pages/feed/rel-%s/install_uuid' % current_version
dst = '%s/install_uuid' % centos_feed_path
os.symlink(src, dst)
def _import_load_error(self, new_load):
"""
Update the load state to 'error' in the database
@ -11851,7 +11893,7 @@ class ConductorManager(service.PeriodicService):
shutil.rmtree(mntdir)
def start_import_load(self, context, path_to_iso, path_to_sig,
import_active=False):
import_type=None):
"""
Mount the ISO and validate the load for import
"""
@ -11859,7 +11901,7 @@ class ConductorManager(service.PeriodicService):
active_load = cutils.get_active_load(loads)
if not import_active:
if import_type != constants.ACTIVE_LOAD_IMPORT:
cutils.validate_loads_for_import(loads)
current_version = active_load.software_version
@ -11887,6 +11929,7 @@ class ConductorManager(service.PeriodicService):
"Unable to mount iso"))
metadata_file_path = mntdir + '/upgrades/metadata.xml'
if not os.path.exists(metadata_file_path):
self._unmount_iso(mounted_iso, mntdir)
raise exception.SysinvException(_("Metadata file not found"))
@ -11907,7 +11950,7 @@ class ConductorManager(service.PeriodicService):
new_version = root.findtext('version')
if import_active:
if import_type == constants.ACTIVE_LOAD_IMPORT:
if new_version != current_version:
raise exception.SysinvException(
_("Active version and import version must match (%s)")
@ -11915,6 +11958,7 @@ class ConductorManager(service.PeriodicService):
# return the matching (active) load in the database
loads = self.dbapi.load_get_list()
for load in loads:
if load.software_version == new_version:
break
@ -11924,6 +11968,19 @@ class ConductorManager(service.PeriodicService):
return load
if import_type == constants.INACTIVE_LOAD_IMPORT:
if LooseVersion(new_version) >= LooseVersion(current_version):
raise exception.SysinvException(
_("Inactive version must be less than the current version (%s)")
% current_version)
supported_versions = self._get_current_supported_upgrade_versions()
if new_version not in supported_versions:
raise exception.SysinvException(
_("Inactive version must be upgradable to the current version (%s)")
% current_version)
if new_version == current_version:
raise exception.SysinvException(
_("Active version and import version match (%s)")
@ -11946,7 +12003,7 @@ class ConductorManager(service.PeriodicService):
upgrade_path = upgrade_element
break
if not path_found:
if not path_found and import_type != constants.INACTIVE_LOAD_IMPORT:
raise exception.SysinvException(
_("No valid upgrade path found"))
@ -11958,9 +12015,12 @@ class ConductorManager(service.PeriodicService):
patch['compatible_version'] = current_version
required_patches = []
patch_elements = upgrade_path.findall('required_patch')
for patch_element in patch_elements:
required_patches.append(patch_element.text)
if upgrade_path:
patch_elements = upgrade_path.findall('required_patch')
for patch_element in patch_elements:
required_patches.append(patch_element.text)
patch['required_patches'] = "\n".join(required_patches)
# create the new imported load in the database
@ -11968,7 +12028,8 @@ class ConductorManager(service.PeriodicService):
return new_load
def import_load(self, context, path_to_iso, new_load):
def import_load(self, context, path_to_iso, new_load,
import_type=None):
"""
Run the import script and add the load to the database
"""
@ -11997,6 +12058,13 @@ class ConductorManager(service.PeriodicService):
self._import_load_error(new_load)
raise exception.SysinvException(_("Unable to mount iso"))
state = constants.IMPORTED_LOAD_STATE
if import_type == constants.INACTIVE_LOAD_IMPORT:
active_load = cutils.get_active_load(loads)
self._create_symlink_install_uuid(active_load.software_version)
state = constants.INACTIVE_LOAD_STATE
# Run the upgrade script
with open(os.devnull, "w") as fnull:
try:
@ -12014,8 +12082,7 @@ class ConductorManager(service.PeriodicService):
# Update the load status in the database
try:
self.dbapi.load_update(new_load['id'],
{'state': constants.IMPORTED_LOAD_STATE})
self.dbapi.load_update(new_load['id'], {'state': state})
except exception.SysinvException as e:
LOG.exception(e)

View File

@ -1204,14 +1204,16 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy):
ihost_uuid=ihost_uuid))
def start_import_load(self, context, path_to_iso, path_to_sig,
import_active=False, timeout=180):
import_type=None, timeout=180):
"""Synchronously, mount the ISO and validate the load for import
:param context: request context.
:param path_to_iso: the file path of the iso on this host
:param path_to_sig: the file path of the iso's detached signature on
this host
:param import_active: boolean allow import of active load
:param import_type: the type of the import, the possible values are
constants.ACTIVE_LOAD_IMPORT for active load or
constants.INACTIVE_LOAD_IMPORT for inactive load.
:param timeout: rpc call timeout in seconds
:returns: the newly create load object.
"""
@ -1219,21 +1221,24 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy):
self.make_msg('start_import_load',
path_to_iso=path_to_iso,
path_to_sig=path_to_sig,
import_active=import_active),
import_type=import_type),
timeout=timeout)
def import_load(self, context, path_to_iso, new_load):
def import_load(self, context, path_to_iso, new_load,
import_type=None):
"""Asynchronously, import a load and add it to the database
:param context: request context.
:param path_to_iso: the file path of the iso on this host
:param new_load: the load object
:param import_type: the type of the import (active or inactive)
:returns: none.
"""
return self.cast(context,
self.make_msg('import_load',
path_to_iso=path_to_iso,
new_load=new_load))
new_load=new_load,
import_type=import_type))
def delete_load(self, context, load_id):
"""Asynchronously, cleanup a load from both controllers

View File

@ -0,0 +1 @@
It'S Ok

View File

@ -0,0 +1 @@
Simple Is Good

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<build>
<version>0.1</version>
<supported_upgrades>
<upgrade>
<version>0.0</version>
<required_patch>PATCH_0001</required_patch>
</upgrade>
</supported_upgrades>
</build>

View File

@ -25,10 +25,13 @@
import copy
import mock
import os.path
import tempfile
import uuid
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from shutil import copy as shutil_copy
from shutil import rmtree
from fm_api import constants as fm_constants
from oslo_context import context
@ -40,7 +43,9 @@ from sysinv.common import exception
from sysinv.common import kubernetes
from sysinv.common import utils as cutils
from sysinv.conductor import manager
from sysinv.db.sqlalchemy.api import Connection
from sysinv.db import api as dbapi
from sysinv.objects.load import Load
from sysinv.tests.db import base
from sysinv.tests.db import utils
@ -4229,8 +4234,318 @@ class ManagerTestCase(base.DbTestCase):
self.assertEqual(endpoints, config_dict)
class ManagerTestCaseInternal(base.BaseHostTestCase):
@mock.patch('sysinv.conductor.manager.verify_files', lambda x, y: True)
@mock.patch('sysinv.conductor.manager.cutils.ISO', mock.MagicMock())
class ManagerStartLoadImportTest(base.BaseHostTestCase):
def setUp(self):
super(ManagerStartLoadImportTest, self).setUp()
# Set up objects for testing
self.service = manager.ConductorManager('test-host', 'test-topic')
self.service.dbapi = dbapi.get_instance()
self.context = context.get_admin_context()
self.tmp_dir = tempfile.mkdtemp(dir='/tmp')
patch_mkdtemp = mock.patch('tempfile.mkdtemp')
mock_mkdtemp = patch_mkdtemp.start()
mock_mkdtemp.return_value = self.tmp_dir
self.addCleanup(patch_mkdtemp.stop)
self.upgrades_path = '%s/upgrades' % self.tmp_dir
os.makedirs(self.upgrades_path, exist_ok=True)
self.metadata = os.path.join(
os.path.dirname(__file__), "data", "metadata.xml"
)
shutil_copy(self.metadata, self.upgrades_path)
self.iso = os.path.join(
os.path.dirname(__file__), "data", "bootimage.iso"
)
self.sig = os.path.join(
os.path.dirname(__file__), "data", "bootimage.sig"
)
def test_start_import_load(self):
result = self.service.start_import_load(
self.context,
path_to_iso=self.iso,
path_to_sig=self.sig,
)
self.assertIsInstance(result, Load)
self.assertEqual(result.state, constants.IMPORTING_LOAD_STATE)
@mock.patch('sysinv.conductor.manager.cutils.get_active_load')
def test_start_import_load_same_version(self, mock_get_active_load):
mock_get_active_load.return_value.software_version = '0.1'
self.assertRaises(
exception.SysinvException,
self.service.start_import_load,
self.context,
self.iso,
self.sig,
)
@mock.patch('sysinv.conductor.manager.cutils.get_active_load')
def test_start_import_load_invalid_from_version(self, mock_get_active_load):
mock_get_active_load.return_value.software_version = '0.2'
self.assertRaises(
exception.SysinvException,
self.service.start_import_load,
self.context,
self.iso,
self.sig,
)
@mock.patch.object(Connection, 'load_get_list')
@mock.patch('sysinv.conductor.manager.cutils.get_active_load')
def test_start_import_load_active(self, mock_get_active_load, mock_load_get_list):
mock_get_active_load.return_value.software_version = '0.1'
load = utils.create_test_load(**{"software_version": "0.1"})
mock_load_get_list.return_value = [load]
result = self.service.start_import_load(
self.context,
path_to_iso=self.iso,
path_to_sig=self.sig,
import_type=constants.ACTIVE_LOAD_IMPORT,
)
self.assertIsInstance(result, Load)
self.assertEqual(result.state, constants.ACTIVE_LOAD_STATE)
def test_start_import_load_active_invalid_version(self):
self.assertRaises(
exception.SysinvException,
self.service.start_import_load,
self.context,
self.iso,
self.sig,
import_type=constants.ACTIVE_LOAD_IMPORT,
)
@mock.patch.object(Connection, 'load_get_list')
def test_start_import_load_active_load_not_found(self, mock_load_get_list):
load = utils.create_test_load(**{"software_version": "0.1"})
mock_load_get_list.side_effect = [[load], []]
self.assertRaises(
exception.SysinvException,
self.service.start_import_load,
self.context,
self.iso,
self.sig,
import_type=constants.ACTIVE_LOAD_IMPORT,
)
@mock.patch('sysinv.conductor.manager.cutils.get_active_load')
def test_start_import_load_inactive(self, mock_get_active_load):
mock_get_active_load.return_value.software_version = '0.2'
metadata_orig = open(self.metadata, 'r').read()
metadata_fake = b'''
<build>\n<version>0.2</version>\n<supported_upgrades>
\n<upgrade>\n<version>0.1</version>\n<required_patch>PATCH_0001
</required_patch>\n</upgrade>\n</supported_upgrades>\n</build>
'''
mock_files = [
mock.mock_open(read_data=metadata_orig).return_value,
mock.mock_open(read_data=metadata_fake).return_value,
]
mock_open = mock.mock_open()
mock_open.side_effect = mock_files
with mock.patch('builtins.open', mock_open):
result = self.service.start_import_load(
self.context,
path_to_iso=self.iso,
path_to_sig=self.sig,
import_type=constants.INACTIVE_LOAD_IMPORT,
)
self.assertIsInstance(result, Load)
self.assertEqual(result.state, constants.IMPORTING_LOAD_STATE)
@mock.patch('sysinv.conductor.manager.open')
@mock.patch('sysinv.conductor.manager.cutils.get_active_load')
def test_start_import_load_inactive_incompatible_version(self, mock_get_active_load, mock_open):
mock_get_active_load.return_value.software_version = '0.3'
metadata_orig = open(self.metadata, 'r').read()
metadata_fake = b'''
<build>\n<version>0.3</version>\n<supported_upgrades>
\n<upgrade>\n<version>0.2</version>\n<required_patch>PATCH_0001
</required_patch>\n</upgrade>\n</supported_upgrades>\n</build>
'''
mock_files = [
mock.mock_open(read_data=metadata_orig).return_value,
mock.mock_open(read_data=metadata_fake).return_value,
]
mock_open.side_effect = mock_files
self.assertRaises(
exception.SysinvException,
self.service.start_import_load,
self.context,
path_to_iso=self.iso,
path_to_sig=self.sig,
import_type=constants.INACTIVE_LOAD_IMPORT,
)
def test_start_import_load_invalid_path(self):
self.assertRaises(
exception.SysinvException,
self.service.start_import_load,
self.context,
'invalid/path/bootimage.iso',
'invalid/path/bootimage.sig',
)
def test_start_import_load_invalid_files(self):
with mock.patch('sysinv.conductor.manager.verify_files', lambda x, y: False):
self.assertRaises(
exception.SysinvException,
self.service.start_import_load,
self.context,
self.iso,
self.sig,
)
def test_start_import_load_without_metadata(self):
rmtree(self.upgrades_path, ignore_errors=True)
self.assertRaises(
exception.SysinvException,
self.service.start_import_load,
self.context,
self.iso,
self.sig,
)
def test_start_import_load_invalid_metadata(self):
iso = os.path.join(
os.path.dirname(__file__), "data", "bootimage.iso"
)
shutil_copy(iso, self.upgrades_path)
os.rename(
'%s/bootimage.iso' % self.upgrades_path,
'%s/metadata.xml' % self.upgrades_path,
)
self.assertRaises(
exception.SysinvException,
self.service.start_import_load,
self.context,
self.iso,
self.sig,
)
@mock.patch('sysinv.conductor.manager.subprocess', mock.MagicMock())
@mock.patch('sysinv.conductor.manager.cutils.ISO', mock.MagicMock())
class ManagerLoadImportTest(base.BaseHostTestCase):
def setUp(self):
super(ManagerLoadImportTest, self).setUp()
# Set up objects for testing
self.service = manager.ConductorManager('test-host', 'test-topic')
self.service.dbapi = dbapi.get_instance()
self.context = context.get_admin_context()
self.iso = os.path.join(
os.path.dirname(__file__), "data", "bootimage.iso"
)
self.load = utils.create_test_load(
**{"software_version": "0.1"}
)
load_update = mock.patch.object(Connection, 'load_update')
self.mock_load_update = load_update.start()
self.mock_load_update.return_value = mock.MagicMock()
self.addCleanup(load_update.stop)
def test_import_load(self):
result = self.service.import_load(
self.context,
path_to_iso=self.iso,
new_load=self.load,
)
self.assertTrue(result)
self.mock_load_update.assert_called_once_with(
mock.ANY,
{'state': constants.IMPORTED_LOAD_STATE},
)
@mock.patch('sysinv.conductor.manager.os.symlink', mock.Mock())
@mock.patch('sysinv.conductor.manager.os.makedirs', mock.Mock())
def test_import_load_inactive(self):
result = self.service.import_load(
self.context,
path_to_iso=self.iso,
new_load=self.load,
import_type=constants.INACTIVE_LOAD_IMPORT,
)
self.assertTrue(result)
self.mock_load_update.assert_called_once_with(
mock.ANY,
{'state': constants.INACTIVE_LOAD_STATE},
)
def test_import_load_empty_new_load(self):
self.assertRaises(
exception.SysinvException,
self.service.import_load,
self.context,
path_to_iso=self.iso,
new_load=None,
)
self.mock_load_update.assert_not_called()
def test_import_load_invalid_iso_path(self):
self.assertRaises(
exception.SysinvException,
self.service.import_load,
self.context,
path_to_iso='invalid',
new_load=self.load,
)
self.mock_load_update.assert_called_once_with(
mock.ANY,
{'state': constants.ERROR_LOAD_STATE},
)
def test_import_load_load_update_failed(self):
self.mock_load_update.side_effect = exception.SysinvException()
self.assertRaises(
exception.SysinvException,
self.service.import_load,
self.context,
path_to_iso=self.iso,
new_load=self.load,
)
self.mock_load_update.assert_called_once_with(
mock.ANY,
{'state': constants.IMPORTED_LOAD_STATE},
)
class ManagerTestCaseInternal(base.BaseHostTestCase):
def setUp(self):
super(ManagerTestCaseInternal, self).setUp()