Merge "Add deletion constraint"
This commit is contained in:
commit
b0afdd45f1
|
@ -4,7 +4,6 @@
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
#
|
#
|
||||||
|
|
||||||
from keystoneauth1 import loading
|
|
||||||
from oslo_utils import importutils
|
from oslo_utils import importutils
|
||||||
|
|
||||||
from software_client import exc
|
from software_client import exc
|
||||||
|
@ -18,6 +17,7 @@ API_ENDPOINT = "http://127.0.0.1:" + API_PORT
|
||||||
|
|
||||||
|
|
||||||
def _make_session(**kwargs):
|
def _make_session(**kwargs):
|
||||||
|
from keystoneauth1 import loading
|
||||||
"""Construct a session based on authentication information
|
"""Construct a session based on authentication information
|
||||||
|
|
||||||
:param kwargs: keyword args containing credentials, either:
|
:param kwargs: keyword args containing credentials, either:
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""
|
||||||
|
Copyright (c) 2024 Wind River Systems, Inc.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# HTTP ERRORS, display message when corresponding status code
|
||||||
|
# is received.
|
||||||
|
# status code 500, will be handled separatedly, as it returns
|
||||||
|
# API request related information.
|
||||||
|
HTTP_ERRORS = {
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Not Found",
|
||||||
|
405: "Method Not Allowed",
|
||||||
|
406: "Not Acceptable",
|
||||||
|
407: "Proxy Authentication Required",
|
||||||
|
408: "Request Timeout",
|
||||||
|
409: "Conflict",
|
||||||
|
410: "Gone",
|
||||||
|
411: "Length Required",
|
||||||
|
412: "Precondition Failed",
|
||||||
|
413: "Content Too Large",
|
||||||
|
414: "URI Too Long",
|
||||||
|
415: "Unsupported Media Type",
|
||||||
|
416: "Range Not Satisfiable",
|
||||||
|
417: "Expectation Failed",
|
||||||
|
418: "I'm a teapot",
|
||||||
|
421: "Misdirected Request",
|
||||||
|
422: "Unprocessable Content",
|
||||||
|
423: "Locked",
|
||||||
|
424: "Failed Dependency",
|
||||||
|
425: "Too Early",
|
||||||
|
426: "Upgrade Required",
|
||||||
|
428: "Precondition Required",
|
||||||
|
429: "Too Many Requests",
|
||||||
|
501: "Not Implemented",
|
||||||
|
502: "Bad Gateway",
|
||||||
|
503: "Service Unavailable",
|
||||||
|
504: "Gateway Timeout",
|
||||||
|
505: "HTTP Version Not Support",
|
||||||
|
506: "Variant Also Negotiates",
|
||||||
|
507: "Insufficient Storage",
|
||||||
|
508: "Loop Detected",
|
||||||
|
509: "Not Extended",
|
||||||
|
511: "Network Authentication Required"
|
||||||
|
}
|
|
@ -25,6 +25,8 @@ import textwrap
|
||||||
from oslo_utils import importutils
|
from oslo_utils import importutils
|
||||||
from six.moves import zip
|
from six.moves import zip
|
||||||
|
|
||||||
|
from software_client.common.http_errors import HTTP_ERRORS
|
||||||
|
|
||||||
|
|
||||||
TERM_WIDTH = 72
|
TERM_WIDTH = 72
|
||||||
|
|
||||||
|
@ -116,6 +118,46 @@ def check_rc(req, data):
|
||||||
return rc
|
return rc
|
||||||
|
|
||||||
|
|
||||||
|
def _display_info(text):
|
||||||
|
''' display the basic info json object '''
|
||||||
|
try:
|
||||||
|
data = json.loads(text)
|
||||||
|
except Exception:
|
||||||
|
print(f"Invalid response format: {text}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if "error" in data and data["error"] != "":
|
||||||
|
print("Error:\n%s" % data["error"])
|
||||||
|
elif "warning" in data and data["warning"] != "":
|
||||||
|
print("Warning:\n%s" % data["warning"])
|
||||||
|
elif "info" in data and data["info"] != "":
|
||||||
|
print(data["info"])
|
||||||
|
|
||||||
|
|
||||||
|
def display_info(resp):
|
||||||
|
'''
|
||||||
|
This function displays basic REST API return, w/ info json object:
|
||||||
|
{
|
||||||
|
"info":"",
|
||||||
|
"warning":"",
|
||||||
|
"error":"",
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
status_code = resp.status_code
|
||||||
|
text = resp.text
|
||||||
|
|
||||||
|
if resp.status_code == 500:
|
||||||
|
# all 500 error comes with basic info json object
|
||||||
|
_display_info(text)
|
||||||
|
elif resp.status_code in HTTP_ERRORS:
|
||||||
|
# any 4xx and 5xx errors does not contain API information.
|
||||||
|
print("Error:\n%s", HTTP_ERRORS[status_code])
|
||||||
|
else:
|
||||||
|
# print out the basic info json object
|
||||||
|
_display_info(text)
|
||||||
|
|
||||||
|
|
||||||
def print_result_list(header_data_list, data_list, has_error, sort_key=0):
|
def print_result_list(header_data_list, data_list, has_error, sort_key=0):
|
||||||
"""
|
"""
|
||||||
Print a list of data in a simple table format
|
Print a list of data in a simple table format
|
||||||
|
|
|
@ -392,12 +392,17 @@ class SoftwareClientShell(object):
|
||||||
args.os_endpoint_type = endpoint_type
|
args.os_endpoint_type = endpoint_type
|
||||||
client = sclient.get_client(api_version, auth_mode, **(args.__dict__))
|
client = sclient.get_client(api_version, auth_mode, **(args.__dict__))
|
||||||
|
|
||||||
|
return args.func(client, args)
|
||||||
|
# TODO(bqian) reenable below once Exception classes are defined
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
args.func(client, args)
|
args.func(client, args)
|
||||||
except exc.Unauthorized:
|
except exc.Unauthorized:
|
||||||
raise exc.CommandError("Invalid Identity credentials.")
|
raise exc.CommandError("Invalid Identity credentials.")
|
||||||
except exc.HTTPForbidden:
|
except exc.HTTPForbidden:
|
||||||
raise exc.CommandError("Error: Forbidden")
|
raise exc.CommandError("Error: Forbidden")
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def do_bash_completion(self, args):
|
def do_bash_completion(self, args):
|
||||||
"""Prints all of the commands and options to stdout.
|
"""Prints all of the commands and options to stdout.
|
||||||
|
@ -435,7 +440,7 @@ class HelpFormatter(argparse.HelpFormatter):
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
try:
|
||||||
SoftwareClientShell().main(sys.argv[1:])
|
return SoftwareClientShell().main(sys.argv[1:])
|
||||||
|
|
||||||
except KeyboardInterrupt as e:
|
except KeyboardInterrupt as e:
|
||||||
print(('caught: %r, aborting' % (e)), file=sys.stderr)
|
print(('caught: %r, aborting' % (e)), file=sys.stderr)
|
||||||
|
@ -451,4 +456,4 @@ def main():
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
sys.exit(main())
|
||||||
|
|
|
@ -83,7 +83,7 @@ class ReleaseManager(base.Manager):
|
||||||
|
|
||||||
path = '/v1/software/upload'
|
path = '/v1/software/upload'
|
||||||
if is_local:
|
if is_local:
|
||||||
to_upload_filenames = json.dumps(valid_files)
|
to_upload_filenames = valid_files
|
||||||
headers = {'Content-Type': 'text/plain'}
|
headers = {'Content-Type': 'text/plain'}
|
||||||
return self._create(path, body=to_upload_filenames, headers=headers)
|
return self._create(path, body=to_upload_filenames, headers=headers)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -192,9 +192,5 @@ def do_upload_dir(cc, args):
|
||||||
def do_delete(cc, args):
|
def do_delete(cc, args):
|
||||||
"""Delete the software release"""
|
"""Delete the software release"""
|
||||||
resp, body = cc.release.release_delete(args.release)
|
resp, body = cc.release.release_delete(args.release)
|
||||||
if args.debug:
|
utils.display_info(resp)
|
||||||
utils.print_result_debug(resp, body)
|
|
||||||
else:
|
|
||||||
utils.print_software_op_result(resp, body)
|
|
||||||
|
|
||||||
return utils.check_rc(resp, body)
|
return utils.check_rc(resp, body)
|
||||||
|
|
|
@ -14,6 +14,7 @@ from pecan import Response
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from software.exceptions import SoftwareError
|
from software.exceptions import SoftwareError
|
||||||
|
from software.exceptions import SoftwareServiceError
|
||||||
from software.software_controller import sc
|
from software.software_controller import sc
|
||||||
import software.utils as utils
|
import software.utils as utils
|
||||||
import software.constants as constants
|
import software.constants as constants
|
||||||
|
@ -26,32 +27,20 @@ class SoftwareAPIController(object):
|
||||||
|
|
||||||
@expose('json')
|
@expose('json')
|
||||||
def commit_patch(self, *args):
|
def commit_patch(self, *args):
|
||||||
try:
|
result = sc.patch_commit(list(args))
|
||||||
result = sc.patch_commit(list(args))
|
|
||||||
except SoftwareError as e:
|
|
||||||
return dict(error=str(e))
|
|
||||||
|
|
||||||
sc.software_sync()
|
sc.software_sync()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@expose('json')
|
@expose('json')
|
||||||
def commit_dry_run(self, *args):
|
def commit_dry_run(self, *args):
|
||||||
try:
|
result = sc.patch_commit(list(args), dry_run=True)
|
||||||
result = sc.patch_commit(list(args), dry_run=True)
|
|
||||||
except SoftwareError as e:
|
|
||||||
return dict(error=str(e))
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@expose('json')
|
@expose('json')
|
||||||
@expose('query.xml', content_type='application/xml')
|
@expose('query.xml', content_type='application/xml')
|
||||||
def delete(self, *args):
|
def delete(self, *args):
|
||||||
try:
|
result = sc.software_release_delete_api(list(args))
|
||||||
result = sc.software_release_delete_api(list(args))
|
|
||||||
except SoftwareError as e:
|
|
||||||
return dict(error="Error: %s" % str(e))
|
|
||||||
|
|
||||||
sc.software_sync()
|
sc.software_sync()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -60,13 +49,9 @@ class SoftwareAPIController(object):
|
||||||
@expose('query.xml', content_type='application/xml')
|
@expose('query.xml', content_type='application/xml')
|
||||||
def deploy_activate(self, *args):
|
def deploy_activate(self, *args):
|
||||||
if sc.any_patch_host_installing():
|
if sc.any_patch_host_installing():
|
||||||
return dict(error="Rejected: One or more nodes are installing a release.")
|
raise SoftwareServiceError(error="Rejected: One or more nodes are installing a release.")
|
||||||
|
|
||||||
try:
|
|
||||||
result = sc.software_deploy_activate_api(list(args)[0])
|
|
||||||
except SoftwareError as e:
|
|
||||||
return dict(error="Error: %s" % str(e))
|
|
||||||
|
|
||||||
|
result = sc.software_deploy_activate_api(list(args)[0])
|
||||||
sc.software_sync()
|
sc.software_sync()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -74,12 +59,9 @@ class SoftwareAPIController(object):
|
||||||
@expose('query.xml', content_type='application/xml')
|
@expose('query.xml', content_type='application/xml')
|
||||||
def deploy_complete(self, *args):
|
def deploy_complete(self, *args):
|
||||||
if sc.any_patch_host_installing():
|
if sc.any_patch_host_installing():
|
||||||
return dict(error="Rejected: One or more nodes are installing a release.")
|
raise SoftwareServiceError(error="Rejected: One or more nodes are installing a release.")
|
||||||
|
|
||||||
try:
|
result = sc.software_deploy_complete_api(list(args)[0])
|
||||||
result = sc.software_deploy_complete_api(list(args)[0])
|
|
||||||
except SoftwareError as e:
|
|
||||||
return dict(error="Error: %s" % str(e))
|
|
||||||
|
|
||||||
sc.software_sync()
|
sc.software_sync()
|
||||||
return result
|
return result
|
||||||
|
@ -93,10 +75,7 @@ class SoftwareAPIController(object):
|
||||||
if len(list(args)) > 1 and 'force' in list(args)[1:]:
|
if len(list(args)) > 1 and 'force' in list(args)[1:]:
|
||||||
force = True
|
force = True
|
||||||
|
|
||||||
try:
|
result = sc.software_deploy_host_api(list(args)[0], force, async_req=True)
|
||||||
result = sc.software_deploy_host_api(list(args)[0], force, async_req=True)
|
|
||||||
except SoftwareError as e:
|
|
||||||
return dict(error="Error: %s" % str(e))
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -107,10 +86,7 @@ class SoftwareAPIController(object):
|
||||||
if 'force' in list(args):
|
if 'force' in list(args):
|
||||||
force = True
|
force = True
|
||||||
|
|
||||||
try:
|
result = sc.software_deploy_precheck_api(list(args)[0], force, **kwargs)
|
||||||
result = sc.software_deploy_precheck_api(list(args)[0], force, **kwargs)
|
|
||||||
except SoftwareError as e:
|
|
||||||
return dict(error="Error: %s" % str(e))
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -121,12 +97,9 @@ class SoftwareAPIController(object):
|
||||||
force = 'force' in list(args)
|
force = 'force' in list(args)
|
||||||
|
|
||||||
if sc.any_patch_host_installing():
|
if sc.any_patch_host_installing():
|
||||||
return dict(error="Rejected: One or more nodes are installing releases.")
|
raise SoftwareServiceError(error="Rejected: One or more nodes are installing a release.")
|
||||||
|
|
||||||
try:
|
result = sc.software_deploy_start_api(list(args)[0], force, **kwargs)
|
||||||
result = sc.software_deploy_start_api(list(args)[0], force, **kwargs)
|
|
||||||
except SoftwareError as e:
|
|
||||||
return dict(error="Error: %s" % str(e))
|
|
||||||
|
|
||||||
sc.send_latest_feed_commit_to_agent()
|
sc.send_latest_feed_commit_to_agent()
|
||||||
sc.software_sync()
|
sc.software_sync()
|
||||||
|
@ -144,10 +117,7 @@ class SoftwareAPIController(object):
|
||||||
@expose('json')
|
@expose('json')
|
||||||
@expose('query.xml', content_type='application/xml')
|
@expose('query.xml', content_type='application/xml')
|
||||||
def install_local(self):
|
def install_local(self):
|
||||||
try:
|
result = sc.software_install_local_api()
|
||||||
result = sc.software_install_local_api()
|
|
||||||
except SoftwareError as e:
|
|
||||||
return dict(error="Error: %s" % str(e))
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -166,10 +136,7 @@ class SoftwareAPIController(object):
|
||||||
@expose('json')
|
@expose('json')
|
||||||
@expose('show.xml', content_type='application/xml')
|
@expose('show.xml', content_type='application/xml')
|
||||||
def show(self, *args):
|
def show(self, *args):
|
||||||
try:
|
result = sc.software_release_query_specific_cached(list(args))
|
||||||
result = sc.software_release_query_specific_cached(list(args))
|
|
||||||
except SoftwareError as e:
|
|
||||||
return dict(error="Error: %s" % str(e))
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -212,8 +179,6 @@ class SoftwareAPIController(object):
|
||||||
# Process uploaded files
|
# Process uploaded files
|
||||||
return sc.software_release_upload(uploaded_files)
|
return sc.software_release_upload(uploaded_files)
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return dict(error=str(e))
|
|
||||||
finally:
|
finally:
|
||||||
# Remove all uploaded files from /scratch dir
|
# Remove all uploaded files from /scratch dir
|
||||||
sc.software_sync()
|
sc.software_sync()
|
||||||
|
@ -223,10 +188,7 @@ class SoftwareAPIController(object):
|
||||||
@expose('json')
|
@expose('json')
|
||||||
@expose('query.xml', content_type='application/xml')
|
@expose('query.xml', content_type='application/xml')
|
||||||
def query(self, **kwargs):
|
def query(self, **kwargs):
|
||||||
try:
|
sd = sc.software_release_query_cached(**kwargs)
|
||||||
sd = sc.software_release_query_cached(**kwargs)
|
|
||||||
except SoftwareError as e:
|
|
||||||
return dict(error="Error: %s" % str(e))
|
|
||||||
|
|
||||||
return dict(sd=sd)
|
return dict(sd=sd)
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,9 @@ ADDRESS_VERSION_IPV4 = 4
|
||||||
ADDRESS_VERSION_IPV6 = 6
|
ADDRESS_VERSION_IPV6 = 6
|
||||||
CONTROLLER_FLOATING_HOSTNAME = "controller"
|
CONTROLLER_FLOATING_HOSTNAME = "controller"
|
||||||
|
|
||||||
|
DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER = 'systemcontroller'
|
||||||
|
SYSTEM_CONTROLLER_REGION = 'SystemController'
|
||||||
|
|
||||||
SOFTWARE_STORAGE_DIR = "/opt/software"
|
SOFTWARE_STORAGE_DIR = "/opt/software"
|
||||||
SOFTWARE_CONFIG_FILE_LOCAL = "/etc/software/software.conf"
|
SOFTWARE_CONFIG_FILE_LOCAL = "/etc/software/software.conf"
|
||||||
|
|
||||||
|
@ -64,7 +67,8 @@ UNAVAILABLE = 'unavailable'
|
||||||
DEPLOYING = 'deploying'
|
DEPLOYING = 'deploying'
|
||||||
DEPLOYED = 'deployed'
|
DEPLOYED = 'deployed'
|
||||||
REMOVING = 'removing'
|
REMOVING = 'removing'
|
||||||
UNKNOWN = 'n/a'
|
|
||||||
|
DELETABLE_STATE = [AVAILABLE, UNAVAILABLE]
|
||||||
|
|
||||||
# TODO(bqian) states to be removed once current references are removed
|
# TODO(bqian) states to be removed once current references are removed
|
||||||
ABORTING = 'aborting'
|
ABORTING = 'aborting'
|
||||||
|
@ -182,6 +186,7 @@ class DEPLOY_STATES(Enum):
|
||||||
HOST_DONE = 'host-done'
|
HOST_DONE = 'host-done'
|
||||||
HOST_FAILED = 'host-failed'
|
HOST_FAILED = 'host-failed'
|
||||||
|
|
||||||
|
|
||||||
class DEPLOY_HOST_STATES(Enum):
|
class DEPLOY_HOST_STATES(Enum):
|
||||||
DEPLOYED = 'deployed'
|
DEPLOYED = 'deployed'
|
||||||
DEPLOYING = 'deploying'
|
DEPLOYING = 'deploying'
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
"""
|
||||||
|
Copyright (c) 2024 Wind River Systems, Inc.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from keystoneauth1 import exceptions
|
||||||
|
from keystoneauth1 import identity
|
||||||
|
from keystoneauth1 import session
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_utils import encodeutils
|
||||||
|
from six.moves.urllib.request import Request
|
||||||
|
from six.moves.urllib.request import urlopen
|
||||||
|
|
||||||
|
from software import utils
|
||||||
|
from software.constants import SYSTEM_CONTROLLER_REGION
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger('main_logger')
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def get_token_endpoint(service_type, region_name=None, interface="internal"):
|
||||||
|
config = CONF.get('keystone_authtoken')
|
||||||
|
if region_name is None:
|
||||||
|
region_name = config.region_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth = identity.Password(
|
||||||
|
auth_url=config.auth_url,
|
||||||
|
username=config.username,
|
||||||
|
password=config.password,
|
||||||
|
project_name=config.project_name,
|
||||||
|
user_domain_name=config.user_domain_name,
|
||||||
|
project_domain_name=config.project_domain_name
|
||||||
|
)
|
||||||
|
sess = session.Session(auth=auth)
|
||||||
|
token = sess.get_token()
|
||||||
|
endpoint = sess.get_endpoint(service_type=service_type,
|
||||||
|
region_name=region_name,
|
||||||
|
interface=interface)
|
||||||
|
except exceptions.http.Unauthorized:
|
||||||
|
raise Exception("Failed to authenticate to Keystone. Request unauthorized")
|
||||||
|
except Exception as e:
|
||||||
|
msg = "Failed to get token and endpoint. Error: %s", str(e)
|
||||||
|
raise Exception(msg)
|
||||||
|
return token, endpoint
|
||||||
|
|
||||||
|
|
||||||
|
def rest_api_request(token, method, api_cmd,
|
||||||
|
api_cmd_payload=None, timeout=45):
|
||||||
|
"""
|
||||||
|
Make a rest-api request
|
||||||
|
Returns: response as a dictionary
|
||||||
|
"""
|
||||||
|
api_cmd_headers = dict()
|
||||||
|
api_cmd_headers['Content-type'] = "application/json"
|
||||||
|
api_cmd_headers['User-Agent'] = "usm/1.0"
|
||||||
|
|
||||||
|
request_info = Request(api_cmd)
|
||||||
|
request_info.get_method = lambda: method
|
||||||
|
if token:
|
||||||
|
request_info.add_header("X-Auth-Token", token)
|
||||||
|
request_info.add_header("Accept", "application/json")
|
||||||
|
|
||||||
|
if api_cmd_headers is not None:
|
||||||
|
for header_type, header_value in api_cmd_headers.items():
|
||||||
|
request_info.add_header(header_type, header_value)
|
||||||
|
|
||||||
|
if api_cmd_payload is not None:
|
||||||
|
request_info.data = encodeutils.safe_encode(api_cmd_payload)
|
||||||
|
|
||||||
|
request = None
|
||||||
|
try:
|
||||||
|
request = urlopen(request_info, timeout=timeout)
|
||||||
|
response = request.read()
|
||||||
|
finally:
|
||||||
|
if request:
|
||||||
|
request.close()
|
||||||
|
|
||||||
|
if response == "":
|
||||||
|
response = json.loads("{}")
|
||||||
|
else:
|
||||||
|
response = json.loads(response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def get_subclouds_from_dcmanager():
|
||||||
|
token, api_url = get_token_endpoint("dcmanager", region_name=SYSTEM_CONTROLLER_REGION)
|
||||||
|
|
||||||
|
api_cmd = api_url + '/subclouds'
|
||||||
|
LOG.debug('api_cmd %s' % api_cmd)
|
||||||
|
data = rest_api_request(token, "GET", api_cmd)
|
||||||
|
if 'subclouds' in data:
|
||||||
|
return data['subclouds']
|
||||||
|
raise Exception(f"Incorrect response from dcmanager for querying subclouds {data}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_subcloud_groupby_version():
|
||||||
|
subclouds = get_subclouds_from_dcmanager()
|
||||||
|
grouped_subclouds = {}
|
||||||
|
for subcloud in subclouds:
|
||||||
|
major_ver = utils.get_major_release_version(subcloud['software_version'])
|
||||||
|
if major_ver not in grouped_subclouds:
|
||||||
|
grouped_subclouds[major_ver] = [subcloud]
|
||||||
|
else:
|
||||||
|
grouped_subclouds[major_ver].append(subcloud)
|
||||||
|
|
||||||
|
msg = "total %s subclouds." % len(subclouds)
|
||||||
|
for ver in grouped_subclouds:
|
||||||
|
msg = msg + " %s: %s subclouds." % (ver, len(grouped_subclouds[ver]))
|
||||||
|
|
||||||
|
LOG.info(msg)
|
||||||
|
return grouped_subclouds
|
|
@ -5,11 +5,13 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from packaging import version
|
||||||
import shutil
|
import shutil
|
||||||
from software import constants
|
from software import constants
|
||||||
from software.exceptions import FileSystemError
|
from software.exceptions import FileSystemError
|
||||||
from software.exceptions import InternalError
|
from software.exceptions import InternalError
|
||||||
from software.software_functions import LOG
|
from software.software_functions import LOG
|
||||||
|
from software import utils
|
||||||
|
|
||||||
|
|
||||||
class SWRelease(object):
|
class SWRelease(object):
|
||||||
|
@ -19,6 +21,7 @@ class SWRelease(object):
|
||||||
self._id = rel_id
|
self._id = rel_id
|
||||||
self._metadata = metadata
|
self._metadata = metadata
|
||||||
self._contents = contents
|
self._contents = contents
|
||||||
|
self._sw_version = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self):
|
def metadata(self):
|
||||||
|
@ -83,9 +86,17 @@ class SWRelease(object):
|
||||||
raise InternalError(error)
|
raise InternalError(error)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sw_version(self):
|
def sw_release(self):
|
||||||
|
'''3 sections MM.mm.pp release version'''
|
||||||
return self.metadata['sw_version']
|
return self.metadata['sw_version']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sw_version(self):
|
||||||
|
'''2 sections MM.mm software version'''
|
||||||
|
if self._sw_version is None:
|
||||||
|
self._sw_version = utils.get_major_release_version(self.sw_release)
|
||||||
|
return self._sw_version
|
||||||
|
|
||||||
def _get_latest_commit(self):
|
def _get_latest_commit(self):
|
||||||
num_commits = self.contents['number_of_commits']
|
num_commits = self.contents['number_of_commits']
|
||||||
if int(num_commits) > 0:
|
if int(num_commits) > 0:
|
||||||
|
@ -156,6 +167,16 @@ class SWRelease(object):
|
||||||
# latest commit
|
# latest commit
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_ga_release(self):
|
||||||
|
ver = version.parse(self.sw_release)
|
||||||
|
_, _, pp = ver.release
|
||||||
|
return pp == 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_deletable(self):
|
||||||
|
return self.state in constants.DELETABLE_STATE
|
||||||
|
|
||||||
|
|
||||||
class SWReleaseCollection(object):
|
class SWReleaseCollection(object):
|
||||||
'''SWReleaseCollection encapsulates aggregated software release collection
|
'''SWReleaseCollection encapsulates aggregated software release collection
|
||||||
|
|
|
@ -31,6 +31,7 @@ from software.api import app
|
||||||
from software.authapi import app as auth_app
|
from software.authapi import app as auth_app
|
||||||
from software.constants import DEPLOY_STATES
|
from software.constants import DEPLOY_STATES
|
||||||
from software.base import PatchService
|
from software.base import PatchService
|
||||||
|
from software.dc_utils import get_subcloud_groupby_version
|
||||||
from software.exceptions import APTOSTreeCommandFail
|
from software.exceptions import APTOSTreeCommandFail
|
||||||
from software.exceptions import InternalError
|
from software.exceptions import InternalError
|
||||||
from software.exceptions import MetadataFail
|
from software.exceptions import MetadataFail
|
||||||
|
@ -66,6 +67,7 @@ from software.release_verify import verify_files
|
||||||
import software.config as cfg
|
import software.config as cfg
|
||||||
import software.utils as utils
|
import software.utils as utils
|
||||||
from software.sysinv_utils import get_k8s_ver
|
from software.sysinv_utils import get_k8s_ver
|
||||||
|
from software.sysinv_utils import is_system_controller
|
||||||
|
|
||||||
from software.db.api import get_instance
|
from software.db.api import get_instance
|
||||||
|
|
||||||
|
@ -1148,7 +1150,7 @@ class PatchController(PatchService):
|
||||||
max_major_releases = 2
|
max_major_releases = 2
|
||||||
major_releases = []
|
major_releases = []
|
||||||
for rel in self.release_collection.iterate_releases():
|
for rel in self.release_collection.iterate_releases():
|
||||||
major_rel = utils.get_major_release_version(rel.sw_version)
|
major_rel = rel.sw_version
|
||||||
if major_rel not in major_releases:
|
if major_rel not in major_releases:
|
||||||
major_releases.append(major_rel)
|
major_releases.append(major_rel)
|
||||||
|
|
||||||
|
@ -1470,7 +1472,43 @@ class PatchController(PatchService):
|
||||||
msg_error = ""
|
msg_error = ""
|
||||||
|
|
||||||
# Protect against duplications
|
# Protect against duplications
|
||||||
release_list = sorted(list(set(release_ids)))
|
full_list = sorted(list(set(release_ids)))
|
||||||
|
|
||||||
|
not_founds = []
|
||||||
|
cannot_del = []
|
||||||
|
used_by_subcloud = []
|
||||||
|
release_list = []
|
||||||
|
for rel_id in full_list:
|
||||||
|
rel = self.release_collection.get_release_by_id(rel_id)
|
||||||
|
if rel is None:
|
||||||
|
not_founds.append(rel_id)
|
||||||
|
else:
|
||||||
|
if not rel.is_deletable:
|
||||||
|
cannot_del.append(rel_id)
|
||||||
|
elif rel.is_ga_release and is_system_controller():
|
||||||
|
subcloud_by_sw_version = get_subcloud_groupby_version()
|
||||||
|
if rel.sw_version in subcloud_by_sw_version:
|
||||||
|
used_by_subcloud.append(rel_id)
|
||||||
|
else:
|
||||||
|
release_list.append(rel_id)
|
||||||
|
else:
|
||||||
|
release_list.append(rel_id)
|
||||||
|
|
||||||
|
err_msg = ""
|
||||||
|
if len(not_founds) > 0:
|
||||||
|
list_str = ','.join(not_founds)
|
||||||
|
err_msg = f"Releases {list_str} can not be found\n"
|
||||||
|
|
||||||
|
if len(cannot_del) > 0:
|
||||||
|
list_str = ','.join(cannot_del)
|
||||||
|
err_msg = err_msg + f"Releases {list_str} are not ready to delete\n"
|
||||||
|
|
||||||
|
if len(used_by_subcloud) > 0:
|
||||||
|
list_str = ','.join(used_by_subcloud)
|
||||||
|
err_msg = err_msg + f"Releases {list_str} are still used by subcloud(s)"
|
||||||
|
|
||||||
|
if len(err_msg) > 0:
|
||||||
|
raise SoftwareServiceError(error=err_msg)
|
||||||
|
|
||||||
msg = "Deleting releases: %s" % ",".join(release_list)
|
msg = "Deleting releases: %s" % ",".join(release_list)
|
||||||
LOG.info(msg)
|
LOG.info(msg)
|
||||||
|
@ -2915,7 +2953,6 @@ class PatchController(PatchService):
|
||||||
return None
|
return None
|
||||||
deploy = deploy[0]
|
deploy = deploy[0]
|
||||||
|
|
||||||
|
|
||||||
deploy_host_list = []
|
deploy_host_list = []
|
||||||
for host in deploy_hosts:
|
for host in deploy_hosts:
|
||||||
state = host.get("state")
|
state = host.get("state")
|
||||||
|
|
|
@ -276,8 +276,8 @@ class DeployHandler(Deploy):
|
||||||
"""
|
"""
|
||||||
super().query(from_release, to_release)
|
super().query(from_release, to_release)
|
||||||
for deploy in self.data.get("deploy", []):
|
for deploy in self.data.get("deploy", []):
|
||||||
if (deploy.get("from_release") == from_release
|
if (deploy.get("from_release") == from_release and
|
||||||
and deploy.get("to_release") == to_release):
|
deploy.get("to_release") == to_release):
|
||||||
return deploy
|
return deploy
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -325,7 +325,7 @@ class DeployHostHandler(DeployHosts):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.data = get_software_filesystem_data()
|
self.data = get_software_filesystem_data()
|
||||||
|
|
||||||
def create(self, hostname, state:DEPLOY_HOST_STATES=None):
|
def create(self, hostname, state: DEPLOY_HOST_STATES = None):
|
||||||
super().create(hostname, state)
|
super().create(hostname, state)
|
||||||
deploy = self.query(hostname)
|
deploy = self.query(hostname)
|
||||||
if deploy:
|
if deploy:
|
||||||
|
|
|
@ -1178,7 +1178,6 @@ def create_deploy_hosts():
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def collect_current_load_for_hosts():
|
def collect_current_load_for_hosts():
|
||||||
load_data = {
|
load_data = {
|
||||||
"current_loads": []
|
"current_loads": []
|
||||||
|
|
|
@ -5,8 +5,10 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import software.utils as utils
|
|
||||||
from software.exceptions import SysinvClientNotInitialized
|
from software.exceptions import SysinvClientNotInitialized
|
||||||
|
from software import constants
|
||||||
|
from software import utils
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger('main_logger')
|
LOG = logging.getLogger('main_logger')
|
||||||
|
@ -41,6 +43,7 @@ def get_k8s_ver():
|
||||||
return k8s_ver.version
|
return k8s_ver.version
|
||||||
raise Exception("Failed to get current k8s version")
|
raise Exception("Failed to get current k8s version")
|
||||||
|
|
||||||
|
|
||||||
def get_ihost_list():
|
def get_ihost_list():
|
||||||
try:
|
try:
|
||||||
token, endpoint = utils.get_endpoints_token()
|
token, endpoint = utils.get_endpoints_token()
|
||||||
|
@ -49,3 +52,18 @@ def get_ihost_list():
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
LOG.error("Error getting ihost list: %s", err)
|
LOG.error("Error getting ihost list: %s", err)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_dc_role():
|
||||||
|
try:
|
||||||
|
token, endpoint = utils.get_endpoints_token()
|
||||||
|
sysinv_client = get_sysinv_client(token=token, endpoint=endpoint)
|
||||||
|
system = sysinv_client.isystem.list()[0]
|
||||||
|
return system.distributed_cloud_role
|
||||||
|
except Exception as err:
|
||||||
|
LOG.error("Error getting DC role: %s", err)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def is_system_controller():
|
||||||
|
return get_dc_role() == constants.DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER
|
||||||
|
|
|
@ -42,10 +42,10 @@ class ExceptionHook(hooks.PecanHook):
|
||||||
|
|
||||||
if isinstance(e, SoftwareServiceError):
|
if isinstance(e, SoftwareServiceError):
|
||||||
LOG.warning("An issue is detected. Signature [%s]" % signature)
|
LOG.warning("An issue is detected. Signature [%s]" % signature)
|
||||||
|
# TODO(bqian) remove the logging after it is stable
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
|
|
||||||
data = dict(info=e.info, warning=e.warning, error=e.error)
|
data = dict(info=e.info, warning=e.warning, error=e.error)
|
||||||
data['error'] = data['error'] + " Error signature [%s]" % signature
|
|
||||||
else:
|
else:
|
||||||
err_msg = "Internal error occurred. Error signature [%s]" % signature
|
err_msg = "Internal error occurred. Error signature [%s]" % signature
|
||||||
LOG.error(err_msg)
|
LOG.error(err_msg)
|
||||||
|
|
Loading…
Reference in New Issue