Merge "Add inactive param for import-load on sysinv api"

This commit is contained in:
Zuul 2023-03-07 18:20:21 +00:00 committed by Gerrit Code Review
commit e9a8c70268
5 changed files with 271 additions and 37 deletions

View File

@ -336,10 +336,11 @@ class LoadController(rest.RestController):
" %s is active.")
% constants.CONTROLLER_0_HOSTNAME)
system_controller_import_active = False
req_content = dict()
load_files = dict()
is_multiform_req = True
import_type = None
# Request coming from dc-api-proxy is not multiform, file transfer is handled
# by dc-api-proxy, the request contains only the vault file location
if request.content_type == "application/json":
@ -351,15 +352,27 @@ class LoadController(rest.RestController):
if not req_content:
raise wsme.exc.ClientSideError(_("Empty request."))
if 'active' in req_content:
if req_content['active'] == 'true':
if pecan.request.dbapi.isystem_get_one().\
distributed_cloud_role == \
constants.DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER:
LOG.info("System Controller allow start import_load")
system_controller_import_active = True
active = req_content.get('active')
inactive = req_content.get('inactive')
self._check_existing_loads(active_import=system_controller_import_active)
if active == 'true' and inactive == 'true':
raise wsme.exc.ClientSideError(_("Invalid use of --active and"
" --inactive arguments at"
" the same time."))
if active == 'true' or inactive == 'true':
isystem = pecan.request.dbapi.isystem_get_one()
if isystem.distributed_cloud_role == \
constants.DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER:
LOG.info("System Controller allow start import_load")
if active == 'true':
import_type = constants.ACTIVE_LOAD_IMPORT
elif inactive == 'true':
import_type = constants.INACTIVE_LOAD_IMPORT
self._check_existing_loads(import_type=import_type)
try:
for file in constants.IMPORT_LOAD_FILES:
@ -395,18 +408,21 @@ class LoadController(rest.RestController):
pecan.request.context,
load_files[constants.LOAD_ISO],
load_files[constants.LOAD_SIGNATURE],
system_controller_import_active)
import_type,
)
if new_load is None:
raise wsme.exc.ClientSideError(_("Error importing load. Load not found"))
if not system_controller_import_active:
if import_type != constants.ACTIVE_LOAD_IMPORT:
# Signature and upgrade path checks have passed, make rpc call
# to the conductor to run import script in the background.
pecan.request.rpcapi.import_load(
pecan.request.context,
load_files[constants.LOAD_ISO],
new_load)
new_load,
import_type,
)
except (rpc.common.Timeout, common.RemoteError) as e:
if os.path.isdir(constants.LOAD_FILES_STAGING_DIR):
shutil.rmtree(constants.LOAD_FILES_STAGING_DIR)
@ -454,34 +470,50 @@ class LoadController(rest.RestController):
return load.convert_with_links(new_load)
def _check_existing_loads(self, active_import=False):
def _check_existing_loads(self, import_type=None):
# Only are allowed at one time:
# - the active load
# - an imported load regardless of its current state
# - an inactive load.
loads = pecan.request.dbapi.load_get_list()
# Only 2 loads are allowed at one time: the active load
# and an imported load regardless of its current state
# (e.g. importing, error, deleting).
load_state = None
if len(loads) > constants.IMPORTED_LOAD_MAX_COUNT:
for load in loads:
if load.state != constants.ACTIVE_LOAD_STATE:
load_state = load.state
else:
if len(loads) <= constants.IMPORTED_LOAD_MAX_COUNT:
return
if load_state == constants.ERROR_LOAD_STATE:
err_msg = _("Please remove the load in error state "
"before importing a new one.")
elif load_state == constants.DELETING_LOAD_STATE:
err_msg = _("Please wait for the current load delete "
"to complete before importing a new one.")
elif not active_import:
# Already imported or being imported
err_msg = _("Max number of loads (2) reached. Please "
"remove the old or unused load before "
"importing a new one.")
else:
return
raise wsme.exc.ClientSideError(err_msg)
for load in loads:
if load.state == constants.ACTIVE_LOAD_STATE:
continue
load_state = load.state
if load_state == constants.ERROR_LOAD_STATE:
err_msg = _("Please remove the load in error state "
"before importing a new one.")
elif load_state == constants.DELETING_LOAD_STATE:
err_msg = _("Please wait for the current load delete "
"to complete before importing a new one.")
elif load_state == constants.INACTIVE_LOAD_STATE:
if import_type != constants.INACTIVE_LOAD_IMPORT:
continue
err_msg = _("An inactived load already exists. "
"Please, remove the inactive load "
"before trying to import a new one.")
elif import_type == constants.ACTIVE_LOAD_IMPORT or \
import_type == constants.INACTIVE_LOAD_IMPORT:
continue
elif not err_msg:
# Already imported or being imported
err_msg = _("Max number of loads (2) reached. Please "
"remove the old or unused load before "
"importing a new one.")
raise wsme.exc.ClientSideError(err_msg)
@cutils.synchronized(LOCK_NAME)
@wsme.validate(six.text_type, [LoadPatchType])

View File

@ -851,7 +851,8 @@ ERROR_LOAD_STATE = 'error'
DELETING_LOAD_STATE = 'deleting'
IMPORTED_LOAD_STATES = [
IMPORTED_LOAD_STATE,
IMPORTED_METADATA_LOAD_STATE
IMPORTED_METADATA_LOAD_STATE,
INACTIVE_LOAD_STATE,
]
DELETE_LOAD_SCRIPT = '/etc/sysinv/upgrades/delete_load.sh'

View File

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

View File

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

View File

