Add version to software-api and software client
The software-api is versioned in order to allow for future upgrades. This implementation add 'v1' version to all software REST APIs and modify software client to use the updated 'v1' APIs. Example: - POST /v1/software/upload - GET /v1/software/query - GET /v1/software/show/<release> - GET /v1/software/commit_patch/<release> ... Story: 2010676 Task: 49478 Test Plan: PASS: Verify REST interfaces of software APIs PASS: Verify software client with new APIs PASS: Verify client with sudo, without keystone auth prior bootstrap PASS: Verify client without sudo, with keystone auth prior bootstrap Change-Id: I10250676fbbcf7501913f21dedea769b581128af Signed-off-by: Joseph Vazhappilly <joseph.vazhappillypaily@windriver.com>
This commit is contained in:
parent
75212da467
commit
617a379659
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Copyright (c) 2023 Wind River Systems, Inc.
|
||||
Copyright (c) 2023-2024 Wind River Systems, Inc.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
|
@ -313,7 +313,7 @@ def software_command_not_implemented_yet(args):
|
|||
def release_is_available_req(args):
|
||||
|
||||
releases = "/".join(args.release)
|
||||
url = "http://%s/software/is_available/%s" % (api_addr, releases)
|
||||
url = "http://%s/v1/software/is_available/%s" % (api_addr, releases)
|
||||
|
||||
headers = {}
|
||||
append_auth_token_if_required(headers)
|
||||
|
@ -337,7 +337,7 @@ def release_is_available_req(args):
|
|||
def release_is_deployed_req(args):
|
||||
|
||||
releases = "/".join(args.release)
|
||||
url = "http://%s/software/is_deployed/%s" % (api_addr, releases)
|
||||
url = "http://%s/v1/software/is_deployed/%s" % (api_addr, releases)
|
||||
|
||||
headers = {}
|
||||
append_auth_token_if_required(headers)
|
||||
|
@ -361,7 +361,7 @@ def release_is_deployed_req(args):
|
|||
def release_is_committed_req(args):
|
||||
|
||||
releases = "/".join(args.release)
|
||||
url = "http://%s/software/is_committed/%s" % (api_addr, releases)
|
||||
url = "http://%s/v1/software/is_committed/%s" % (api_addr, releases)
|
||||
|
||||
headers = {}
|
||||
append_auth_token_if_required(headers)
|
||||
|
@ -426,7 +426,7 @@ def release_upload_req(args):
|
|||
encoder = MultipartEncoder(fields=to_upload_files)
|
||||
headers = {'Content-Type': encoder.content_type}
|
||||
|
||||
url = "http://%s/software/upload" % api_addr
|
||||
url = "http://%s/v1/software/upload" % api_addr
|
||||
append_auth_token_if_required(headers)
|
||||
req = requests.post(url,
|
||||
data=to_upload_filenames if is_local else encoder,
|
||||
|
@ -458,7 +458,7 @@ def release_delete_req(args):
|
|||
# Ignore interrupts during this function
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
url = "http://%s/software/delete/%s" % (api_addr, releases)
|
||||
url = "http://%s/v1/software/delete/%s" % (api_addr, releases)
|
||||
|
||||
headers = {}
|
||||
append_auth_token_if_required(headers)
|
||||
|
@ -490,7 +490,7 @@ def commit_patch_req(args):
|
|||
elif args.all:
|
||||
# Get a list of all patches
|
||||
extra_opts = "&release=%s" % relopt
|
||||
url = "http://%s/software/query?show=patch%s" % (api_addr, extra_opts)
|
||||
url = "http://%s/v1/software/query?show=patch%s" % (api_addr, extra_opts)
|
||||
|
||||
req = requests.get(url, headers=headers)
|
||||
|
||||
|
@ -519,7 +519,7 @@ def commit_patch_req(args):
|
|||
patches = "/".join(args.patch)
|
||||
|
||||
# First, get a list of dependencies and ask for confirmation
|
||||
url = "http://%s/software/query_dependencies/%s?recursive=yes" % (api_addr, patches)
|
||||
url = "http://%s/v1/software/query_dependencies/%s?recursive=yes" % (api_addr, patches)
|
||||
|
||||
req = requests.get(url, headers=headers)
|
||||
|
||||
|
@ -540,7 +540,7 @@ def commit_patch_req(args):
|
|||
return 1
|
||||
|
||||
# Run dry-run
|
||||
url = "http://%s/software/commit_dry_run/%s" % (api_addr, patches)
|
||||
url = "http://%s/v1/software/commit_dry_run/%s" % (api_addr, patches)
|
||||
|
||||
req = requests.post(url, headers=headers)
|
||||
print_software_op_result(req)
|
||||
|
@ -563,7 +563,7 @@ def commit_patch_req(args):
|
|||
print("Aborting...")
|
||||
return 1
|
||||
|
||||
url = "http://%s/software/commit_patch/%s" % (api_addr, patches)
|
||||
url = "http://%s/v1/software/commit_patch/%s" % (api_addr, patches)
|
||||
req = requests.post(url, headers=headers)
|
||||
|
||||
if args.debug:
|
||||
|
@ -579,7 +579,7 @@ def release_list_req(args):
|
|||
extra_opts = ""
|
||||
if args.release:
|
||||
extra_opts = "&release=%s" % args.release
|
||||
url = "http://%s/software/query?show=%s%s" % (api_addr, state, extra_opts)
|
||||
url = "http://%s/v1/software/query?show=%s%s" % (api_addr, state, extra_opts)
|
||||
headers = {}
|
||||
append_auth_token_if_required(headers)
|
||||
req = requests.get(url, headers=headers)
|
||||
|
@ -653,7 +653,7 @@ def print_software_deploy_host_list_result(req):
|
|||
|
||||
|
||||
def deploy_host_list_req(args):
|
||||
url = "http://%s/software/host_list" % api_addr
|
||||
url = "http://%s/v1/software/host_list" % api_addr
|
||||
req = requests.get(url)
|
||||
if args.debug:
|
||||
print_result_debug(req)
|
||||
|
@ -667,7 +667,7 @@ def release_show_req(args):
|
|||
# arg.release is a list
|
||||
releases = "/".join(args.release)
|
||||
|
||||
url = "http://%s/software/show/%s" % (api_addr, releases)
|
||||
url = "http://%s/v1/software/show/%s" % (api_addr, releases)
|
||||
|
||||
headers = {}
|
||||
append_auth_token_if_required(headers)
|
||||
|
@ -683,7 +683,7 @@ def release_show_req(args):
|
|||
|
||||
|
||||
def wait_for_install_complete(agent_ip):
|
||||
url = "http://%s/software/host_list" % api_addr
|
||||
url = "http://%s/v1/software/host_list" % api_addr
|
||||
rc = 0
|
||||
|
||||
max_retries = 4
|
||||
|
@ -779,7 +779,7 @@ def host_install(args):
|
|||
agent_ip = args.agent
|
||||
|
||||
# Issue deploy_host request and poll for results
|
||||
url = "http://%s/software/deploy_host/%s" % (api_addr, agent_ip)
|
||||
url = "http://%s/v1/software/deploy_host/%s" % (api_addr, agent_ip)
|
||||
|
||||
if args.force:
|
||||
url += "/force"
|
||||
|
@ -812,7 +812,7 @@ def host_install(args):
|
|||
def drop_host(args):
|
||||
host_ip = args.host
|
||||
|
||||
url = "http://%s/software/drop_host/%s" % (api_addr, host_ip)
|
||||
url = "http://%s/v1/software/drop_host/%s" % (api_addr, host_ip)
|
||||
|
||||
req = requests.post(url)
|
||||
|
||||
|
@ -828,7 +828,7 @@ def install_local(args): # pylint: disable=unused-argument
|
|||
# Ignore interrupts during this function
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
url = "http://%s/software/install_local" % (api_addr)
|
||||
url = "http://%s/v1/software/install_local" % (api_addr)
|
||||
|
||||
headers = {}
|
||||
append_auth_token_if_required(headers)
|
||||
|
@ -866,7 +866,7 @@ def release_upload_dir_req(args):
|
|||
to_upload_files[software_file] = (software_file, open(software_file, 'rb'))
|
||||
|
||||
encoder = MultipartEncoder(fields=to_upload_files)
|
||||
url = "http://%s/software/upload" % api_addr
|
||||
url = "http://%s/v1/software/upload" % api_addr
|
||||
headers = {'Content-Type': encoder.content_type}
|
||||
append_auth_token_if_required(headers)
|
||||
req = requests.post(url,
|
||||
|
@ -887,7 +887,7 @@ def deploy_precheck_req(args):
|
|||
region_name = args.region_name
|
||||
|
||||
# Issue deploy_precheck request
|
||||
url = "http://%s/software/deploy_precheck/%s" % (api_addr, deployment)
|
||||
url = "http://%s/v1/software/deploy_precheck/%s" % (api_addr, deployment)
|
||||
if args.force:
|
||||
url += "/force"
|
||||
url += "?region_name=%s" % region_name
|
||||
|
@ -913,9 +913,9 @@ def deploy_start_req(args):
|
|||
|
||||
# Issue deploy_start request
|
||||
if args.force:
|
||||
url = "http://%s/software/deploy_start/%s/force" % (api_addr, deployment)
|
||||
url = "http://%s/v1/software/deploy_start/%s/force" % (api_addr, deployment)
|
||||
else:
|
||||
url = "http://%s/software/deploy_start/%s" % (api_addr, deployment)
|
||||
url = "http://%s/v1/software/deploy_start/%s" % (api_addr, deployment)
|
||||
|
||||
headers = {}
|
||||
append_auth_token_if_required(headers)
|
||||
|
@ -937,7 +937,7 @@ def deploy_activate_req(args):
|
|||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
# Issue deploy_start request
|
||||
url = "http://%s/software/deploy_activate/%s" % (api_addr, deployment)
|
||||
url = "http://%s/v1/software/deploy_activate/%s" % (api_addr, deployment)
|
||||
|
||||
headers = {}
|
||||
append_auth_token_if_required(headers)
|
||||
|
@ -959,7 +959,7 @@ def deploy_complete_req(args):
|
|||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
# Issue deploy_complete request
|
||||
url = "http://%s/software/deploy_complete/%s" % (api_addr, deployment)
|
||||
url = "http://%s/v1/software/deploy_complete/%s" % (api_addr, deployment)
|
||||
|
||||
headers = {}
|
||||
append_auth_token_if_required(headers)
|
||||
|
@ -974,7 +974,7 @@ def deploy_complete_req(args):
|
|||
|
||||
|
||||
def deploy_show_req(args):
|
||||
url = "http://%s/software/deploy_show" % api_addr
|
||||
url = "http://%s/v1/software/deploy_show" % api_addr
|
||||
headers = {}
|
||||
append_auth_token_if_required(headers)
|
||||
req = requests.get(url, headers=headers)
|
||||
|
@ -1016,7 +1016,7 @@ def deploy_host_req(args):
|
|||
agent_ip = args.agent
|
||||
|
||||
# Issue deploy_host request and poll for results
|
||||
url = "http://%s/software/deploy_host/%s" % (api_addr, agent_ip)
|
||||
url = "http://%s/v1/software/deploy_host/%s" % (api_addr, agent_ip)
|
||||
|
||||
if args.force:
|
||||
url += "/force"
|
||||
|
@ -1051,7 +1051,7 @@ def patch_init_release(args):
|
|||
|
||||
release = args.release
|
||||
|
||||
url = "http://%s/software/init_release/%s" % (api_addr, release)
|
||||
url = "http://%s/v1/software/init_release/%s" % (api_addr, release)
|
||||
|
||||
req = requests.post(url)
|
||||
|
||||
|
@ -1069,7 +1069,7 @@ def patch_del_release(args):
|
|||
|
||||
release = args.release
|
||||
|
||||
url = "http://%s/software/del_release/%s" % (api_addr, release)
|
||||
url = "http://%s/v1/software/del_release/%s" % (api_addr, release)
|
||||
|
||||
req = requests.post(url)
|
||||
|
||||
|
@ -1086,7 +1086,7 @@ def patch_report_app_dependencies_req(args): # pylint: disable=unused-argument
|
|||
extra_opts_str = '?%s' % '&'.join(extra_opts)
|
||||
|
||||
patches = "/".join(args)
|
||||
url = "http://%s/software/report_app_dependencies/%s%s" \
|
||||
url = "http://%s/v1/software/report_app_dependencies/%s%s" \
|
||||
% (api_addr, patches, extra_opts_str)
|
||||
|
||||
headers = {}
|
||||
|
@ -1102,7 +1102,7 @@ def patch_report_app_dependencies_req(args): # pylint: disable=unused-argument
|
|||
|
||||
|
||||
def patch_query_app_dependencies_req():
|
||||
url = "http://%s/software/query_app_dependencies" % api_addr
|
||||
url = "http://%s/v1/software/query_app_dependencies" % api_addr
|
||||
|
||||
headers = {}
|
||||
append_auth_token_if_required(headers)
|
||||
|
|
|
@ -466,12 +466,14 @@ confidence=HIGH,
|
|||
# W1505 deprecated-method
|
||||
# W1514 unspecified-encoding
|
||||
# W3101 missing-timeout
|
||||
disable= C0103,C0114,C0115,C0116,C0201,C0206,C0209,C2801,
|
||||
disable= C0103,C0114,C0115,C0116,C0201,C0202,C0206,C0209,C2801,
|
||||
C0301,C0302,C0325,C0411,C0413,C0415,
|
||||
R0205,R0402,R0801,R0902,R0903,R0904,R0911,
|
||||
R0912,R0913,R0914,R0915,R1702,R1705,R1714,
|
||||
R1715,R1722,R1724,R1725,R1732,R1735,
|
||||
W0107,W0602,W0603,W0703,W0707,W0719,W1201,W1514,W3101
|
||||
W0107,W0231,W0602,W0603,W0621,W0622,
|
||||
W0703,W0707,W0719,W1201,W1514,W3101,
|
||||
E0605
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
|
|
|
@ -14,3 +14,4 @@ PyGObject
|
|||
requests_toolbelt
|
||||
sh
|
||||
WebOb
|
||||
WSME>=0.5b2
|
||||
|
|
|
@ -1,253 +1,75 @@
|
|||
"""
|
||||
Copyright (c) 2023 Wind River Systems, Inc.
|
||||
Copyright (c) 2023-2024 Wind River Systems, Inc.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
import cgi
|
||||
import json
|
||||
import os
|
||||
from oslo_log import log
|
||||
from pecan import expose
|
||||
from pecan import request
|
||||
import shutil
|
||||
import pecan
|
||||
from pecan import rest
|
||||
|
||||
from software.exceptions import SoftwareError
|
||||
from software.software_controller import sc
|
||||
import software.utils as utils
|
||||
import software.constants as constants
|
||||
from software.api.controllers import v1
|
||||
from software.api.controllers.v1 import base
|
||||
from software.api.controllers.v1 import link
|
||||
|
||||
from wsme import types as wtypes
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class SoftwareAPIController(object):
|
||||
class Version(base.APIBase):
|
||||
"""An API version representation."""
|
||||
|
||||
@expose('json')
|
||||
def commit_patch(self, *args):
|
||||
try:
|
||||
result = sc.patch_commit(list(args))
|
||||
except SoftwareError as e:
|
||||
return dict(error=str(e))
|
||||
id = wtypes.text
|
||||
"The ID of the version, also acts as the release number"
|
||||
|
||||
sc.software_sync()
|
||||
links = [link.Link]
|
||||
"A Link that point to a specific version of the API"
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
def commit_dry_run(self, *args):
|
||||
try:
|
||||
result = sc.patch_commit(list(args), dry_run=True)
|
||||
except SoftwareError as e:
|
||||
return dict(error=str(e))
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def delete(self, *args):
|
||||
try:
|
||||
result = sc.software_release_delete_api(list(args))
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
sc.software_sync()
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def deploy_activate(self, *args):
|
||||
if sc.any_patch_host_installing():
|
||||
return dict(error="Rejected: One or more nodes are installing a release.")
|
||||
|
||||
try:
|
||||
result = sc.software_deploy_activate_api(list(args)[0])
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
sc.software_sync()
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def deploy_complete(self, *args):
|
||||
if sc.any_patch_host_installing():
|
||||
return dict(error="Rejected: One or more nodes are installing a release.")
|
||||
|
||||
try:
|
||||
result = sc.software_deploy_complete_api(list(args)[0])
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
sc.software_sync()
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def deploy_host(self, *args):
|
||||
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
|
||||
|
||||
try:
|
||||
result = sc.software_deploy_host_api(list(args)[0], force, async_req=True)
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def deploy_precheck(self, *args, **kwargs):
|
||||
force = False
|
||||
if 'force' in list(args):
|
||||
force = True
|
||||
|
||||
try:
|
||||
result = sc.software_deploy_precheck_api(list(args)[0], force, **kwargs)
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def deploy_start(self, *args, **kwargs):
|
||||
# if --force is provided
|
||||
force = 'force' in list(args)
|
||||
|
||||
if sc.any_patch_host_installing():
|
||||
return dict(error="Rejected: One or more nodes are installing releases.")
|
||||
|
||||
try:
|
||||
result = sc.software_deploy_start_api(list(args)[0], force, **kwargs)
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
sc.send_latest_feed_commit_to_agent()
|
||||
sc.software_sync()
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def deploy_show(self):
|
||||
try:
|
||||
result = sc.software_deploy_show_api()
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def install_local(self):
|
||||
try:
|
||||
result = sc.software_install_local_api()
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
def is_available(self, *args):
|
||||
return sc.is_available(list(args))
|
||||
|
||||
@expose('json')
|
||||
def is_committed(self, *args):
|
||||
return sc.is_committed(list(args))
|
||||
|
||||
@expose('json')
|
||||
def is_deployed(self, *args):
|
||||
return sc.is_deployed(list(args))
|
||||
|
||||
@expose('json')
|
||||
@expose('show.xml', content_type='application/xml')
|
||||
def show(self, *args):
|
||||
try:
|
||||
result = sc.software_release_query_specific_cached(list(args))
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def upload(self):
|
||||
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)
|
||||
|
||||
except Exception as e:
|
||||
return dict(error=str(e))
|
||||
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):
|
||||
try:
|
||||
sd = sc.software_release_query_cached(**kwargs)
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
return dict(sd=sd)
|
||||
|
||||
@expose('json')
|
||||
@expose('query_hosts.xml', content_type='application/xml')
|
||||
def host_list(self, *args): # pylint: disable=unused-argument
|
||||
try:
|
||||
query_hosts = sc.deploy_host_list()
|
||||
except Exception as e:
|
||||
return dict(error=str(e))
|
||||
return dict(data=query_hosts)
|
||||
@classmethod
|
||||
def convert(self, id):
|
||||
version = Version()
|
||||
version.id = id
|
||||
version.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||
id, '', bookmark=True)]
|
||||
return version
|
||||
|
||||
|
||||
class RootController:
|
||||
"""pecan REST API root"""
|
||||
class Root(base.APIBase):
|
||||
|
||||
@expose()
|
||||
@expose('json')
|
||||
def index(self):
|
||||
"""index for the root"""
|
||||
return "Unified Software Management API, Available versions: /v1"
|
||||
name = wtypes.text
|
||||
"The name of the API"
|
||||
|
||||
software = SoftwareAPIController()
|
||||
v1 = SoftwareAPIController()
|
||||
description = wtypes.text
|
||||
"Some information about this API"
|
||||
|
||||
versions = [Version]
|
||||
"Links to all the versions available in this API"
|
||||
|
||||
default_version = Version
|
||||
"A link to the default version of the API"
|
||||
|
||||
@classmethod
|
||||
def convert(self):
|
||||
root = Root()
|
||||
root.name = "StarlingX USM API"
|
||||
root.description = ("Unified Software Management API allows for a "
|
||||
"single REST API / CLI and single procedure for updating "
|
||||
"the StarlingX software on a Standalone Cloud or Distributed Cloud."
|
||||
)
|
||||
root.versions = [Version.convert('v1')]
|
||||
root.default_version = Version.convert('v1')
|
||||
return root
|
||||
|
||||
|
||||
class RootController(rest.RestController):
|
||||
|
||||
v1 = v1.Controller()
|
||||
|
||||
@wsme_pecan.wsexpose(Root)
|
||||
def get(self):
|
||||
# NOTE: The reason why convert() it's being called for every
|
||||
# request is because we need to get the host url from
|
||||
# the request object to make the links.
|
||||
return Root.convert()
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
#
|
||||
# Copyright (c) 2013-2024 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# All Rights Reserved.
|
||||
#
|
||||
|
||||
"""
|
||||
Version 1 of the USM API
|
||||
|
||||
Specification can be found in code repo.
|
||||
"""
|
||||
|
||||
import pecan
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
from pecan import rest
|
||||
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
|
||||
|
||||
|
||||
class MediaType(base.APIBase):
|
||||
"""A media type representation."""
|
||||
|
||||
base = wtypes.text
|
||||
type = wtypes.text
|
||||
|
||||
def __init__(self, base, type):
|
||||
self.base = base
|
||||
self.type = type
|
||||
|
||||
|
||||
class V1(base.APIBase):
|
||||
"""The representation of the version 1 of the API."""
|
||||
|
||||
id = wtypes.text
|
||||
"The ID of the version, also acts as the release number"
|
||||
|
||||
media_types = [MediaType]
|
||||
"An array of supported media types for this version"
|
||||
|
||||
links = [link.Link]
|
||||
"Links that point to a specific URL for this version and documentation"
|
||||
|
||||
software = [link.Link]
|
||||
"Links to the software resource"
|
||||
|
||||
@classmethod
|
||||
def convert(self):
|
||||
v1 = V1()
|
||||
v1.id = "v1"
|
||||
v1.links = [link.Link.make_link('self', pecan.request.host_url,
|
||||
'v1', '', bookmark=True),
|
||||
]
|
||||
v1.media_types = [MediaType('application/json',
|
||||
'application/vnd.openstack.software.v1+json')]
|
||||
|
||||
v1.software = [link.Link.make_link('self', pecan.request.host_url,
|
||||
'software', ''),
|
||||
link.Link.make_link('bookmark',
|
||||
pecan.request.host_url,
|
||||
'software', '',
|
||||
bookmark=True)
|
||||
]
|
||||
|
||||
return v1
|
||||
|
||||
|
||||
class Controller(rest.RestController):
|
||||
"""Version 1 API controller root."""
|
||||
|
||||
software = software.SoftwareAPIController()
|
||||
|
||||
@wsme_pecan.wsexpose(V1)
|
||||
def get(self):
|
||||
# NOTE: The reason why convert() it's being called for every
|
||||
# request is because we need to get the host url from
|
||||
# the request object to make the links.
|
||||
return V1.convert()
|
||||
|
||||
|
||||
__all__ = (Controller)
|
|
@ -0,0 +1,52 @@
|
|||
"""
|
||||
Copyright (c) 2013-2024 Wind River Systems, Inc.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import datetime
|
||||
|
||||
import wsme
|
||||
from wsme import types as wtypes
|
||||
|
||||
|
||||
class APIBase(wtypes.Base):
|
||||
|
||||
created_at = datetime.datetime
|
||||
"The time in UTC at which the object is created"
|
||||
|
||||
updated_at = datetime.datetime
|
||||
"The time in UTC at which the object is updated"
|
||||
|
||||
def as_dict(self):
|
||||
"""Render this object as a dict of its fields."""
|
||||
return dict((k, getattr(self, k))
|
||||
for k in self.fields # pylint: disable=no-member
|
||||
if hasattr(self, k) and
|
||||
getattr(self, k) != wsme.Unset)
|
||||
|
||||
def unset_fields_except(self, except_list=None):
|
||||
"""Unset fields so they don't appear in the message body.
|
||||
|
||||
:param except_list: A list of fields that won't be touched.
|
||||
|
||||
"""
|
||||
if except_list is None:
|
||||
except_list = []
|
||||
|
||||
for k in self.as_dict():
|
||||
if k not in except_list:
|
||||
setattr(self, k, wsme.Unset)
|
||||
|
||||
@classmethod
|
||||
def from_rpc_object(cls, m, fields=None):
|
||||
"""Convert a RPC object to an API object."""
|
||||
obj_dict = m.as_dict()
|
||||
# Unset non-required fields so they do not appear
|
||||
# in the message body
|
||||
obj_dict.update(dict((k, wsme.Unset)
|
||||
for k in obj_dict.keys()
|
||||
if fields and k not in fields))
|
||||
return cls(**obj_dict)
|
|
@ -0,0 +1,43 @@
|
|||
# Copyright 2013 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright (c) 2013-2024 Wind River Systems, Inc.
|
||||
#
|
||||
|
||||
from wsme import types as wtypes
|
||||
|
||||
from software.api.controllers.v1 import base
|
||||
|
||||
|
||||
class Link(base.APIBase):
|
||||
"""A link representation."""
|
||||
|
||||
href = wtypes.text
|
||||
"The url of a link."
|
||||
|
||||
rel = wtypes.text
|
||||
"The name of a link."
|
||||
|
||||
type = wtypes.text
|
||||
"Indicates the type of document/link."
|
||||
|
||||
@classmethod
|
||||
def make_link(cls, rel_name, url, resource, resource_args,
|
||||
bookmark=False, type=wtypes.Unset):
|
||||
template = '%s/%s' if bookmark else '%s/v1/%s'
|
||||
template += '%s' if resource_args.startswith('?') else '/%s'
|
||||
|
||||
return Link(href=(template) % (url, resource, resource_args),
|
||||
rel=rel_name, type=type)
|
|
@ -0,0 +1,241 @@
|
|||
"""
|
||||
Copyright (c) 2023-2024 Wind River Systems, Inc.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
import cgi
|
||||
import json
|
||||
import os
|
||||
from oslo_log import log
|
||||
from pecan import expose
|
||||
from pecan import request
|
||||
import shutil
|
||||
|
||||
from software.exceptions import SoftwareError
|
||||
from software.software_controller import sc
|
||||
import software.utils as utils
|
||||
import software.constants as constants
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class SoftwareAPIController(object):
|
||||
|
||||
@expose('json')
|
||||
def commit_patch(self, *args):
|
||||
try:
|
||||
result = sc.patch_commit(list(args))
|
||||
except SoftwareError as e:
|
||||
return dict(error=str(e))
|
||||
|
||||
sc.software_sync()
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
def commit_dry_run(self, *args):
|
||||
try:
|
||||
result = sc.patch_commit(list(args), dry_run=True)
|
||||
except SoftwareError as e:
|
||||
return dict(error=str(e))
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def delete(self, *args):
|
||||
try:
|
||||
result = sc.software_release_delete_api(list(args))
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
sc.software_sync()
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def deploy_activate(self, *args):
|
||||
if sc.any_patch_host_installing():
|
||||
return dict(error="Rejected: One or more nodes are installing a release.")
|
||||
|
||||
try:
|
||||
result = sc.software_deploy_activate_api(list(args)[0])
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
sc.software_sync()
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def deploy_complete(self, *args):
|
||||
if sc.any_patch_host_installing():
|
||||
return dict(error="Rejected: One or more nodes are installing a release.")
|
||||
|
||||
try:
|
||||
result = sc.software_deploy_complete_api(list(args)[0])
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
sc.software_sync()
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def deploy_host(self, *args):
|
||||
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
|
||||
|
||||
try:
|
||||
result = sc.software_deploy_host_api(list(args)[0], force, async_req=True)
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def deploy_precheck(self, *args, **kwargs):
|
||||
force = False
|
||||
if 'force' in list(args):
|
||||
force = True
|
||||
|
||||
try:
|
||||
result = sc.software_deploy_precheck_api(list(args)[0], force, **kwargs)
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def deploy_start(self, *args, **kwargs):
|
||||
# if --force is provided
|
||||
force = 'force' in list(args)
|
||||
|
||||
if sc.any_patch_host_installing():
|
||||
return dict(error="Rejected: One or more nodes are installing releases.")
|
||||
|
||||
try:
|
||||
result = sc.software_deploy_start_api(list(args)[0], force, **kwargs)
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
sc.send_latest_feed_commit_to_agent()
|
||||
sc.software_sync()
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def deploy_show(self):
|
||||
try:
|
||||
result = sc.software_deploy_show_api()
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def install_local(self):
|
||||
try:
|
||||
result = sc.software_install_local_api()
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
def is_available(self, *args):
|
||||
return sc.is_available(list(args))
|
||||
|
||||
@expose('json')
|
||||
def is_committed(self, *args):
|
||||
return sc.is_committed(list(args))
|
||||
|
||||
@expose('json')
|
||||
def is_deployed(self, *args):
|
||||
return sc.is_deployed(list(args))
|
||||
|
||||
@expose('json')
|
||||
@expose('show.xml', content_type='application/xml')
|
||||
def show(self, *args):
|
||||
try:
|
||||
result = sc.software_release_query_specific_cached(list(args))
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
return result
|
||||
|
||||
@expose('json')
|
||||
@expose('query.xml', content_type='application/xml')
|
||||
def upload(self):
|
||||
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)
|
||||
|
||||
except Exception as e:
|
||||
return dict(error=str(e))
|
||||
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):
|
||||
try:
|
||||
sd = sc.software_release_query_cached(**kwargs)
|
||||
except SoftwareError as e:
|
||||
return dict(error="Error: %s" % str(e))
|
||||
|
||||
return dict(sd=sd)
|
||||
|
||||
@expose('json')
|
||||
@expose('query_hosts.xml', content_type='application/xml')
|
||||
def host_list(self, *args): # pylint: disable=unused-argument
|
||||
try:
|
||||
query_hosts = sc.deploy_host_list()
|
||||
except Exception as e:
|
||||
return dict(error=str(e))
|
||||
return dict(data=query_hosts)
|
Loading…
Reference in New Issue