Gustavo Herzmann 6f092dbbd2 Fix software proxy for upload-dir requests
This commit updates the USM proxy to prevent it from attempting to
upload the same files multiple times, resulting in an error. Each
release from the software API response is now correctly matched with
its corresponding file from the request, ensuring each file is
uploaded to its respective major release subdirectory only once.

Additionally, it fixes the error parser to avoid formatting software
API responses, as they do not follow the same format as cgts client
requests. This resolves the incorrect output message displayed when
attempting to delete a nonexistent release.

It also fixes the USM release URL by removing an extra '/', fixing
the issue where the proxy wouldn't work and the webob files were
not closed (flling up the /scratch storage).

Test Plan:
1. PASS - Create a directory containing multiple patches and run the
'software --os-region-name SystemController upload-dir <dir>' command.
Wait for the operation to complete successfully, then verify that the
patches were uploaded to dc-vault;
2. PASS - Upload a single patch using 'software --os-region-name
SystemController upload --local <patch>' and verify that it is
successfully uploaded to dc-vault;
3. PASS - Upload a single patch using 'software --os-region-name
SystemController upload <patch>' and verify that it is successfully
uploaded to dc-vault;
4. PASS - Attempt to delete a nonexistent release and verify that the
CLI output is: 'Error: Release <release> can not be found';
5. PASS - Upload an iso using 'software --os-region-name
SystemController upload --local <iso> <sig>' and verify that both are
successfully uploaded to dc-vault;
6. PASS - Verify that the webob file objects are properly closed
after the upload request is complete;
7. PASS - Verify that in case of an error on the software API side,
the message returned by the proxy to the CLI is in a valid format.

Closes-Bug: 2080549
Closes-Bug: 2081320
Closes-Bug: 2081321

Change-Id: I33e533854fa8f126690dda613a3b20845d490ee3
Signed-off-by: Gustavo Herzmann <gustavo.herzmann@windriver.com>
2024-09-20 12:58:48 -03:00

130 lines
5.0 KiB
Python

# Copyright (c) 2021, 2024 Wind River Systems, Inc
#
# 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.
import json
from xml import etree as et
from oslo_log import log
import webob
from dcorch.api.proxy.common.service import Middleware
LOG = log.getLogger(__name__)
# As per webob.exc code:
# https://github.com/Pylons/webob/blob/master/src/webob/exc.py
# The explanation field is added to the HTTP exception as following:
# ${explanation}<br /><br />
WEBOB_EXPL_SEP = "<br /><br />"
SOFTWARE_RELEASE_PATH = "/v1/release"
class ParseError(Middleware):
"""WSGI middleware to replace the plain text message body of an
error response with one formatted so the client can parse it.
Based on pecan.middleware.errordocument
"""
def __init__(self, app, conf):
self.app = app
def __call__(self, environ, start_response):
# Request for this state, modified by replace_start_response()
# and used when an error is being reported.
state = {}
def replacement_start_response(status, headers, exc_info=None):
"""Overrides the default response to make errors parsable."""
try:
status_code = int(status.split(" ")[0])
state["status_code"] = status_code
except (ValueError, TypeError): # pragma: nocover
raise Exception(
("ErrorDocumentMiddleware received an invalid status %s" % status)
)
else:
if (state["status_code"] // 100) not in (2, 3):
# Remove some headers so we can replace them later
# when we have the full error message and can
# compute the length.
headers = [
(h, v)
for (h, v) in headers
if h not in ("Content-Length", "Content-Type")
]
# Save the headers in case we need to modify them.
state["headers"] = headers
return start_response(status, headers, exc_info)
app_iter = self.app(environ, replacement_start_response)
req = webob.Request(environ)
# NOTE: The SOFTWARE_RELEASE_PATH is excluded because the software client
# does not expect the same error_message format like cgts client does
if (state["status_code"] // 100) not in (
2,
3,
) and SOFTWARE_RELEASE_PATH not in req.path:
if (
req.accept.best_match(["application/json", "application/xml"])
== "application/xml"
):
try:
# simple check xml is valid
body = [
et.ElementTree.tostring(
et.ElementTree.fromstring(
"<error_message>"
+ "\n".join(app_iter)
+ "</error_message>"
)
)
]
except et.ElementTree.ParseError as err:
LOG.error("Error parsing HTTP response: %s" % err)
body = [
"<error_message>%s" % state["status_code"] + "</error_message>"
]
state["headers"].append(("Content-Type", "application/xml"))
else:
app_iter = [i.decode("utf-8") for i in app_iter]
# Parse explanation field from webob.exc and add it as
# 'faulstring' to be processed by cgts-client
fault = None
app_data = "\n".join(app_iter)
for data in app_data.split("\n"):
if WEBOB_EXPL_SEP in str(data):
# Remove separator, trailing and leading white spaces
fault = str(data).replace(WEBOB_EXPL_SEP, "").strip()
break
if fault is None:
body = [json.dumps({"error_message": app_data})]
else:
body = [
json.dumps(
{"error_message": json.dumps({"faultstring": fault})}
)
]
body = [item.encode("utf-8") for item in body]
state["headers"].append(("Content-Type", "application/json"))
state["headers"].append(("Content-Length", str(len(body[0]))))
else:
body = app_iter
return body