Merge "Add inactive param for import-load on sysinv api"
This commit is contained in:
commit
e9a8c70268
|
@ -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])
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
It'S Ok
|
|
@ -0,0 +1 @@
|
|||
Simple Is Good
|
|
@ -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()
|
Loading…
Reference in New Issue