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:
parent
1d6add41a2
commit
c3456027d2
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -6,3 +6,4 @@ coverage
|
|||
httplib2
|
||||
pylint
|
||||
stestr
|
||||
tabulate
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -0,0 +1,131 @@
|
|||
"""
|
||||
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()
|
||||
LOG.info("get_one %s" % release)
|
||||
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))
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue