diff --git a/software-client/software_client/client.py b/software-client/software_client/client.py index d9b90fa7..eae18191 100644 --- a/software-client/software_client/client.py +++ b/software-client/software_client/client.py @@ -7,7 +7,7 @@ from oslo_utils import importutils from software_client import exc -from software_client.constants import TOKEN, KEYSTONE, LOCAL_ROOT +from software_client.constants import LOCAL_ROOT SERVICE_NAME = 'usm' @@ -121,8 +121,7 @@ def get_client(api_version, auth_mode, session=None, service_type=SERVICE_TYPE, except Exception as e: msg = ('Failed to get openstack endpoint') raise exc.EndpointException( - ('%(message)s, error was: %(error)s') % - {'message': msg, 'error': e}) + ('%(message)s, error was: %(error)s') % {'message': msg, 'error': e}) elif local_root: endpoint = API_ENDPOINT else: diff --git a/software-client/software_client/common/base.py b/software-client/software_client/common/base.py index f144550b..600d1594 100644 --- a/software-client/software_client/common/base.py +++ b/software-client/software_client/common/base.py @@ -36,6 +36,9 @@ class Manager(object): def _create_multipart(self, url, **kwargs): return self.api.multipart_request('POST', url, **kwargs) + def _post(self, url, **kwargs): + return self.api.json_request('POST', url, **kwargs) + def _list(self, url, response_key=None, obj_class=None, body=None): resp, body = self.api.json_request('GET', url) if response_key: @@ -48,6 +51,14 @@ class Manager(object): return resp, data + def _fetch(self, url): + resp, body = self.api.json_request('GET', url) + data = body + return resp, data + + def _delete(self, url): + return self.api.json_request('DELETE', url) + class Resource(object): """A resource represents a particular instance of an object (tenant, user, diff --git a/software-client/software_client/common/http.py b/software-client/software_client/common/http.py index f2de3f79..678ce492 100644 --- a/software-client/software_client/common/http.py +++ b/software-client/software_client/common/http.py @@ -176,13 +176,9 @@ class SessionClient(adapter.LegacyJsonAdapter): resp = self.session.request(url, method, raise_exc=False, **kwargs) - if 400 <= resp.status_code < 600: - error_json = _extract_error_json(resp.content, resp) - raise exceptions.from_response( - resp, error_json.get('faultstring'), - error_json.get('debuginfo'), method, url) - elif resp.status_code in (300, 301, 302, 305): - raise exceptions.from_response(resp, method=method, url=url) + # NOTE (bqian) Do not recreate and raise exceptions. Let the + # display_error utility function to handle the well formatted + # response for webob.exc.HTTPClientError return resp def json_request(self, method, url, **kwargs): @@ -270,8 +266,8 @@ class SessionClient(adapter.LegacyJsonAdapter): if response.status_code != 200: err_message = _extract_error_json(response.text, response) fault_text = ( - err_message.get("faultstring") - or "Unknown error in SessionClient while uploading request with multipart" + err_message.get("faultstring") or + "Unknown error in SessionClient while uploading request with multipart" ) raise exceptions.HTTPBadRequest(fault_text) @@ -475,7 +471,6 @@ class HTTPClient(httplib2.Http): def multipart_request(self, method, url, **kwargs): return self.upload_request_with_multipart(method, url, **kwargs) - def raw_request(self, method, url, **kwargs): if not self.local_root: self.authenticate_and_fetch_endpoint_url() @@ -539,8 +534,8 @@ class HTTPClient(httplib2.Http): 'tenantName': self.tenant_name, }, } resp, resp_body = self._cs_request(token_url, "POST", - body=json.dumps(body), - content_type="application/json") + body=json.dumps(body), + content_type="application/json") status_code = self.get_status_code(resp) if status_code != 200: raise exceptions.HTTPUnauthorized(resp_body) @@ -570,7 +565,7 @@ class HTTPClient(httplib2.Http): body_json = json.loads(body) if 'error' in body_json: error_json = {'faultstring': body_json.get('error'), - 'debuginfo': body_json.get('info')} + 'debuginfo': body_json.get('info')} elif 'error_message' in body_json: raw_msg = body_json['error_message'] error_json = json.loads(raw_msg) diff --git a/software-client/software_client/common/utils.py b/software-client/software_client/common/utils.py index 441115c5..7d85151d 100644 --- a/software-client/software_client/common/utils.py +++ b/software-client/software_client/common/utils.py @@ -20,32 +20,11 @@ import argparse import json import os import re -import textwrap from tabulate import tabulate from oslo_utils import importutils -from six.moves import zip from software_client.common.http_errors import HTTP_ERRORS -# TODO(bqian) remove below overrides when switching to -# system command style CLI display for USM CLI is ready -from tabulate import _table_formats -from tabulate import TableFormat -from tabulate import Line -from tabulate import DataRow - -simple = TableFormat( - lineabove=Line("", "-", " ", ""), - linebelowheader=Line("", "=", " ", ""), - linebetweenrows=None, - linebelow=Line("", "-", " ", ""), - headerrow=DataRow("", " ", ""), - datarow=DataRow("", " ", ""), - padding=0, - with_header_hide=["lineabove", "linebelow"], -) - -# _table_formats['pretty'] = simple ##################################################### TERM_WIDTH = 72 @@ -140,7 +119,7 @@ def check_rc(req, data): def _display_info(text): - ''' display the basic info json object ''' + '''display the basic info json object ''' try: data = json.loads(text) except Exception: @@ -155,6 +134,27 @@ def _display_info(text): print(data["info"]) +def _display_error(status_code, text): + try: + data = json.loads(text) + except Exception: + print("Error:\n%s", HTTP_ERRORS[status_code]) + return + + if "code" in data: + print("Status: %s." % data["code"], end="") + else: + print("Status: %s." % status_code, end="") + + if "description" in data: + print(" " + data["description"]) + elif "title" in data: + print(" " + data["title"]) + else: + # any 4xx and 5xx errors does not contain API information. + print(HTTP_ERRORS[status_code]) + + def display_info(resp): ''' This function displays basic REST API return, w/ info json object: @@ -163,17 +163,21 @@ def display_info(resp): "warning":"", "error":"", } + + or an webob exception: + {"code": 404, "title": "", "description": ""} + + or default message based on status code ''' status_code = resp.status_code text = resp.text - if resp.status_code == 500: + if 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]) + elif status_code in HTTP_ERRORS: + _display_error(status_code, text) else: # print out the basic info json object _display_info(text) @@ -214,205 +218,6 @@ def display_detail_result(data): print(tabulate(table, header, tablefmt='pretty', colalign=("left", "left"))) -def print_result_list(header_data_list, data_list, has_error, sort_key=0): - """ - Print a list of data in a simple table format - :param header_data_list: Array of header data - :param data_list: Array of data - :param has_error: Boolean indicating if the request has error message - :param sort_key: Sorting key for the list - """ - - if has_error: - return - - if data_list is None or len(data_list) == 0: - return - - # Find the longest header string in each column - header_lengths = [len(str(x)) for x in header_data_list] - # Find the longest content string in each column - content_lengths = [max(len(str(x[i])) for x in data_list) - for i in range(len(header_data_list))] - # Find the max of the two for each column - col_lengths = [(x if x > y else y) for x, y in zip(header_lengths, content_lengths)] - - print(' '.join(f"{x.center(col_lengths[i])}" for i, x in enumerate(header_data_list))) - print(' '.join('=' * length for length in col_lengths)) - for item in sorted(data_list, key=lambda d: d[sort_key]): - print(' '.join(f"{str(x).center(col_lengths[i])}" for i, x in enumerate(item))) - print("\n") - - -def print_software_deploy_host_list_result(req, data): - if req.status_code == 200: - if not data: - print("No deploy in progress.\n") - return - - # Calculate column widths - hdr_hn = "Hostname" - hdr_rel = "Software Release" - hdr_tg_rel = "Target Release" - hdr_rr = "Reboot Required" - hdr_state = "Host State" - - width_hn = len(hdr_hn) - width_rel = len(hdr_rel) - width_tg_rel = len(hdr_tg_rel) - width_rr = len(hdr_rr) - width_state = len(hdr_state) - - for agent in sorted(data, key=lambda a: a["hostname"]): - if agent.get("host_state") is None: - agent["host_state"] = "No active deployment" - if agent.get("target_release") is None: - agent["target_release"] = "N/A" - if len(agent["hostname"]) > width_hn: - width_hn = len(agent["hostname"]) - if len(agent["software_release"]) > width_rel: - width_rel = len(agent["software_release"]) - if len(agent["target_release"]) > width_tg_rel: - width_tg_rel = len(agent["target_release"]) - if len(agent["host_state"]) > width_state: - width_state = len(agent["host_state"]) - - print("{0:^{width_hn}} {1:^{width_rel}} {2:^{width_tg_rel}} {3:^{width_rr}} {4:^{width_state}}".format( - hdr_hn, hdr_rel, hdr_tg_rel, hdr_rr, hdr_state, - width_hn=width_hn, width_rel=width_rel, width_tg_rel=width_tg_rel, width_rr=width_rr, width_state=width_state)) - - print("{0} {1} {2} {3} {4}".format( - '=' * width_hn, '=' * width_rel, '=' * width_tg_rel, '=' * width_rr, '=' * width_state)) - - for agent in sorted(data, key=lambda a: a["hostname"]): - print("{0:<{width_hn}} {1:^{width_rel}} {2:^{width_tg_rel}} {3:^{width_rr}} {4:^{width_state}}".format( - agent["hostname"], - agent["software_release"], - agent["target_release"], - "Yes" if agent.get("reboot_required", None) else "No", - agent["host_state"], - width_hn=width_hn, width_rel=width_rel, width_tg_rel=width_tg_rel, width_rr=width_rr, width_state=width_state)) - - elif req.status_code == 500: - print("An internal error has occurred. Please check /var/log/software.log for details") - - - -def print_release_show_result(req, data, list_packages=False): - if req.status_code == 200: - - if 'metadata' in data: - sd = data['metadata'] - contents = data['contents'] - for release_id in sorted(list(sd)): - print("%s:" % release_id) - - if "sw_version" in sd[release_id] and sd[release_id]["sw_version"] != "": - print(textwrap.fill(" {0:<15} ".format("Version:") + sd[release_id]["sw_version"], - width=TERM_WIDTH, subsequent_indent=' ' * 20)) - - if "state" in sd[release_id] and sd[release_id]["state"] != "": - print(textwrap.fill(" {0:<15} ".format("State:") + sd[release_id]["state"], - width=TERM_WIDTH, subsequent_indent=' ' * 20)) - - if "status" in sd[release_id] and sd[release_id]["status"] != "": - print(textwrap.fill(" {0:<15} ".format("Status:") + sd[release_id]["status"], - width=TERM_WIDTH, subsequent_indent=' ' * 20)) - - if "unremovable" in sd[release_id] and sd[release_id]["unremovable"] != "": - print(textwrap.fill(" {0:<15} ".format("Unremovable:") + sd[release_id]["unremovable"], - width=TERM_WIDTH, subsequent_indent=' ' * 20)) - - if "reboot_required" in sd[release_id] and sd[release_id]["reboot_required"] != "": - print(textwrap.fill(" {0:<15} ".format("RR:") + sd[release_id]["reboot_required"], - width=TERM_WIDTH, subsequent_indent=' ' * 20)) - - if "apply_active_release_only" in sd[release_id] and sd[release_id]["apply_active_release_only"] != "": - print(textwrap.fill(" {0:<15} ".format("Apply Active Release Only:") + sd[release_id]["apply_active_release_only"], - width=TERM_WIDTH, subsequent_indent=' ' * 20)) - - if "summary" in sd[release_id] and sd[release_id]["summary"] != "": - print(textwrap.fill(" {0:<15} ".format("Summary:") + sd[release_id]["summary"], - width=TERM_WIDTH, subsequent_indent=' ' * 20)) - - if "description" in sd[release_id] and sd[release_id]["description"] != "": - first_line = True - for line in sd[release_id]["description"].split('\n'): - if first_line: - print(textwrap.fill(" {0:<15} ".format("Description:") + line, - width=TERM_WIDTH, subsequent_indent=' ' * 20)) - first_line = False - else: - print(textwrap.fill(line, - width=TERM_WIDTH, subsequent_indent=' ' * 20, - initial_indent=' ' * 20)) - - if "install_instructions" in sd[release_id] and sd[release_id]["install_instructions"] != "": - print(" Install Instructions:") - for line in sd[release_id]["install_instructions"].split('\n'): - print(textwrap.fill(line, - width=TERM_WIDTH, subsequent_indent=' ' * 20, - initial_indent=' ' * 20)) - - if "warnings" in sd[release_id] and sd[release_id]["warnings"] != "": - first_line = True - for line in sd[release_id]["warnings"].split('\n'): - if first_line: - print(textwrap.fill(" {0:<15} ".format("Warnings:") + line, - width=TERM_WIDTH, subsequent_indent=' ' * 20)) - first_line = False - else: - print(textwrap.fill(line, - width=TERM_WIDTH, subsequent_indent=' ' * 20, - initial_indent=' ' * 20)) - - if "requires" in sd[release_id] and len(sd[release_id]["requires"]) > 0: - print(" Requires:") - for req_patch in sorted(sd[release_id]["requires"]): - print(' ' * 20 + req_patch) - - if "contents" in data and release_id in data["contents"]: - print(" Contents:\n") - if "number_of_commits" in contents[release_id] and \ - contents[release_id]["number_of_commits"] != "": - print(textwrap.fill(" {0:<15} ".format("No. of commits:") + - contents[release_id]["number_of_commits"], - width=TERM_WIDTH, subsequent_indent=' ' * 20)) - if "base" in contents[release_id] and \ - contents[release_id]["base"]["commit"] != "": - print(textwrap.fill(" {0:<15} ".format("Base commit:") + - contents[release_id]["base"]["commit"], - width=TERM_WIDTH, subsequent_indent=' ' * 20)) - if "number_of_commits" in contents[release_id] and \ - contents[release_id]["number_of_commits"] != "": - for i in range(int(contents[release_id]["number_of_commits"])): - print(textwrap.fill(" {0:<15} ".format("Commit%s:" % (i + 1)) + - contents[release_id]["commit%s" % (i + 1)]["commit"], - width=TERM_WIDTH, subsequent_indent=' ' * 20)) - - if list_packages: - if "packages" in sd[release_id] and len(sd[release_id]["packages"]): - print(" Packages:") - for package in sorted(sd[release_id]["packages"]): - print(" " * 20 + package) - - print("\n") - - if 'info' in data and data["info"] != "": - print(data["info"]) - - if 'warning' in data and data["warning"] != "": - print("Warning:") - print(data["warning"]) - - if 'error' in data and data["error"] != "": - print("Error:") - print(data["error"]) - - elif req.status_code == 500: - print("An internal error has occurred. Please check /var/log/software.log for details") - - def print_software_op_result(resp, data): if resp.status_code == 200: if 'sd' in data: diff --git a/software-client/software_client/software_client.py b/software-client/software_client/software_client.py index 0df7ef29..7ecef158 100644 --- a/software-client/software_client/software_client.py +++ b/software-client/software_client/software_client.py @@ -22,7 +22,9 @@ import software_client from software_client import client as sclient from software_client import exc from software_client.common import utils -from software_client.constants import TOKEN, KEYSTONE, LOCAL_ROOT +from software_client.constants import LOCAL_ROOT +from software_client.constants import KEYSTONE +from software_client.constants import TOKEN VIRTUAL_REGION = 'SystemController' @@ -251,7 +253,6 @@ class SoftwareClientShell(object): default=utils.env('OS_PROJECT_DOMAIN_NAME'), help='Defaults to env[OS_PROJECT_DOMAIN_NAME].') - # All commands are considered restricted, unless explicitly set to False parser.set_defaults(restricted=True) # All functions are initially defined as 'not implemented yet' diff --git a/software-client/software_client/tests/test_shell.py b/software-client/software_client/tests/test_shell.py index 56921df0..9419687a 100644 --- a/software-client/software_client/tests/test_shell.py +++ b/software-client/software_client/tests/test_shell.py @@ -15,7 +15,7 @@ from testtools import matchers import keystoneauth1 from software_client import exc -from software_client import software_client +from software_client.software_client import SoftwareClientShell from software_client.tests import utils FAKE_ENV = {'OS_USERNAME': 'username', @@ -49,7 +49,7 @@ class ShellTest(utils.BaseTestCase): orig = sys.stdout try: sys.stdout = StringIO() - _shell = software_client.SoftwareClientShell() + _shell = SoftwareClientShell() _shell.main(argstr.split()) except SystemExit: exc_type, exc_value, exc_traceback = sys.exc_info() diff --git a/software-client/software_client/tests/test_software_client.py b/software-client/software_client/tests/test_software_client.py index 1c773783..371f0035 100644 --- a/software-client/software_client/tests/test_software_client.py +++ b/software-client/software_client/tests/test_software_client.py @@ -131,26 +131,6 @@ class SoftwareClientHelpTestCase(SoftwareClientTestCase, SoftwareClientNonRootMi print_help is invoked when there is a failure. """ - @mock.patch('software_client.software_client.check_for_os_region_name') - @mock.patch('argparse.ArgumentParser.print_help') - @mock.patch('argparse.ArgumentParser.print_usage') - def test_main_no_args(self, mock_usage, mock_help, mock_check): - """When no arguments are called, it should call print_usage""" - shell_args = [self.PROG, ] - self._test_method(shell_args=shell_args) - mock_help.assert_called() - mock_check.assert_not_called() - - @mock.patch('software_client.software_client.check_for_os_region_name') - @mock.patch('argparse.ArgumentParser.print_help') - @mock.patch('argparse.ArgumentParser.print_usage') - def test_main_help(self, mock_usage, mock_help, mock_check): - """When -h is passed in, this should invoke print_help""" - shell_args = [self.PROG, "-h"] - self._test_method(shell_args=shell_args) - mock_help.assert_called() - mock_check.assert_not_called() - @mock.patch('software_client.software_client.check_for_os_region_name') @mock.patch('argparse.ArgumentParser.print_help') @mock.patch('argparse.ArgumentParser.print_usage') diff --git a/software-client/software_client/tests/v1/test_deploy.py b/software-client/software_client/tests/v1/test_deploy.py index 0d1f0372..cb88def7 100644 --- a/software-client/software_client/tests/v1/test_deploy.py +++ b/software-client/software_client/tests/v1/test_deploy.py @@ -20,75 +20,86 @@ import software_client.v1.deploy import software_client.v1.deploy_shell -HOST_LIST = {'data': [{ - 'ip': '192.168.204.2', - 'hostname': 'controller-0', - 'deployed': True, - 'secs_since_ack': 20, - 'patch_failed': True, - 'stale_details': False, - 'latest_sysroot_commit': '95139a5067', - 'nodetype': 'controller', - 'subfunctions': ['controller', 'worker'], - 'sw_version': '24.03', - 'state': 'install-failed', - 'allow_insvc_patching': True, - 'interim_state': False, - 'reboot_required': False}] +HOST_LIST = { + 'data': [ + { + 'ip': '192.168.204.2', + 'hostname': 'controller-0', + 'deployed': True, + 'secs_since_ack': 20, + 'patch_failed': True, + 'stale_details': False, + 'latest_sysroot_commit': '95139a5067', + 'nodetype': 'controller', + 'subfunctions': ['controller', 'worker'], + 'sw_version': '24.03', + 'state': 'install-failed', + 'allow_insvc_patching': True, + 'interim_state': False, + 'reboot_required': False + } + ] } fixtures = { - '/v1/software/host_list': + '/v1/deploy_host': { 'GET': ( {}, HOST_LIST, ), }, - '/v1/software/deploy_show': + '/v1/deploy': { 'GET': ( {}, {}, ), }, - '/v1/software/deploy_precheck/1': + '/v1/deploy/precheck/1': { 'GET': ( {}, {}, ), }, - '/v1/software/deploy_precheck/1/force?region_name=RegionOne': + '/v1/deploy/1/precheck': { 'POST': ( {}, {}, ), }, - '/v1/software/deploy_start/1/force': + '/v1/deploy/1/start': { 'POST': ( {}, - {}, + {'force': 'true'}, ), }, - '/v1/software/deploy_host/1/force': + '/v1/deploy_host/controller-1': { 'POST': ( {}, {"error": True}, ), }, - '/v1/software/deploy_activate/1': + '/v1/deploy_host/controller-1/force': + { + 'POST': ( + {}, + None, + ), + }, + '/v1/deploy/activate': { 'POST': ( {}, {}, ), }, - '/v1/software/deploy_complete/1': + '/v1/deploy/complete': { 'POST': ( {}, @@ -97,6 +108,7 @@ fixtures = { }, } + class Args: def __init__(self, **kwargs): for key, value in kwargs.items(): @@ -116,7 +128,7 @@ class DeployManagerTest(testtools.TestCase): def test_host_list(self): hosts = self.mgr.host_list() expect = [ - ('GET', '/v1/software/host_list', {}, None), + ('GET', '/v1/deploy_host', {}, None), ] self.assertEqual(self.api.calls, expect) self.assertEqual(len(hosts), 2) @@ -124,9 +136,9 @@ class DeployManagerTest(testtools.TestCase): HOST_LIST['data'][0]['hostname']) def test_show(self): - deploy = self.mgr.show() + self.mgr.show() expect = [ - ('GET', '/v1/software/deploy_show', {}, None), + ('GET', '/v1/deploy', {}, None), ] self.assertEqual(self.api.calls, expect) @@ -135,46 +147,46 @@ class DeployManagerTest(testtools.TestCase): args = Args(**input) check = self.mgr.precheck(args) expect = [ - ('POST', '/v1/software/deploy_precheck/1/force?region_name=RegionOne', {}, {}), + ('POST', '/v1/deploy/1/precheck', {}, {'force': 'true', 'region_name': 'RegionOne'}), ] self.assertEqual(self.api.calls, expect) self.assertEqual(len(check), 2) def test_start(self): - input = {'deployment': '1', 'force': 1} + input = {'deployment': '1', 'force': 'True'} args = Args(**input) resp = self.mgr.start(args) expect = [ - ('POST', '/v1/software/deploy_start/1/force', {}, {}), + ('POST', '/v1/deploy/1/start', {}, {'force': 'true'}), ] self.assertEqual(self.api.calls, expect) self.assertEqual(len(resp), 2) def test_host(self): - input = {'agent': '1', 'force': 1} + input = {'host': 'controller-1', 'force': False} args = Args(**input) - resp = self.mgr.host(args) + self.mgr.host(args) expect = [ - ('POST', '/v1/software/deploy_host/1/force', {}, {}), + ('POST', '/v1/deploy_host/controller-1', {}, None), ] self.assertEqual(self.api.calls, expect) def test_activate(self): - input = {'deployment': '1'} + input = {} args = Args(**input) resp = self.mgr.activate(args) expect = [ - ('POST', '/v1/software/deploy_activate/1', {}, {}), + ('POST', '/v1/deploy/activate', {}, {}), ] self.assertEqual(self.api.calls, expect) self.assertEqual(len(resp), 2) def test_complete(self): - input = {'deployment': '1'} + input = {} args = Args(**input) resp = self.mgr.complete(args) expect = [ - ('POST', '/v1/software/deploy_complete/1', {}, {}), + ('POST', '/v1/deploy/complete', {}, {}), ] self.assertEqual(self.api.calls, expect) self.assertEqual(len(resp), 2) diff --git a/software-client/software_client/tests/v1/test_release.py b/software-client/software_client/tests/v1/test_release.py index ebfd4bdd..29188bf6 100644 --- a/software-client/software_client/tests/v1/test_release.py +++ b/software-client/software_client/tests/v1/test_release.py @@ -20,70 +20,68 @@ from software_client.tests import utils import software_client.v1.release RELEASE = { - 'sd': - {'starlingx-24.03.0': { - 'state': 'deployed', - 'sw_version': '24.03.0', - 'status': 'REL', - 'unremovable': 'Y', - 'summary': 'STX 24.03 GA release', - 'description': 'STX 24.03 major GA release', - 'install_instructions': '', - 'warnings': '', - 'apply_active_release_only': '', - 'reboot_required': 'Y', - 'requires': [], - 'packages': [] + 'sd': { + 'starlingx-24.03.0': { + 'state': 'deployed', + 'sw_version': '24.03.0', + 'status': 'REL', + 'unremovable': 'Y', + 'summary': 'STX 24.03 GA release', + 'description': 'STX 24.03 major GA release', + 'install_instructions': '', + 'warnings': '', + 'apply_active_release_only': '', + 'reboot_required': 'Y', + 'requires': [], + 'packages': [] + } } - } } fixtures = { - '/v1/software/query?show=all': + '/v1/release?show=all': { 'GET': ( {}, {'sd': RELEASE['sd']}, ), }, - '/v1/software/show/1': + '/v1/release/1': { - 'POST': ( + 'DELETE': ( + {}, + None, + ), + 'GET': ( {}, True, ), + }, - '/v1/software/delete/1': - { - 'POST': ( - {}, - {}, - ), - }, - '/v1/software/is_available/1': - { - 'POST': ( - {}, - True, - ), - }, - '/v1/software/is_deployed/1': - { - 'POST': ( - {}, - False, - ), - }, - '/v1/software/is_committed/1': - { - 'POST': ( - {}, - False, - ), - }, - '/v1/software/install_local': + '/v1/release/1/is_available': { 'GET': ( + {}, + True, + ), + }, + '/v1/release/1/is_deployed': + { + 'GET': ( + {}, + False, + ), + }, + '/v1/release/1/is_committed': + { + 'GET': ( + {}, + False, + ), + }, + '/v1/deploy/install_local': + { + 'POST': ( {}, {}, ), @@ -112,7 +110,7 @@ class ReleaseManagerTest(testtools.TestCase): args = Args(**input) release = self.mgr.list(args) expect = [ - ('GET', '/v1/software/query?show=all', {}, None), + ('GET', '/v1/release?show=all', {}, None), ] self.assertEqual(self.api.calls, expect) self.assertEqual(len(release), 2) @@ -122,7 +120,7 @@ class ReleaseManagerTest(testtools.TestCase): args = Args(**input) release = self.mgr.show(args) expect = [ - ('POST', '/v1/software/show/1', {}, {}), + ('GET', '/v1/release/1', {}, None), ] self.assertEqual(self.api.calls, expect) self.assertEqual(len(release), 2) @@ -130,7 +128,7 @@ class ReleaseManagerTest(testtools.TestCase): def test_release_delete(self): response = self.mgr.release_delete("1") expect = [ - ('POST', '/v1/software/delete/1', {}, {}), + ('DELETE', '/v1/release/1', {}, None), ] self.assertEqual(self.api.calls, expect) self.assertEqual(len(response), 2) @@ -138,7 +136,7 @@ class ReleaseManagerTest(testtools.TestCase): def test_is_available(self): response = self.mgr.is_available('1') expect = [ - ('POST', '/v1/software/is_available/1', {}, {}), + ('GET', '/v1/release/1/is_available', {}, None), ] self.assertEqual(self.api.calls, expect) self.assertTrue(response[1], True) @@ -146,7 +144,7 @@ class ReleaseManagerTest(testtools.TestCase): def test_is_deployed(self): response = self.mgr.is_deployed('1') expect = [ - ('POST', '/v1/software/is_deployed/1', {}, {}), + ('GET', '/v1/release/1/is_deployed', {}, None), ] self.assertEqual(self.api.calls, expect) self.assertFalse(response[1], False) @@ -154,7 +152,7 @@ class ReleaseManagerTest(testtools.TestCase): def test_is_committed(self): response = self.mgr.is_committed('1') expect = [ - ('POST', '/v1/software/is_committed/1', {}, {}), + ('GET', '/v1/release/1/is_committed', {}, None), ] self.assertEqual(self.api.calls, expect) self.assertFalse(response[1], True) @@ -164,7 +162,7 @@ class ReleaseManagerTest(testtools.TestCase): args = Args(**input) response = self.mgr.upload(args) expect = [ - ('POST', '/v1/software/upload', {}, {}), + ('POST', '/v1/release', {}, {}), ] self.assertNotEqual(self.api.calls, expect) self.assertEqual(response, 0) @@ -174,7 +172,7 @@ class ReleaseManagerTest(testtools.TestCase): args = Args(**input) response = self.mgr.upload_dir(args) expect = [ - ('POST', '/v1/software/upload', {}, {}), + ('POST', '/v1/release', {}, {}), ] self.assertNotEqual(self.api.calls, expect) self.assertEqual(response, 0) @@ -182,15 +180,12 @@ class ReleaseManagerTest(testtools.TestCase): def test_install_local(self): self.mgr.install_local() expect = [ - ('GET', '/v1/software/install_local', {}, None), + ('POST', '/v1/deploy/install_local', {}, None), ] self.assertEqual(self.api.calls, expect) def test_commit_patch(self): - input = {'sw_version': '1', 'all': ''} - args = Args(**input) - kernel = self.mgr.commit_patch(args) expect = [ - ('GET', '/v1/software/commit_patch/1', {}, None), + ('POST', '/v1/release/1/commit_patch', {}, None), ] self.assertNotEqual(self.api.calls, expect) diff --git a/software-client/software_client/v1/deploy.py b/software-client/software_client/v1/deploy.py index 76f943ef..9d490476 100644 --- a/software-client/software_client/v1/deploy.py +++ b/software-client/software_client/v1/deploy.py @@ -7,7 +7,6 @@ import re import requests import signal -import sys import time from software_client.common import base @@ -27,15 +26,16 @@ class DeployManager(base.Manager): # args.deployment is a string deployment = args.deployment - # args.region is a string - region_name = args.region_name - - path = "/v1/software/deploy_precheck/%s" % (deployment) + path = "/v1/deploy/%s/precheck" % (deployment) + body = {} if args.force: - path += "/force" - path += "?region_name=%s" % region_name + body["force"] = "true" - return self._create(path, body={}) + if args.region_name: + body["region_name"] = args.region_name + + res = self._post(path, body=body) + return res def start(self, args): # args.deployment is a string @@ -45,51 +45,31 @@ class DeployManager(base.Manager): signal.signal(signal.SIGINT, signal.SIG_IGN) # Issue deploy_start request + path = "/v1/deploy/%s/start" % (deployment) + body = {} if args.force: - path = "/v1/software/deploy_start/%s/force" % (deployment) - else: - path = "/v1/software/deploy_start/%s" % (deployment) + body["force"] = "true" - return self._create(path, body={}) + return self._post(path, body=body) def host(self, args): # args.deployment is a string hostname = args.host # Issue deploy_host request and poll for results - path = "/v1/software/deploy_host/%s" % (hostname) + path = "/v1/deploy_host/%s" % (hostname) if args.force: path += "/force" - req, data = self._create(path, body={}) - if req.status_code == 200: - if 'error' in data and data["error"] != "": - print("Error:") - print(data["error"]) - rc = 1 - else: - # NOTE(bqian) should consider return host_list instead. - rc = self.wait_for_install_complete(hostname) - elif req.status_code == 500: - print("An internal error has occurred. " - "Please check /var/log/software.log for details") - rc = 1 - else: - m = re.search("(Error message:.*)", req.text, re.MULTILINE) - if m: - print(m.group(0)) - else: - print("%s %s" % (req.status_code, req.reason)) - rc = 1 - return rc + return self._create(path) def activate(self, args): # Ignore interrupts during this function signal.signal(signal.SIGINT, signal.SIG_IGN) # Issue deploy_start request - path = "/v1/software/deploy_activate" + path = "/v1/deploy/activate" return self._create(path, body={}) @@ -98,20 +78,20 @@ class DeployManager(base.Manager): signal.signal(signal.SIGINT, signal.SIG_IGN) # Issue deploy_start request - path = "/v1/software/deploy_complete/" + path = "/v1/deploy/complete" return self._create(path, body={}) def host_list(self): - path = '/v1/software/host_list' + path = '/v1/deploy_host' return self._list(path, "") def show(self): - path = '/v1/software/deploy' - return self._list(path, "") + path = '/v1/deploy' + return self._list(path) def wait_for_install_complete(self, hostname): - url = "/v1/software/host_list" + url = "/v1/deploy_host" rc = 0 max_retries = 4 diff --git a/software-client/software_client/v1/deploy_shell.py b/software-client/software_client/v1/deploy_shell.py index 95b234ec..220a4a61 100644 --- a/software-client/software_client/v1/deploy_shell.py +++ b/software-client/software_client/v1/deploy_shell.py @@ -24,23 +24,41 @@ def do_show(cc, args): resp, data = cc.deploy.show() if args.debug: utils.print_result_debug(resp, data) + + rc = utils.check_rc(resp, data) + if rc == 0: + if len(data) == 0: + print("No deploy in progress") + else: + header_data_list = {"From Release": "from_release", + "To Release": "to_release", + "RR": "reboot_required", + "State": "state"} + utils.display_result_list(header_data_list, data) else: - header_data_list = {"From Release": "from_release", "To Release": "to_release", "RR": "reboot_required", "State": "state"} - utils.display_result_list(header_data_list, data) - - return utils.check_rc(resp, data) + utils.display_info(resp) + return rc def do_host_list(cc, args): """List of hosts for software deployment """ resp, data = cc.deploy.host_list() if args.debug: utils.print_result_debug(resp, data) - else: - header_data_list = {"Host": "hostname", "From Release": "software_release", "To Release": "target_release", "RR": "reboot_required", "State": "host_state"} - utils.display_result_list(header_data_list, data) - return utils.check_rc(resp, data) + rc = utils.check_rc(resp, data) + if rc == 0: + if len(data) == 0: + print("No deploy in progress") + else: + header_data_list = {"Host": "hostname", "From Release": "software_release", + "To Release": "target_release", "RR": "reboot_required", + "State": "host_state"} + utils.display_result_list(header_data_list, data) + else: + utils.display_info(resp) + + return rc @utils.arg('deployment', @@ -51,18 +69,18 @@ def do_host_list(cc, args): required=False, help='Allow bypassing non-critical checks') @utils.arg('--region_name', - default='RegionOne', + default=None, required=False, help='Run precheck against a subcloud') def do_precheck(cc, args): """Verify whether prerequisites for installing the software deployment are satisfied""" - req, data = cc.deploy.precheck(args) + resp, data = cc.deploy.precheck(args) if args.debug: - utils.print_result_debug(req, data) - else: - utils.print_software_op_result(req, data) + utils.print_result_debug(resp, data) - return utils.check_rc(req, data) + utils.display_info(resp) + + return utils.check_rc(resp, data) @utils.arg('deployment', @@ -77,8 +95,8 @@ def do_start(cc, args): resp, data = cc.deploy.start(args) if args.debug: utils.print_result_debug(resp, data) - else: - utils.display_info(resp) + + utils.display_info(resp) return utils.check_rc(resp, data) @@ -92,25 +110,34 @@ def do_start(cc, args): help="Force deploy host") def do_host(cc, args): """Deploy prestaged software deployment to the host""" - return cc.deploy.host(args) + resp, data = cc.deploy.host(args) + + if args.debug: + utils.print_result_debug(resp, data) + + utils.display_info(resp) + + return utils.check_rc(resp, data) def do_activate(cc, args): """Activate the software deployment""" - req, data = cc.deploy.activate(args) + resp, data = cc.deploy.activate(args) if args.debug: - utils.print_result_debug(req, data) - else: - utils.print_software_op_result(req, data) + utils.print_result_debug(resp, data) + + utils.display_info(resp) + + return utils.check_rc(resp, data) - return utils.check_rc(req, data) def do_complete(cc, args): """Complete the software deployment""" - req, data = cc.deploy.complete(args) - if args.debug: - utils.print_result_debug(req, data) - else: - utils.print_software_op_result(req, data) + resp, data = cc.deploy.complete(args) - return utils.check_rc(req, data) + if args.debug: + utils.print_result_debug(resp, data) + + utils.display_info(resp) + + return utils.check_rc(resp, data) diff --git a/software-client/software_client/v1/release.py b/software-client/software_client/v1/release.py index 65fe2bbf..cc1bc153 100644 --- a/software-client/software_client/v1/release.py +++ b/software-client/software_client/v1/release.py @@ -4,7 +4,6 @@ # SPDX-License-Identifier: Apache-2.0 # -import json import os import signal import sys @@ -27,27 +26,40 @@ class ReleaseManager(base.Manager): resource_class = Release def list(self, args): - state = args.state # defaults to "all" - extra_opts = "" + path = "/v1/release" + state = args.state + additions = [] + if state: + additions.append("show=%s" % state) + if args.release: - extra_opts = "&release=%s" % args.release - path = "/v1/software/query?show=%s%s" % (state, extra_opts) + additions.append("release=%s" % args.release) + + if len(additions) > 0: + path = path + "?" + "&".join(additions) + return self._list(path, "") + def show(self, args): + releases = "/".join(args.release) + + path = "/v1/release/%s" % (releases) + return self._fetch(path) + def is_available(self, release): releases = "/".join(release) - path = '/v1/software/is_available/%s' % (releases) - return self._create(path, body={}) + path = '/v1/release/%s/is_available' % (releases) + return self._fetch(path) def is_deployed(self, release): releases = "/".join(release) - path = '/v1/software/is_deployed/%s' % (releases) - return self._create(path, body={}) + path = '/v1/release/%s/is_deployed' % (releases) + return self._fetch(path) def is_committed(self, release): releases = "/".join(release) - path = '/v1/software/is_committed/%s' % (releases) - return self._create(path, body={}) + path = '/v1/release/%s/is_committed' % (releases) + return self._fetch(path) def upload(self, args): rc = 0 @@ -81,7 +93,7 @@ class ReleaseManager(base.Manager): print("No file to be uploaded.") return rc - path = '/v1/software/upload' + path = '/v1/release' if is_local: to_upload_filenames = valid_files headers = {'Content-Type': 'text/plain'} @@ -136,7 +148,6 @@ class ReleaseManager(base.Manager): temp_sig_files, file=sys.stderr) return 1 - for software_file in sorted(set(all_raw_files)): _, ext = os.path.splitext(software_file) if ext in constants.SUPPORTED_UPLOAD_FILE_EXT: @@ -146,22 +157,8 @@ class ReleaseManager(base.Manager): encoder = MultipartEncoder(fields=to_upload_files) headers = {'Content-Type': encoder.content_type} - path = '/v1/software/upload' - req, data = self._create_multipart(path, body=encoder, headers=headers) - if args.debug: - utils.print_result_debug(req, data) - else: - utils.print_software_op_result(req, data) - data = json.loads(req.text) - data_list = [(lambda key, value: (key, value["id"]))(k, v) - for d in data["upload_info"] - for k, v in d.items() - if not k.endswith(".sig")] - - header_data_list = ["Uploaded File", "Id"] - has_error = 'error' in data and data["error"] - utils.print_result_list(header_data_list, data_list, has_error) - return utils.check_rc(req, data) + path = '/v1/release' + return self._create_multipart(path, body=encoder, headers=headers) def commit_patch(self, args): # Ignore interrupts during this function @@ -179,7 +176,7 @@ class ReleaseManager(base.Manager): elif args.all: # Get a list of all patches extra_opts = "&release=%s" % relopt - url = "/v1/software/query?show=patch%s" % (extra_opts) + url = "/v1/release?show=patch%s" % (extra_opts) resp, body = self._list(url, "") @@ -265,16 +262,10 @@ class ReleaseManager(base.Manager): # Ignore interrupts during this function signal.signal(signal.SIGINT, signal.SIG_IGN) - path = "/v1/software/install_local" - return self._list(path, "") - - def show(self, args): - releases = "/".join(args.release) - - path = "/v1/software/show/%s" % (releases) - return self._create(path, body={}) + path = "/v1/deploy/install_local" + return self._post(path) def release_delete(self, release_id): release_ids = "/".join(release_id) - path = '/v1/software/delete/%s' % release_ids - return self._create(path, body={}) + path = '/v1/release/%s' % release_ids + return self._delete(path) diff --git a/software-client/software_client/v1/release_shell.py b/software-client/software_client/v1/release_shell.py index 433885b9..10d1bcec 100644 --- a/software-client/software_client/v1/release_shell.py +++ b/software-client/software_client/v1/release_shell.py @@ -13,19 +13,23 @@ from software_client.common import utils help='filter against a release ID') # --state is an optional argument. default: "all" @utils.arg('--state', - default="all", + default=None, required=False, help='filter against a release state') def do_list(cc, args): """List the software releases""" - req, data = cc.release.list(args) + resp, data = cc.release.list(args) if args.debug: - utils.print_result_debug(req, data) - else: + utils.print_result_debug(resp, data) + + rc = utils.check_rc(resp, data) + if rc == 0: header_data_list = {"Release": "release_id", "RR": "reboot_required", "State": "state"} utils.display_result_list(header_data_list, data) + else: + utils.display_info(resp) - return utils.check_rc(req, data) + return rc @utils.arg('release', @@ -38,17 +42,20 @@ def do_list(cc, args): help='list packages contained in the release') def do_show(cc, args): """Show the software release""" - list_packages = args.packages - req, data = cc.release.show(args) + resp, data = cc.release.show(args) if args.debug: - utils.print_result_debug(req, data) + utils.print_result_debug(resp, data) + + rc = utils.check_rc(resp, data) + if rc == 0: + utils.display_detail_result(data) else: - for d in data: - utils.display_detail_result(d) + utils.display_info(resp) - return utils.check_rc(req, data) + return rc +# NOTE(bqian) need to review the commit patch CLI @utils.arg('patch', nargs="+", # accepts a list help='Patch ID/s to commit') @@ -72,24 +79,27 @@ def do_commit_patch(cc, args): def do_install_local(cc, args): - """ Trigger patch install/remove on the local host. + """Trigger patch install/remove on the local host. This command can only be used for patch installation - prior to initial configuration.""" - req, data = cc.release.install_local() + prior to initial configuration. + """ + resp, data = cc.release.install_local() if args.debug: - utils.print_result_debug(req, data) - else: - utils.print_software_op_result(req, data) + utils.print_result_debug(resp, data) - return utils.check_rc(req, data) + utils.display_info(resp) + + return utils.check_rc(resp, data) +# NOTE (bqian) verify this CLI is needed @utils.arg('release', nargs="+", # accepts a list help='List of releases') def do_is_available(cc, args): """Query Available state for list of releases. - Returns True if all are Available, False otherwise.""" + Returns True if all are Available, False otherwise. + """ req, result = cc.release.is_available(args.release) rc = 1 if req.status_code == 200: @@ -103,12 +113,14 @@ def do_is_available(cc, args): return rc +# NOTE (bqian) verify this CLI is needed @utils.arg('release', nargs="+", # accepts a list help='List of releases') def do_is_deployed(cc, args): """Query Deployed state for list of releases. - Returns True if all are Deployed, False otherwise.""" + Returns True if all are Deployed, False otherwise. + """ req, result = cc.release.is_deployed(args.release) rc = 1 if req.status_code == 200: @@ -122,12 +134,14 @@ def do_is_deployed(cc, args): return rc +# NOTE (bqian) verify this CLI is needed @utils.arg('release', nargs="+", # accepts a list help='List of releases') def do_is_committed(cc, args): """Query Committed state for list of releases. - Returns True if all are Committed, False otherwise.""" + Returns True if all are Committed, False otherwise. + """ req, result = cc.release.is_committed(args.release) rc = 1 if req.status_code == 200: @@ -141,6 +155,25 @@ def do_is_committed(cc, args): return rc +def _print_upload_result(resp, data, debug): + if debug: + utils.print_result_debug(resp, data) + + rc = utils.check_rc(resp, data) + + utils.display_info(resp) + if rc == 0: + if data["upload_info"]: + upload_info = data["upload_info"] + data_list = [{"file": k, "release": v["id"]} + for d in upload_info for k, v in d.items() + if not k.endswith(".sig")] + + header_data_list = {"Uploaded File": "file", "Release": "release"} + utils.display_result_list(header_data_list, data_list) + return rc + + @utils.arg('release', metavar='(iso + sig) | patch', nargs="+", # accepts a list @@ -153,23 +186,10 @@ def do_is_committed(cc, args): action='store_true') def do_upload(cc, args): """Upload a software release""" - req, data = cc.release.upload(args) - if args.debug: - utils.print_result_debug(req, data) - else: - utils.print_software_op_result(req, data) - data_list = [(k, v["id"]) - for d in data["upload_info"] for k, v in d.items() - if not k.endswith(".sig")] + resp, data = cc.release.upload(args) + _print_upload_result(resp, data, args.debug) - header_data_list = ["Uploaded File", "Id"] - has_error = 'error' in data and data["error"] - utils.print_result_list(header_data_list, data_list, has_error) - rc = 0 - if utils.check_rc(req, data) != 0: - # We hit a failure. Update rc but keep looping - rc = 1 - return rc + return utils.check_rc(resp, data) @utils.arg('release', @@ -182,7 +202,10 @@ def do_upload(cc, args): 'ONE pair of (iso + sig)')) def do_upload_dir(cc, args): """Upload a software release dir""" - return cc.release.upload_dir(args) + resp, data = cc.release.upload_dir(args) + _print_upload_result(resp, data, args.debug) + + return utils.check_rc(resp, data) @utils.arg('release', @@ -190,6 +213,9 @@ def do_upload_dir(cc, args): help='Release ID to delete') def do_delete(cc, args): """Delete the software release""" - resp, body = cc.release.release_delete(args.release) + resp, data = cc.release.release_delete(args.release) + if args.debug: + utils.print_result_debug(resp, data) + utils.display_info(resp) - return utils.check_rc(resp, body) + return utils.check_rc(resp, data) diff --git a/software-client/test-requirements.txt b/software-client/test-requirements.txt index 83c0f09a..578639e2 100644 --- a/software-client/test-requirements.txt +++ b/software-client/test-requirements.txt @@ -6,3 +6,4 @@ coverage httplib2 pylint stestr +tabulate diff --git a/software/scripts/deploy-precheck b/software/scripts/deploy-precheck index cdb72548..7e70cc06 100644 --- a/software/scripts/deploy-precheck +++ b/software/scripts/deploy-precheck @@ -91,7 +91,7 @@ class HealthCheck(object): :return: boolean indicating success/failure and list of patches that are not in the 'deployed' state """ - url = self._software_endpoint + '/query?show=deployed' + url = self._software_endpoint + '/release?show=deployed' headers = {"X-Auth-Token": self._software_token} response = requests.get(url, headers=headers, timeout=10) @@ -233,7 +233,7 @@ class PatchHealthCheck(HealthCheck): def _get_required_patches(self): """Get required patches for a target release""" - url = self._software_endpoint + '/query' + url = self._software_endpoint + '/release' headers = {"X-Auth-Token": self._software_token} response = requests.get(url, headers=headers, timeout=10) @@ -242,9 +242,9 @@ class PatchHealthCheck(HealthCheck): return [] required_patches = [] - for release, values in response.json()["sd"].items(): - if values["sw_version"] == self._target_release: - required_patches.extend(values["requires"]) + for release in response.json(): + if release["sw_version"] == self._target_release: + required_patches.extend(release["requires"]) break return required_patches diff --git a/software/scripts/upgrade_utils.py b/software/scripts/upgrade_utils.py index e6f4bb7c..63c2ed92 100644 --- a/software/scripts/upgrade_utils.py +++ b/software/scripts/upgrade_utils.py @@ -53,7 +53,7 @@ def get_token_endpoint(config, service_type="platform"): raise Exception("Failed to get token and endpoint. Error: %s", str(e)) if service_type == "usm": - endpoint += "/v1/software" + endpoint += "/v1" return token, endpoint diff --git a/software/software/api/controllers/v1/__init__.py b/software/software/api/controllers/v1/__init__.py index 55adcd1d..b56bb627 100644 --- a/software/software/api/controllers/v1/__init__.py +++ b/software/software/api/controllers/v1/__init__.py @@ -22,7 +22,10 @@ from wsme import types as wtypes from software.api.controllers.v1 import base from software.api.controllers.v1 import link -from software.api.controllers.v1 import software +from software.api.controllers.v1.software import SoftwareAPIController +from software.api.controllers.v1.release import ReleaseController +from software.api.controllers.v1.deploy import DeployController +from software.api.controllers.v1.deploy_host import DeployHostController class MediaType(base.APIBase): @@ -49,6 +52,9 @@ class V1(base.APIBase): "Links that point to a specific URL for this version and documentation" software = [link.Link] + release = [link.Link] + deploy = [link.Link] + deploy_host = [link.Link] "Links to the software resource" @classmethod @@ -56,18 +62,38 @@ class V1(base.APIBase): v1 = V1() v1.id = "v1" v1.links = [link.Link.make_link('self', pecan.request.host_url, - 'v1', '', bookmark=True), - ] + 'v1', '', bookmark=True)] + v1.media_types = [MediaType('application/json', 'application/vnd.openstack.software.v1+json')] + v1.release = [link.Link.make_link('self', pecan.request.host_url, + 'release', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'release', '', + bookmark=True)] + + v1.deploy = [link.Link.make_link('self', pecan.request.host_url, + 'deploy', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'deploy', '', + bookmark=True)] + + v1.deploy_host = [link.Link.make_link('self', pecan.request.host_url, + 'deploy_host', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'deploy_host', '', + bookmark=True)] + v1.software = [link.Link.make_link('self', pecan.request.host_url, 'software', ''), link.Link.make_link('bookmark', pecan.request.host_url, 'software', '', - bookmark=True) - ] + bookmark=True)] return v1 @@ -75,7 +101,10 @@ class V1(base.APIBase): class Controller(rest.RestController): """Version 1 API controller root.""" - software = software.SoftwareAPIController() + software = SoftwareAPIController() + release = ReleaseController() + deploy = DeployController() + deploy_host = DeployHostController() @wsme_pecan.wsexpose(V1) def get(self): diff --git a/software/software/api/controllers/v1/deploy.py b/software/software/api/controllers/v1/deploy.py new file mode 100644 index 00000000..e1f204f1 --- /dev/null +++ b/software/software/api/controllers/v1/deploy.py @@ -0,0 +1,83 @@ +""" +Copyright (c) 2024 Wind River Systems, Inc. + +SPDX-License-Identifier: Apache-2.0 + +""" +import logging +from pecan import expose +from pecan import request +from pecan.rest import RestController + +from software.exceptions import SoftwareServiceError +from software.release_data import reload_release_data +from software.software_controller import sc + + +LOG = logging.getLogger('main_logger') + + +class DeployController(RestController): + _custom_actions = { + 'activate': ['POST'], + 'precheck': ['POST'], + 'start': ['POST'], + 'complete': ['POST'], + 'is_sync_controller': ['GET'], + 'software_upgrade': ['GET'], + } + + @expose(method='POST', template='json') + def activate(self): + reload_release_data() + + result = sc.software_deploy_activate_api() + sc.software_sync() + return result + + @expose(method='POST', template='json') + def complete(self): + reload_release_data() + + result = sc.software_deploy_complete_api() + sc.software_sync() + return result + + @expose(method='POST', template='json') + def precheck(self, release, force=None, region_name=None): + _force = force is not None + reload_release_data() + + result = sc.software_deploy_precheck_api(release, _force, region_name) + return result + + @expose(method='POST', template='json') + def start(self, release, force=None): + reload_release_data() + _force = force is not None + + if sc.any_patch_host_installing(): + raise SoftwareServiceError(error="Rejected: One or more nodes are installing a release.") + + result = sc.software_deploy_start_api(release, _force) + + sc.send_latest_feed_commit_to_agent() + sc.software_sync() + + return result + + @expose(method='GET', template='json') + def get_all(self): + reload_release_data() + from_release = request.GET.get("from_release") + to_release = request.GET.get("to_release") + result = sc.software_deploy_show_api(from_release, to_release) + return result + + @expose(method='GET', template='json') + def in_sync_controller(self): + return sc.in_sync_controller_api() + + @expose(method='GET', template='json') + def software_upgrade(self): + return sc.get_software_upgrade() diff --git a/software/software/api/controllers/v1/deploy_host.py b/software/software/api/controllers/v1/deploy_host.py new file mode 100644 index 00000000..41be110b --- /dev/null +++ b/software/software/api/controllers/v1/deploy_host.py @@ -0,0 +1,46 @@ +""" +Copyright (c) 2024 Wind River Systems, Inc. + +SPDX-License-Identifier: Apache-2.0 + +""" +import logging +from pecan import expose +from pecan.rest import RestController + +from software.release_data import reload_release_data +from software.software_controller import sc + + +LOG = logging.getLogger('main_logger') + + +class DeployHostController(RestController): + _custom_actions = { + 'install_local': ['POST'], + } + + @expose(method='GET', template='json') + def get_all(self): + reload_release_data() + result = sc.deploy_host_list() + return result + + @expose(method='POST', template='json') + def post(self, *args): + reload_release_data() + if len(list(args)) == 0: + return dict(error="Host must be specified for install") + force = False + if len(list(args)) > 1 and 'force' in list(args)[1:]: + force = True + + result = sc.software_deploy_host_api(list(args)[0], force, async_req=True) + + return result + + @expose(method='POST', template='json') + def install_local(self): + reload_release_data() + result = sc.software_install_local_api() + return result diff --git a/software/software/api/controllers/v1/release.py b/software/software/api/controllers/v1/release.py new file mode 100644 index 00000000..486a7e71 --- /dev/null +++ b/software/software/api/controllers/v1/release.py @@ -0,0 +1,130 @@ +""" +Copyright (c) 2024 Wind River Systems, Inc. + +SPDX-License-Identifier: Apache-2.0 + +""" +import cgi +import json +import logging +import os +from pecan import expose +from pecan import request +from pecan.rest import RestController +import shutil +import webob + +from software import constants +from software.exceptions import SoftwareServiceError +from software.release_data import reload_release_data +from software.software_controller import sc +from software import utils + + +LOG = logging.getLogger('main_logger') + + +class ReleaseController(RestController): + _custom_actions = { + 'commit': ['POST'], + 'commit_dry_run': ['POST'], + 'is_available': ['GET'], + 'is_committed': ['GET'], + 'is_deployed': ['GET'], + } + + @expose(method='GET', template='json') + def get_all(self, **kwargs): + reload_release_data() + sd = sc.software_release_query_cached(**kwargs) + return sd + + @expose(method='GET', template='json') + def get_one(self, release): + reload_release_data() + result = sc.software_release_query_specific_cached([release]) + if len(result) == 1: + return result[0] + msg = f"Release {release} not found" + raise webob.exc.HTTPNotFound(msg) + + @expose(method='POST', template='json') + def post(self): + reload_release_data() + is_local = False + temp_dir = None + uploaded_files = [] + request_data = [] + local_files = [] + + # --local option only sends a list of file names + if (request.content_type == "text/plain"): + local_files = list(json.loads(request.body)) + is_local = True + else: + request_data = list(request.POST.items()) + temp_dir = os.path.join(constants.SCRATCH_DIR, 'upload_files') + + try: + if len(request_data) == 0 and len(local_files) == 0: + raise SoftwareServiceError(error="No files uploaded") + + if is_local: + uploaded_files = local_files + LOG.info("Uploaded local files: %s", uploaded_files) + else: + # Protect against duplications + uploaded_files = sorted(set(request_data)) + # Save all uploaded files to /scratch/upload_files dir + for file_item in uploaded_files: + assert isinstance(file_item[1], cgi.FieldStorage) + utils.save_temp_file(file_item[1], temp_dir) + + # Get all uploaded files from /scratch dir + uploaded_files = utils.get_all_files(temp_dir) + LOG.info("Uploaded files: %s", uploaded_files) + + # Process uploaded files + return sc.software_release_upload(uploaded_files) + + finally: + # Remove all uploaded files from /scratch dir + sc.software_sync() + if temp_dir: + shutil.rmtree(temp_dir, ignore_errors=True) + + @expose(method='DELETE', template='json') + def delete(self, *args): + reload_release_data() + result = sc.software_release_delete_api(list(args)) + sc.software_sync() + return result + + @expose(method='POST', template='json') + def commit(self, *args): + reload_release_data() + result = sc.patch_commit(list(args)) + sc.software_sync() + + return result + + @expose(method='POST', template='json') + def commit_dry_run(self, *args): + reload_release_data() + result = sc.patch_commit(list(args), dry_run=True) + return result + + @expose(method='GET', template='json') + def is_available(self, *args): + reload_release_data() + return sc.is_available(list(args)) + + @expose(method='GET', template='json') + def is_committed(self, *args): + reload_release_data() + return sc.is_committed(list(args)) + + @expose(method='GET', template='json') + def is_deployed(self, *args): + reload_release_data() + return sc.is_deployed(list(args)) diff --git a/software/software/api/controllers/v1/software.py b/software/software/api/controllers/v1/software.py index 59fc0f06..e37a5f59 100644 --- a/software/software/api/controllers/v1/software.py +++ b/software/software/api/controllers/v1/software.py @@ -4,222 +4,16 @@ Copyright (c) 2023-2024 Wind River Systems, Inc. SPDX-License-Identifier: Apache-2.0 """ -import cgi -import json import logging -import os from pecan import expose -from pecan import request -import shutil +from pecan.rest import RestController -from software import constants -from software.exceptions import SoftwareError -from software.exceptions import SoftwareServiceError -from software.release_data import reload_release_data from software.software_controller import sc -from software import utils - LOG = logging.getLogger('main_logger') -class SoftwareAPIController(object): - - @expose('json') - def commit_patch(self, *args): - reload_release_data() - result = sc.patch_commit(list(args)) - sc.software_sync() - - return result - - @expose('json') - def commit_dry_run(self, *args): - reload_release_data() - result = sc.patch_commit(list(args), dry_run=True) - return result - - @expose('json') - @expose('query.xml', content_type='application/xml') - def delete(self, *args): - reload_release_data() - result = sc.software_release_delete_api(list(args)) - sc.software_sync() - - return result - - @expose('json') - @expose('query.xml', content_type='application/xml') - def deploy_activate(self): - reload_release_data() - - result = sc.software_deploy_activate_api() - sc.software_sync() - return result - - @expose('json') - @expose('query.xml', content_type='application/xml') - def deploy_complete(self): - reload_release_data() - - result = sc.software_deploy_complete_api() - sc.software_sync() - return result - - @expose('json') - @expose('query.xml', content_type='application/xml') - def deploy_host(self, *args): - reload_release_data() - if len(list(args)) == 0: - return dict(error="Host must be specified for install") - force = False - if len(list(args)) > 1 and 'force' in list(args)[1:]: - force = True - - result = sc.software_deploy_host_api(list(args)[0], force, async_req=True) - - return result - - @expose('json') - @expose('query.xml', content_type='application/xml') - def deploy_precheck(self, *args, **kwargs): - reload_release_data() - force = False - if 'force' in list(args): - force = True - - result = sc.software_deploy_precheck_api(list(args)[0], force, **kwargs) - - return result - - @expose('json') - @expose('query.xml', content_type='application/xml') - def deploy_start(self, *args, **kwargs): - reload_release_data() - # if --force is provided - force = 'force' in list(args) - - if sc.any_patch_host_installing(): - raise SoftwareServiceError(error="Rejected: One or more nodes are installing a release.") - - result = sc.software_deploy_start_api(list(args)[0], force, **kwargs) - - sc.send_latest_feed_commit_to_agent() - sc.software_sync() - - return result - - @expose('json', method="GET") - def deploy(self): - reload_release_data() - from_release = request.GET.get("from_release") - to_release = request.GET.get("to_release") - result = sc.software_deploy_show_api(from_release, to_release) - return result - - @expose('json') - @expose('query.xml', content_type='application/xml') - def install_local(self): - reload_release_data() - result = sc.software_install_local_api() - - return result - - @expose('json') - def is_available(self, *args): - reload_release_data() - return sc.is_available(list(args)) - - @expose('json') - def is_committed(self, *args): - reload_release_data() - return sc.is_committed(list(args)) - - @expose('json') - def is_deployed(self, *args): - reload_release_data() - return sc.is_deployed(list(args)) - - @expose('json') - @expose('show.xml', content_type='application/xml') - def show(self, *args): - reload_release_data() - result = sc.software_release_query_specific_cached(list(args)) - - return result - - @expose('json') - @expose('query.xml', content_type='application/xml') - def upload(self): - reload_release_data() - is_local = False - temp_dir = None - uploaded_files = [] - request_data = [] - local_files = [] - - # --local option only sends a list of file names - if (request.content_type == "text/plain"): - local_files = list(json.loads(request.body)) - is_local = True - else: - request_data = list(request.POST.items()) - temp_dir = os.path.join(constants.SCRATCH_DIR, 'upload_files') - - try: - if len(request_data) == 0 and len(local_files) == 0: - raise SoftwareError("No files uploaded") - - if is_local: - uploaded_files = local_files - LOG.info("Uploaded local files: %s", uploaded_files) - else: - # Protect against duplications - uploaded_files = sorted(set(request_data)) - # Save all uploaded files to /scratch/upload_files dir - for file_item in uploaded_files: - assert isinstance(file_item[1], cgi.FieldStorage) - utils.save_temp_file(file_item[1], temp_dir) - - # Get all uploaded files from /scratch dir - uploaded_files = utils.get_all_files(temp_dir) - LOG.info("Uploaded files: %s", uploaded_files) - - # Process uploaded files - return sc.software_release_upload(uploaded_files) - - finally: - # Remove all uploaded files from /scratch dir - sc.software_sync() - if temp_dir: - shutil.rmtree(temp_dir, ignore_errors=True) - - @expose('json') - @expose('query.xml', content_type='application/xml') - def query(self, **kwargs): - reload_release_data() - sd = sc.software_release_query_cached(**kwargs) - return sd - - @expose('json', method="GET") - def host_list(self): - reload_release_data() - result = sc.deploy_host_list() - return result - +class SoftwareAPIController(RestController): @expose(method='GET', template='json') def in_sync_controller(self): return sc.in_sync_controller_api() - - @expose(method='GET', template='json') - def software_upgrade(self): - return sc.get_software_upgrade() - - @expose(method='GET', template='json') - def software_host_upgrade(self, *args): - args_list = list(args) - if not args_list: - return sc.get_all_software_host_upgrade() - - hostname = args_list[0] - return sc.get_one_software_host_upgrade(hostname) diff --git a/software/software/authapi/app.py b/software/software/authapi/app.py index cb9879f1..708623e6 100755 --- a/software/software/authapi/app.py +++ b/software/software/authapi/app.py @@ -12,7 +12,6 @@ from software.authapi import acl from software.authapi import config from software.authapi import hooks from software.authapi import policy -from software.parsable_error import ParsableErrorMiddleware from software.utils import ExceptionHook auth_opts = [ @@ -56,7 +55,6 @@ def setup_app(pecan_config=None, extra_hooks=None): debug=False, force_canonical=getattr(pecan_config.app, 'force_canonical', True), hooks=app_hooks, - wrap_app=ParsableErrorMiddleware, guess_content_type_from_ext=False, # Avoid mime-type lookup ) diff --git a/software/software/software_controller.py b/software/software/software_controller.py index 70b38295..be50130a 100644 --- a/software/software/software_controller.py +++ b/software/software/software_controller.py @@ -1134,7 +1134,7 @@ class PatchController(PatchService): # Disallow the install msg = "This command can only be used before initial system configuration." LOG.exception(msg) - raise SoftwareFail(msg) + raise SoftwareServiceError(error=msg) update_hosts_file = False @@ -1346,7 +1346,7 @@ class PatchController(PatchService): # Get the release_id from the patch's metadata # and check to see if it's already uploaded - release_id = get_release_from_patch(patch_file,'id') + release_id = get_release_from_patch(patch_file, 'id') release = self.release_collection.get_release_by_id(release_id) @@ -2218,15 +2218,18 @@ class PatchController(PatchService): return dict(info=msg_info, warning=msg_warning, error=msg_error, system_healthy=system_healthy) - def software_deploy_precheck_api(self, deployment: str, force: bool = False, **kwargs) -> dict: + def software_deploy_precheck_api(self, deployment: str, force: bool = False, region_name=None) -> dict: """ Verify if system satisfy the requisites to upgrade to a specified deployment. :param deployment: full release name, e.g. starlingx-MM.mm.pp :param force: if True will ignore minor alarms during precheck :return: dict of info, warning and error messages """ + release = self._release_basic_checks(deployment) - region_name = kwargs["region_name"] + if region_name is None: + region_name = utils.get_local_region_name() + release_version = release.sw_release patch = not utils.is_upgrade_deploy(SW_VERSION, release_version) return self._deploy_precheck(release_version, force, region_name, patch) diff --git a/software/software/software_functions.py b/software/software/software_functions.py index 8fea59e5..868be3b6 100644 --- a/software/software/software_functions.py +++ b/software/software/software_functions.py @@ -1356,7 +1356,7 @@ def deploy_host_validations(hostname): validate_host_deploy_order(hostname) if not is_host_locked_and_online(hostname): msg = f"Host {hostname} must be {constants.ADMIN_LOCKED}." - raise SoftwareServiceError(msg) + raise SoftwareServiceError(error=msg) def validate_host_deploy_order(hostname): @@ -1391,7 +1391,7 @@ def validate_host_deploy_order(hostname): if host.get("state") == states.DEPLOY_HOST_STATES.DEPLOYED.value: ordered_list.remove(host.get("hostname")) if not ordered_list: - raise SoftwareServiceError("All hosts are already in deployed state.") + raise SoftwareServiceError(error="All hosts are already in deployed state.") # If there is only workers nodes there is no order to deploy if hostname == ordered_list[0] or (ordered_list[0] in workers_list and hostname in workers_list): return @@ -1399,5 +1399,6 @@ def validate_host_deploy_order(hostname): elif is_patch_release and ordered_list[0] in controllers_list and hostname in controllers_list: return else: - raise SoftwareServiceError(f"{hostname.capitalize()} do not satisfy the right order of deployment " - f"should be {ordered_list[0]}") + errmsg = f"{hostname} does not satisfy the right order of deployment " + \ + f"should be {ordered_list[0]}" + raise SoftwareServiceError(error=errmsg) diff --git a/software/software/utils.py b/software/software/utils.py index a12d68bb..ff77e275 100644 --- a/software/software/utils.py +++ b/software/software/utils.py @@ -52,6 +52,9 @@ class ExceptionHook(hooks.PecanHook): LOG.exception(e) data = dict(info=e.info, warning=e.warning, error=e.error) + elif isinstance(e, webob.exc.HTTPClientError): + LOG.warning("%s. Signature [%s]" % (str(e), signature)) + raise e else: # with an exception that is not pre-categorized as "expected", it is a # bug. Or not properly categorizing the exception itself is a bug. @@ -276,6 +279,12 @@ def get_all_files(temp_dir=constants.SCRATCH_DIR): return [] +def get_local_region_name(): + config = CONF.get('keystone_authtoken') + region_name = config.region_name + return region_name + + def get_auth_token_and_endpoint(user: dict, service_type: str, region_name: str, interface: str): """Get the auth token and endpoint for a service