diff --git a/software-client/software_client/software_client.py b/software-client/software_client/software_client.py index e3d8c5df..a0d49bcd 100644 --- a/software-client/software_client/software_client.py +++ b/software-client/software_client/software_client.py @@ -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 @@ -321,7 +321,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) @@ -345,7 +345,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) @@ -369,7 +369,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) @@ -434,7 +434,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, @@ -466,7 +466,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) @@ -498,7 +498,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) @@ -527,7 +527,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) @@ -548,7 +548,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) @@ -571,7 +571,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: @@ -587,7 +587,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) @@ -662,7 +662,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) @@ -676,7 +676,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) @@ -692,7 +692,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 @@ -788,7 +788,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" @@ -821,7 +821,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) @@ -837,7 +837,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) @@ -875,7 +875,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, @@ -896,7 +896,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 @@ -922,9 +922,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) @@ -946,7 +946,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) @@ -968,7 +968,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) @@ -983,7 +983,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) @@ -1025,7 +1025,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" @@ -1060,7 +1060,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) @@ -1078,7 +1078,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) @@ -1095,7 +1095,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 = {} @@ -1111,7 +1111,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) diff --git a/software/pylint.rc b/software/pylint.rc index 347f18ed..e6516404 100644 --- a/software/pylint.rc +++ b/software/pylint.rc @@ -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 diff --git a/software/requirements.txt b/software/requirements.txt index 33386a6c..beede63e 100644 --- a/software/requirements.txt +++ b/software/requirements.txt @@ -14,3 +14,4 @@ PyGObject requests_toolbelt sh WebOb +WSME>=0.5b2 diff --git a/software/software/api/controllers/root.py b/software/software/api/controllers/root.py index 83d201a1..075b02ca 100644 --- a/software/software/api/controllers/root.py +++ b/software/software/api/controllers/root.py @@ -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() diff --git a/software/software/api/controllers/v1/__init__.py b/software/software/api/controllers/v1/__init__.py new file mode 100644 index 00000000..55adcd1d --- /dev/null +++ b/software/software/api/controllers/v1/__init__.py @@ -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) diff --git a/software/software/api/controllers/v1/base.py b/software/software/api/controllers/v1/base.py new file mode 100644 index 00000000..86231654 --- /dev/null +++ b/software/software/api/controllers/v1/base.py @@ -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) diff --git a/software/software/api/controllers/v1/link.py b/software/software/api/controllers/v1/link.py new file mode 100644 index 00000000..a08fb83e --- /dev/null +++ b/software/software/api/controllers/v1/link.py @@ -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) diff --git a/software/software/api/controllers/v1/software.py b/software/software/api/controllers/v1/software.py new file mode 100644 index 00000000..335d8047 --- /dev/null +++ b/software/software/api/controllers/v1/software.py @@ -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)