Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-redfish for openSUSE:Factory checked in at 2022-01-20 00:12:10 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-redfish (Old) and /work/SRC/openSUSE:Factory/.python-redfish.new.1892 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-redfish" Thu Jan 20 00:12:10 2022 rev:8 rq:947367 version:3.1.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-redfish/python-redfish.changes 2021-10-26 20:14:40.314040808 +0200 +++ /work/SRC/openSUSE:Factory/.python-redfish.new.1892/python-redfish.changes 2022-01-20 00:12:55.750607969 +0100 @@ -1,0 +2,7 @@ +Wed Jan 19 09:35:39 UTC 2022 - Dirk M??ller <[email protected]> + +- update to 3.1.0: + * Updated library to leverage 'requests' in favor of 'http.client' +- add collections-python310.patch + +------------------------------------------------------------------- Old: ---- redfish-3.0.3.tar.gz New: ---- collections-python310.patch redfish-3.1.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-redfish.spec ++++++ --- /var/tmp/diff_new_pack.2dq39E/_old 2022-01-20 00:12:56.214608344 +0100 +++ /var/tmp/diff_new_pack.2dq39E/_new 2022-01-20 00:12:56.222608350 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-redfish # -# Copyright (c) 2021 SUSE LLC +# Copyright (c) 2022 SUSE LLC # Copyright (c) 2020-2021, Martin Hauke <[email protected]> # # All modifications and additions to the file contributed by third parties @@ -19,19 +19,22 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-redfish -Version: 3.0.3 +Version: 3.1.0 Release: 0 Summary: Redfish Python Library License: BSD-3-Clause Group: Development/Languages/Python URL: https://github.com/DMTF/python-redfish-library Source: https://github.com/DMTF/python-redfish-library/archive/%{version}.tar.gz#/redfish-%{version}.tar.gz +# submitted as gh#DMTF/python-redfish-library#118 +Patch1: collections-python310.patch BuildRequires: %{python_module setuptools} BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: python-jsonpatch Requires: python-jsonpath-rw Requires: python-jsonpointer +Requires: python-requests BuildArch: noarch # SECTION test requirements BuildRequires: %{python_module jsonpatch} @@ -40,6 +43,7 @@ BuildRequires: %{python_module mock} BuildRequires: %{python_module pytest} BuildRequires: %{python_module requests-toolbelt} +BuildRequires: %{python_module requests} # /SECTION %python_subpackages @@ -49,7 +53,7 @@ the Engine of Application State) Redfish architecture. %prep -%setup -q -n %{name}-library-%{version} +%autosetup -p1 -n %{name}-library-%{version} %build %python_build ++++++ collections-python310.patch ++++++ --- python-redfish-library-3.1.0/src/redfish/ris/rmc.py 2022-01-10 14:55:53.000000000 +0100 +++ python-redfish-library-3.1.0/src/redfish/ris/rmc.py 2022-01-16 15:02:43.088058912 +0100 @@ -15,7 +15,8 @@ import copy import shutil import logging -from collections import OrderedDict, Mapping +from collections import OrderedDict +from collections.abc import Mapping import jsonpatch import jsonpath_rw ++++++ redfish-3.0.3.tar.gz -> redfish-3.1.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redfish-library-3.0.3/CHANGELOG.md new/python-redfish-library-3.1.0/CHANGELOG.md --- old/python-redfish-library-3.0.3/CHANGELOG.md 2021-10-15 22:51:22.000000000 +0200 +++ new/python-redfish-library-3.1.0/CHANGELOG.md 2022-01-10 14:55:53.000000000 +0100 @@ -1,5 +1,8 @@ # Change Log +## [3.1.0] - 2022-01-10 +- Updated library to leverage 'requests' in favor of 'http.client' + ## [3.0.3] - 2021-10-15 - Added support for performing multi-part HTTP POST requests diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redfish-library-3.0.3/requirements.txt new/python-redfish-library-3.1.0/requirements.txt --- old/python-redfish-library-3.0.3/requirements.txt 2021-10-15 22:51:22.000000000 +0200 +++ new/python-redfish-library-3.1.0/requirements.txt 2022-01-10 14:55:53.000000000 +0100 @@ -3,4 +3,5 @@ jsonpath_rw jsonpointer urllib3 +requests requests-toolbelt diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redfish-library-3.0.3/setup.py new/python-redfish-library-3.1.0/setup.py --- old/python-redfish-library-3.0.3/setup.py 2021-10-15 22:51:22.000000000 +0200 +++ new/python-redfish-library-3.1.0/setup.py 2022-01-10 14:55:53.000000000 +0100 @@ -12,7 +12,7 @@ long_description = f.read() setup(name='redfish', - version='3.0.3', + version='3.1.0', description='Redfish Python Library', long_description=long_description, long_description_content_type='text/x-rst', @@ -31,6 +31,7 @@ install_requires=[ 'jsonpath_rw', 'jsonpointer', + "requests", 'requests_toolbelt', ], extras_require={ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redfish-library-3.0.3/src/redfish/__init__.py new/python-redfish-library-3.1.0/src/redfish/__init__.py --- old/python-redfish-library-3.0.3/src/redfish/__init__.py 2021-10-15 22:51:22.000000000 +0200 +++ new/python-redfish-library-3.1.0/src/redfish/__init__.py 2022-01-10 14:55:53.000000000 +0100 @@ -6,7 +6,7 @@ """ Redfish restful library """ __all__ = ['rest', 'ris', 'discovery'] -__version__ = "3.0.3" +__version__ = "3.1.0" from redfish.rest.v1 import redfish_client from redfish.rest.v1 import AuthMethod diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/python-redfish-library-3.0.3/src/redfish/rest/v1.py new/python-redfish-library-3.1.0/src/redfish/rest/v1.py --- old/python-redfish-library-3.0.3/src/redfish/rest/v1.py 2021-10-15 22:51:22.000000000 +0200 +++ new/python-redfish-library-3.1.0/src/redfish/rest/v1.py 2022-01-10 14:55:53.000000000 +0100 @@ -8,26 +8,26 @@ #---------Imports--------- -import os import sys -import ssl import time import gzip import json import base64 import logging -import http.client -import re import warnings +import requests from collections import (OrderedDict) from urllib.parse import urlparse, urlencode, quote from io import StringIO -from io import BytesIO from requests_toolbelt import MultipartEncoder +# Many services come with self-signed certificates and will remain as such; need to suppress warnings for this +from requests.packages.urllib3.exceptions import InsecureRequestWarning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + #---------End of imports--------- #---------Debug logger--------- @@ -152,7 +152,7 @@ :params rest_request: Holder for request information :type rest_request: RestRequest object :params http_response: Response from HTTP - :type http_response: HTTPResponse + :type http_response: requests.Response """ self._read = None @@ -163,14 +163,15 @@ self._rest_request = rest_request self._http_response = http_response - if self._http_response: - self._read = self._http_response.read() - else: - self._read = None + if http_response is not None: + self._read = http_response.text + self._status = http_response.status_code @property def read(self): """Wrapper around httpresponse.read()""" + + # Backwards compatibility: for requests, we can simply use "text"; need to see if there are external dependencies to continue using read return self._read @read.setter @@ -188,7 +189,12 @@ def getheaders(self): """Property for accessing the headers""" - return self._http_response.getheaders() + + # Backwards compatibility: requests simply uses a dictionary, but older versions of this library returned a list of tuples + headers = [] + for header in self._http_response.headers: + headers.append((header, self._http_response.headers[header])) + return headers def getheader(self, name): """Property for accessing an individual header @@ -198,7 +204,7 @@ :returns: returns a header from HTTP response """ - return self._http_response.getheader(name, None) + return self._http_response.headers.get(name.lower(), None) def json(self, newdict): """Property for setting JSON data @@ -212,11 +218,7 @@ @property def text(self): """Property for accessing the data as an unparsed string""" - if isinstance(self.read, str): - value = self.read - else: - value = self.read.decode("utf-8", "ignore") - return value + return self.read @text.setter def text(self, value): @@ -232,10 +234,10 @@ def dict(self): """Property for accessing the data as an dict""" try: - return json.loads(self.text) + return json.loads(self.read) except: str = "Service responded with invalid JSON at URI {}\n{}".format( - self._rest_request.path, self.text) + self._rest_request.path, self.read) LOGGER.error(str) raise JsonDecodingError(str) from None @@ -247,10 +249,7 @@ @property def status(self): """Property for accessing the status code""" - if self._status: - return self._status - - return self._http_response.status + return self._status @property def session_key(self): @@ -258,7 +257,7 @@ if self._session_key: return self._session_key - self._session_key = self._http_response.getheader('x-auth-token') + self._session_key = self.getheader('x-auth-token') return self._session_key @property @@ -267,7 +266,7 @@ if self._session_location: return self._session_location - self._session_location = self._http_response.getheader('location') + self._session_location = self.getheader('location') return self._session_location @property @@ -276,7 +275,7 @@ if self._task_location: return self._task_location - self._task_location = self._http_response.getheader('location') + self._task_location = self.getheader('location') return self._task_location @property @@ -287,7 +286,14 @@ @property def retry_after(self): """Retry After header""" - return self._http_response.getheader('retry-after') + retry_after = self.getheader('retry-after') + if retry_after is not None: + # Convert to int for ease of use by callers + try: + retry_after = int(retry_after) + except: + retry_after = 5 + return retry_after def monitor(self, context): """Function to process Task, used on an action or POST/PATCH that returns 202""" @@ -438,112 +444,17 @@ self.__base_url = base_url.rstrip('/') self.__username = username self.__password = password - self.__url = urlparse(self.__base_url) self.__session_key = sessionkey self.__authorization_key = None self.__session_location = None - self._conn = None - self._conn_count = 0 + self._session = requests.Session() self._timeout = timeout self._max_retry = max_retry if max_retry is not None else 10 self.login_url = None self.default_prefix = default_prefix self.capath = capath self.cafile = cafile - - self.__init_connection() self.get_root_object() - self.__destroy_connection() - - @staticmethod - def _bypass_proxy(host): - """ - Read NO_PROXY environment variable to determine if proxy should be - bypassed for host. - - :param host: the host to check - :return: True is proxy should be bypassed, False otherwise - """ - if 'NO_PROXY' in os.environ: - no_proxy = os.environ['NO_PROXY'] - if no_proxy == '*': - return True - hostonly = host.rsplit(':', 1)[0] # without port - no_proxy_list = [proxy.strip() for proxy in no_proxy.split(',')] - for name in no_proxy_list: - if name: - name = name.lstrip('.') # ignore leading dots - name = re.escape(name) - pattern = r'(.+\.)?%s$' % name - if (re.match(pattern, hostonly, re.I) - or re.match(pattern, host, re.I)): - print('returning true (re)') - return True - return False - - def _get_connection(self, url, **kwargs): - """ - Wrapper function for the HTTPSConnection/HTTPConnection constructor - that handles proxies set by the HTTPS_PROXY and HTTP_PROXY environment - variables - - :param url: the target URL - :param kwargs: keyword arguments for the connection constructor - :return: the connection - """ - bypass_proxy = self._bypass_proxy(url.netloc) - proxy = None - if url.scheme.upper() == "HTTPS": - connection = http.client.HTTPSConnection - if not bypass_proxy and 'HTTPS_PROXY' in os.environ: - host = urlparse(os.environ['HTTPS_PROXY']).netloc - proxy = url.netloc - else: - host = url.netloc - else: - connection = http.client.HTTPConnection - if not bypass_proxy and 'HTTP_PROXY' in os.environ: - host = urlparse(os.environ['HTTP_PROXY']).netloc - proxy = url.netloc - else: - host = url.netloc - conn = connection(host, **kwargs) - if proxy: - LOGGER.debug("Proxy %s connection to %s through %s" % ( - url.scheme.upper(), proxy, host)) - conn.set_tunnel(proxy) - return conn - - def __init_connection(self, url=None): - """Function for initiating connection with remote server - - :param url: The URL of the remote system - :type url: str - - """ - self.__destroy_connection() - - url = url if url else self.__url - if url.scheme.upper() == "HTTPS": - if self.cafile or self.capath is not None: - ssl_context = ssl.create_default_context(capath=self.capath, - cafile=self.cafile) - else: - ssl_context = ssl._create_unverified_context() - self._conn = self._get_connection(url, context=ssl_context, - timeout=self._timeout) - elif url.scheme.upper() == "HTTP": - self._conn = self._get_connection(url, timeout=self._timeout) - else: - pass - - def __destroy_connection(self): - """Function for closing connection with remote server""" - if self._conn: - self._conn.close() - - self._conn = None - self._conn_count = 0 def __enter__(self): self.login() @@ -551,7 +462,6 @@ def __exit__(self, exc_type, exc_value, exc_traceback): self.logout() - self.__destroy_connection() def get_username(self): """Return used user name""" @@ -634,7 +544,7 @@ def get_root_object(self): """Perform an initial get and store the result""" try: - resp = self.get('%s%s' % (self.__url.path, self.default_prefix)) + resp = self.get(self.default_prefix) except Exception as excp: raise excp @@ -647,8 +557,8 @@ try: root_data = json.loads(content) except: - str = 'Service responded with invalid JSON at URI {}{}\n{}'.format( - self.__url.path, self.default_prefix, content) + str = 'Service responded with invalid JSON at URI {}\n{}'.format( + self.default_prefix, content) LOGGER.error(str) raise JsonDecodingError(str) from None @@ -775,12 +685,10 @@ if 'accept' not in headers_keys: headers['Accept'] = '*/*' - headers['Connection'] = 'Keep-Alive' - return headers def _rest_request(self, path, method='GET', args=None, body=None, - headers=None, skip_redirect=False): + headers=None, allow_redirects=True): """Rest request main function :param path: path within tree @@ -793,8 +701,8 @@ :type body: dict :param headers: provide additional headers :type headers: dict - :param skip_redirect: controls whether redirects are followed - :type skip_redirect: bool + :param allow_redirects: controls whether redirects are followed + :type allow_redirects: bool :returns: returns a RestResponse object """ @@ -857,11 +765,10 @@ LOGGER.error('Error occur while compressing body: %s', excp) raise - headers['Content-Length'] = len(body) - if args: if method == 'GET': - # Workaround for this bug: https://bugs.python.org/issue18857 + # Workaround for this: https://github.com/psf/requests/issues/993 + # Redfish supports some query parameters without using '=', which is apparently against HTML5 none_list = [] args_copy = {} for query in args: @@ -904,66 +811,33 @@ LOGGER.info('Attempt %s of %s', attempts, path) try: - while True: - if self._conn is None: - self.__init_connection() - - self._conn.request(method.upper(), reqpath, body=body, - headers=headers) - self._conn_count += 1 + if sys.version_info < (3, 3): + inittime = time.clock() + else: + inittime = time.perf_counter() - if sys.version_info < (3, 3): - inittime = time.clock() - else: - inittime = time.perf_counter() - resp = self._conn.getresponse() - if sys.version_info < (3, 3): - endtime = time.clock() - else: - endtime = time.perf_counter() - LOGGER.info('Response Time for %s to %s: %s seconds.' % - (method, reqpath, str(endtime-inittime))) - - if resp.getheader('Connection') == 'close': - self.__destroy_connection() - - # redirect handling - if resp.status not in list(range(300, 399)) or \ - resp.status == 304 or skip_redirect is True: - break - newloc = resp.getheader('location') - newurl = urlparse(newloc) - if resp.status in [301, 302, 303]: - method = 'GET' - body = None - for h in ['Content-Type', 'Content-Length']: - if h in headers: - del headers[h] + # TODO: Migration to requests lost the "CA directory" capability; need to revisit + verify = False + if self.cafile: + verify = self.cafile + resp = self._session.request(method.upper(), "{}{}".format(self.__base_url, reqpath), data=body, + headers=headers, timeout=self._timeout, allow_redirects=allow_redirects, + verify=verify) - reqpath = newurl.path - self.__init_connection(newurl) + if sys.version_info < (3, 3): + endtime = time.clock() + else: + endtime = time.perf_counter() + LOGGER.info('Response Time for %s to %s: %s seconds.' % + (method, reqpath, str(endtime-inittime))) restresp = RestResponse(restreq, resp) - - try: - if restresp.getheader('content-encoding') == "gzip": - compressedfile = BytesIO(restresp.read) - decompressedfile = gzip.GzipFile(fileobj=compressedfile) - restresp.text = decompressedfile.read().decode("utf-8") - except Exception as excp: - LOGGER.error('Error occur while decompressing body: %s', - excp) - raise DecompressResponseError() except Exception as excp: - if isinstance(excp, DecompressResponseError): - raise - if not cause_exception: cause_exception = excp LOGGER.info('Retrying %s [%s]'% (path, excp)) time.sleep(1) - self.__init_connection() continue else: break @@ -980,7 +854,7 @@ LOGGER.debug('HTTP RESPONSE for %s:\nCode: %s\nHeaders:\n' \ '%s\nBody Response of %s: %s'%\ (restresp.request.path, - str(restresp._http_response.status)+ ' ' + \ + str(restresp._http_response.status_code)+ ' ' + \ restresp._http_response.reason, headerstr, restresp.request.path, restresp.read)) except: @@ -1016,8 +890,7 @@ headers = dict() headers['Authorization'] = self.__authorization_key - respvalidate = self._rest_request('%s%s' % (self.__url.path, - self.login_url), headers=headers) + respvalidate = self._rest_request(self.login_url, headers=headers) if respvalidate.status == 401: #If your REST client has a delay for fail attempts add it here @@ -1030,7 +903,7 @@ headers = dict() resp = self._rest_request(self.login_url, method="POST",body=data, - headers=headers, skip_redirect=True) + headers=headers, allow_redirects=False) LOGGER.info('Login returned code %s: %s', resp.status, resp.text) @@ -1063,6 +936,7 @@ self.__session_key = None self.__session_location = None self.__authorization_key = None + self._session.close() class HttpClient(RestClientBase): """A client for Rest""" @@ -1108,7 +982,7 @@ self.login_url = '/redfish/v1/SessionService/Sessions' def _rest_request(self, path='', method="GET", args=None, body=None, - headers=None, skip_redirect=False): + headers=None, allow_redirects=True): """Rest request for HTTP client :param path: path within tree @@ -1121,8 +995,8 @@ :type body: dict :param headers: provide additional headers :type headers: dict - :param skip_redirect: controls whether redirects are followed - :type skip_redirect: bool + :param allow_redirects: controls whether redirects are followed + :type allow_redirects: bool :returns: returns a rest request """ @@ -1134,7 +1008,7 @@ return super(HttpClient, self)._rest_request(path=path, method=method, args=args, body=body, headers=headers, - skip_redirect=skip_redirect) + allow_redirects=allow_redirects) def _get_req_headers(self, headers=None, providerheader=None): """Get the request headers for HTTP client
