Merge "Add version to software-api and software client"

This commit is contained in:
Zuul 2024-02-08 18:21:16 +00:00 committed by Gerrit Code Review
commit 6c96e5d766
8 changed files with 515 additions and 266 deletions

View File

@ -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 SPDX-License-Identifier: Apache-2.0
@ -321,7 +321,7 @@ def software_command_not_implemented_yet(args):
def release_is_available_req(args): def release_is_available_req(args):
releases = "/".join(args.release) 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 = {} headers = {}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)
@ -345,7 +345,7 @@ def release_is_available_req(args):
def release_is_deployed_req(args): def release_is_deployed_req(args):
releases = "/".join(args.release) 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 = {} headers = {}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)
@ -369,7 +369,7 @@ def release_is_deployed_req(args):
def release_is_committed_req(args): def release_is_committed_req(args):
releases = "/".join(args.release) 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 = {} headers = {}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)
@ -434,7 +434,7 @@ def release_upload_req(args):
encoder = MultipartEncoder(fields=to_upload_files) encoder = MultipartEncoder(fields=to_upload_files)
headers = {'Content-Type': encoder.content_type} 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) append_auth_token_if_required(headers)
req = requests.post(url, req = requests.post(url,
data=to_upload_filenames if is_local else encoder, data=to_upload_filenames if is_local else encoder,
@ -466,7 +466,7 @@ def release_delete_req(args):
# Ignore interrupts during this function # Ignore interrupts during this function
signal.signal(signal.SIGINT, signal.SIG_IGN) 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 = {} headers = {}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)
@ -498,7 +498,7 @@ def commit_patch_req(args):
elif args.all: elif args.all:
# Get a list of all patches # Get a list of all patches
extra_opts = "&release=%s" % relopt 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) req = requests.get(url, headers=headers)
@ -527,7 +527,7 @@ def commit_patch_req(args):
patches = "/".join(args.patch) patches = "/".join(args.patch)
# First, get a list of dependencies and ask for confirmation # 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) req = requests.get(url, headers=headers)
@ -548,7 +548,7 @@ def commit_patch_req(args):
return 1 return 1
# Run dry-run # 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) req = requests.post(url, headers=headers)
print_software_op_result(req) print_software_op_result(req)
@ -571,7 +571,7 @@ def commit_patch_req(args):
print("Aborting...") print("Aborting...")
return 1 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) req = requests.post(url, headers=headers)
if args.debug: if args.debug:
@ -587,7 +587,7 @@ def release_list_req(args):
extra_opts = "" extra_opts = ""
if args.release: if args.release:
extra_opts = "&release=%s" % 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 = {} headers = {}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)
req = requests.get(url, headers=headers) req = requests.get(url, headers=headers)
@ -662,7 +662,7 @@ def print_software_deploy_host_list_result(req):
def deploy_host_list_req(args): 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) req = requests.get(url)
if args.debug: if args.debug:
print_result_debug(req) print_result_debug(req)
@ -676,7 +676,7 @@ def release_show_req(args):
# arg.release is a list # arg.release is a list
releases = "/".join(args.release) releases = "/".join(args.release)
url = "http://%s/software/show/%s" % (api_addr, releases) url = "http://%s/v1/software/show/%s" % (api_addr, releases)
headers = {} headers = {}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)
@ -692,7 +692,7 @@ def release_show_req(args):
def wait_for_install_complete(agent_ip): 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 rc = 0
max_retries = 4 max_retries = 4
@ -788,7 +788,7 @@ def host_install(args):
agent_ip = args.agent agent_ip = args.agent
# Issue deploy_host request and poll for results # 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: if args.force:
url += "/force" url += "/force"
@ -821,7 +821,7 @@ def host_install(args):
def drop_host(args): def drop_host(args):
host_ip = args.host 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) req = requests.post(url)
@ -837,7 +837,7 @@ def install_local(args): # pylint: disable=unused-argument
# Ignore interrupts during this function # Ignore interrupts during this function
signal.signal(signal.SIGINT, signal.SIG_IGN) 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 = {} headers = {}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)
@ -875,7 +875,7 @@ def release_upload_dir_req(args):
to_upload_files[software_file] = (software_file, open(software_file, 'rb')) to_upload_files[software_file] = (software_file, open(software_file, 'rb'))
encoder = MultipartEncoder(fields=to_upload_files) 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} headers = {'Content-Type': encoder.content_type}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)
req = requests.post(url, req = requests.post(url,
@ -896,7 +896,7 @@ def deploy_precheck_req(args):
region_name = args.region_name region_name = args.region_name
# Issue deploy_precheck request # 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: if args.force:
url += "/force" url += "/force"
url += "?region_name=%s" % region_name url += "?region_name=%s" % region_name
@ -922,9 +922,9 @@ def deploy_start_req(args):
# Issue deploy_start request # Issue deploy_start request
if args.force: 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: else:
url = "http://%s/software/deploy_start/%s" % (api_addr, deployment) url = "http://%s/v1/software/deploy_start/%s" % (api_addr, deployment)
headers = {} headers = {}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)
@ -946,7 +946,7 @@ def deploy_activate_req(args):
signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGINT, signal.SIG_IGN)
# Issue deploy_start request # 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 = {} headers = {}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)
@ -968,7 +968,7 @@ def deploy_complete_req(args):
signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGINT, signal.SIG_IGN)
# Issue deploy_complete request # 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 = {} headers = {}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)
@ -983,7 +983,7 @@ def deploy_complete_req(args):
def deploy_show_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 = {} headers = {}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)
req = requests.get(url, headers=headers) req = requests.get(url, headers=headers)
@ -1025,7 +1025,7 @@ def deploy_host_req(args):
agent_ip = args.agent agent_ip = args.agent
# Issue deploy_host request and poll for results # 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: if args.force:
url += "/force" url += "/force"
@ -1060,7 +1060,7 @@ def patch_init_release(args):
release = args.release 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) req = requests.post(url)
@ -1078,7 +1078,7 @@ def patch_del_release(args):
release = args.release 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) req = requests.post(url)
@ -1095,7 +1095,7 @@ def patch_report_app_dependencies_req(args): # pylint: disable=unused-argument
extra_opts_str = '?%s' % '&'.join(extra_opts) extra_opts_str = '?%s' % '&'.join(extra_opts)
patches = "/".join(args) 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) % (api_addr, patches, extra_opts_str)
headers = {} headers = {}
@ -1111,7 +1111,7 @@ def patch_report_app_dependencies_req(args): # pylint: disable=unused-argument
def patch_query_app_dependencies_req(): 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 = {} headers = {}
append_auth_token_if_required(headers) append_auth_token_if_required(headers)