@ -0,0 +1,199 @@
#
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
'''
Tests for the API /loads/import_load methods.
'''
import os
import webtest.app
from mock import patch
from mock import MagicMock
from sysinv.common import constants
from sysinv.tests.api import base
from sysinv.tests.db import utils
from sysinv.openstack.common.rpc import common
class FakeConductorAPI(object):
def __init__(self):
self.start_import_load = MagicMock()
self.start_import_load.return_value = utils.create_test_load()
self.import_load = MagicMock()
class TestLoad(base.FunctionalTest):
def setUp(self):
super(TestLoad, self).setUp()
self.API_HEADERS = {'User-Agent': 'sysinv-test'}
self.PATH_PREFIX = '/loads'
conductor_api = patch('sysinv.conductor.rpcapiproxy.ConductorAPI')
self.mock_conductor_api = conductor_api.start()
self.fake_conductor_api = FakeConductorAPI()
self.mock_conductor_api.return_value = self.fake_conductor_api
self.addCleanup(conductor_api.stop)
socket_gethostname = patch('socket.gethostname')
self.mock_socket_gethostname = socket_gethostname.start()
self.mock_socket_gethostname.return_value = 'controller-0'
self.addCleanup(socket_gethostname.stop)
# TODO: Improve these unit test to don't mock this method.
upload_file = patch(
'sysinv.api.controllers.v1.load.LoadController._upload_file'
)
self.mock_upload_file = upload_file.start()
self.mock_upload_file.return_value = '/tmp/iso/'
self.addCleanup(upload_file.stop)
@patch('sysinv.common.utils.is_space_available', lambda x, y: True)
class TestLoadImport(TestLoad):
def setUp(self):
super(TestLoadImport, self).setUp()
path_import = '%s/import_load' % self.PATH_PREFIX
iso = os.path.join(
os.path.dirname(__file__), "data", "bootimage.iso"
)
sig = os.path.join(
os.path.dirname(__file__), "data", "bootimage.sig"
)
self.request_json = {
'path': path_import,
'params': {
'path_to_iso': iso,
'path_to_sig': sig,
'active': 'false',
'inactive': 'false',
},
'headers': self.API_HEADERS,
}
upload_files = [('path_to_iso', iso), ('path_to_sig', sig)]
self.request_multiform = {
'path': path_import,
'params': {'active': 'false', 'inactive': 'false'},
'upload_files': upload_files,
'headers': self.API_HEADERS,
'expect_errors': False,
}
def _assert_load(self, load):
self.assertEqual(load['software_version'], utils.SW_VERSION)
self.assertEqual(load['compatible_version'], 'N/A')
self.assertEqual(load['required_patches'], 'N/A')
self.assertEqual(load['state'], constants.ACTIVE_LOAD_STATE)
def test_load_import(self):
response = self.post_with_files(**self.request_multiform)
self._assert_load(response.json)
self.fake_conductor_api.start_import_load.assert_called_once()
self.fake_conductor_api.import_load.assert_called_once()
def test_load_import_local(self):
response = self.post_json(**self.request_json)
self._assert_load(response.json)
self.fake_conductor_api.start_import_load.assert_called_once()
self.fake_conductor_api.import_load.assert_called_once()
def test_load_import_active(self):
isystem_get_one = self.dbapi.isystem_get_one
self.dbapi.isystem_get_one = MagicMock()
self.dbapi.isystem_get_one.return_value.distributed_cloud_role = \
constants.DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER
self.request_multiform['params']['active'] = 'true'
response = self.post_with_files(**self.request_multiform)
self.dbapi.isystem_get_one = isystem_get_one
self._assert_load(response.json)
self.fake_conductor_api.start_import_load.assert_called_once()
self.fake_conductor_api.import_load.assert_not_called()
def test_load_import_inactive(self):
isystem_get_one = self.dbapi.isystem_get_one
self.dbapi.isystem_get_one = MagicMock()
self.dbapi.isystem_get_one.return_value.distributed_cloud_role = \
constants.DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER
self.request_multiform['params']['inactive'] = 'true'
response = self.post_with_files(**self.request_multiform)
self.dbapi.isystem_get_one = isystem_get_one
self._assert_load(response.json)
self.fake_conductor_api.start_import_load.assert_called_once()
self.fake_conductor_api.import_load.assert_called_once()
def test_load_import_invalid_hostname(self):
self.mock_socket_gethostname.return_value = 'controller-1'
self.assertRaises(
webtest.app.AppError,
self.post_with_files,
**self.request_multiform,
)
self.fake_conductor_api.start_import_load.assert_not_called()
self.fake_conductor_api.import_load.assert_not_called()
def test_load_import_empty_request(self):
self.request_multiform['upload_files'] = None
self.assertRaises(
webtest.app.AppError,
self.post_with_files,
**self.request_multiform,
)
self.fake_conductor_api.start_import_load.assert_not_called()
self.fake_conductor_api.import_load.assert_not_called()
def test_load_import_missing_required_file(self):
self.request_multiform['upload_files'].pop()
self.assertRaises(
webtest.app.AppError,
self.post_with_files,
**self.request_multiform,
)
self.fake_conductor_api.start_import_load.assert_not_called()
self.fake_conductor_api.import_load.assert_not_called()
def test_load_import_failed_to_create_load_conductor(self):
self.fake_conductor_api.start_import_load.return_value = None
self.assertRaises(
webtest.app.AppError,
self.post_with_files,
**self.request_multiform,
)
self.fake_conductor_api.start_import_load.assert_called_once()
self.fake_conductor_api.import_load.assert_not_called()
def test_load_import_failed_to_import_load_conductor(self):
self.fake_conductor_api.import_load.side_effect = common.RemoteError()
self.assertRaises(
webtest.app.AppError,
self.post_with_files,
**self.request_multiform,
)
self.fake_conductor_api.start_import_load.assert_called_once()
self.fake_conductor_api.import_load.assert_called_once()