New USM REST API

Updated the USM REST Api as agreed
Also applied the standardized CLI output w/ tabulate for CLI commands.

Fixed a few things:
1. software upload column header change to "Release"
2. use region_name from usm.conf to replace default "RegionOne" as local
   region.
3. temporarily skip the ParsableErrorMiddleware.
4. do not recreate http exceptions based on the http status_code on
   the client side, use common display function to display the http
   error.
5. expose webob.exc.HTTPClientError to the client side.
6. updated --debug output to include CLI output at the end.

Test Cases:
    passed all CLI commands, verify the endpoints and request body.
    passed verify CLI requests compiled with defined REST Api.

Story: 2010676
Task: 49905

Change-Id: I5ab971b455aed527b7b1a21396b97334ba1e05ab
This commit is contained in:
Bin Qian 2024-04-19 20:14:15 +00:00
parent 1d6add41a2
commit e0415cd65b
25 changed files with 656 additions and 740 deletions

View File

@ -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:

View File

@ -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,

View File

@ -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)

View File

@ -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:

View File

@ -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'

View File

@ -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()

View File

@ -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')

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -6,3 +6,4 @@ coverage
httplib2
pylint
stestr
tabulate

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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()

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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
)

View File

@ -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)

View File

@ -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)

View File

@ -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