View File

@ -466,12 +466,14 @@ confidence=HIGH,
# W1505 deprecated-method # W1505 deprecated-method
# W1514 unspecified-encoding # W1514 unspecified-encoding
# W3101 missing-timeout # 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, C0301,C0302,C0325,C0411,C0413,C0415,
R0205,R0402,R0801,R0902,R0903,R0904,R0911, R0205,R0402,R0801,R0902,R0903,R0904,R0911,
R0912,R0913,R0914,R0915,R1702,R1705,R1714, R0912,R0913,R0914,R0915,R1702,R1705,R1714,
R1715,R1722,R1724,R1725,R1732,R1735, 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 # 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 # either give multiple identifier separated by comma (,) or put this option

View File

@ -14,3 +14,4 @@ PyGObject
requests_toolbelt requests_toolbelt
sh sh
WebOb WebOb
WSME>=0.5b2

View File

@ -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 SPDX-License-Identifier: Apache-2.0
""" """
import cgi
import json
import os
from oslo_log import log from oslo_log import log
from pecan import expose import pecan
from pecan import request from pecan import rest
import shutil
from software.exceptions import SoftwareError from software.api.controllers import v1
from software.software_controller import sc from software.api.controllers.v1 import base
import software.utils as utils from software.api.controllers.v1 import link
import software.constants as constants
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
class SoftwareAPIController(object): class Version(base.APIBase):
"""An API version representation."""
@expose('json') id = wtypes.text
def commit_patch(self, *args): "The ID of the version, also acts as the release number"
try:
result = sc.patch_commit(list(args))
except SoftwareError as e:
return dict(error=str(e))
sc.software_sync() links = [link.Link]
"A Link that point to a specific version of the API"
return result @classmethod
def convert(self, id):
@expose('json') version = Version()
def commit_dry_run(self, *args): version.id = id
try: version.links = [link.Link.make_link('self', pecan.request.host_url,
result = sc.patch_commit(list(args), dry_run=True) id, '', bookmark=True)]
except SoftwareError as e: return version
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)
class RootController: class Root(base.APIBase):
"""pecan REST API root"""
@expose() name = wtypes.text
@expose('json') "The name of the API"
def index(self):
"""index for the root"""
return "Unified Software Management API, Available versions: /v1"
software = SoftwareAPIController() description = wtypes.text
v1 = SoftwareAPIController() "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()

View File

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

View File

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

View File

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

View File

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