pax_global_header00006660000000000000000000000064147324025150014515gustar00rootroot0000000000000052 comment=ad73ed112dd914105d3c39b905dfb47066a172b9 python-redfish-3.2.7/000077500000000000000000000000001473240251500144715ustar00rootroot00000000000000python-redfish-3.2.7/.github/000077500000000000000000000000001473240251500160315ustar00rootroot00000000000000python-redfish-3.2.7/.github/workflows/000077500000000000000000000000001473240251500200665ustar00rootroot00000000000000python-redfish-3.2.7/.github/workflows/main.yml000066400000000000000000000067401473240251500215440ustar00rootroot00000000000000name: Release and Publish on: workflow_dispatch: inputs: version: description: 'Version number' required: true changes_1: description: 'Change entry' required: true changes_2: description: 'Change entry' required: false changes_3: description: 'Change entry' required: false changes_4: description: 'Change entry' required: false changes_5: description: 'Change entry' required: false changes_6: description: 'Change entry' required: false changes_7: description: 'Change entry' required: false changes_8: description: 'Change entry' required: false jobs: release_build: name: Build the release runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: token: ${{secrets.GITHUB_TOKEN}} - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build the changelog text run: | echo 'CHANGES<> $GITHUB_ENV echo "## [${{github.event.inputs.version}}] - $(date +'%Y-%m-%d')" >> $GITHUB_ENV echo "- ${{github.event.inputs.changes_1}}" >> $GITHUB_ENV if [[ -n "${{github.event.inputs.changes_2}}" ]]; then echo "- ${{github.event.inputs.changes_2}}" >> $GITHUB_ENV; fi if [[ -n "${{github.event.inputs.changes_3}}" ]]; then echo "- ${{github.event.inputs.changes_3}}" >> $GITHUB_ENV; fi if [[ -n "${{github.event.inputs.changes_4}}" ]]; then echo "- ${{github.event.inputs.changes_4}}" >> $GITHUB_ENV; fi if [[ -n "${{github.event.inputs.changes_5}}" ]]; then echo "- ${{github.event.inputs.changes_5}}" >> $GITHUB_ENV; fi if [[ -n "${{github.event.inputs.changes_6}}" ]]; then echo "- ${{github.event.inputs.changes_6}}" >> $GITHUB_ENV; fi if [[ -n "${{github.event.inputs.changes_7}}" ]]; then echo "- ${{github.event.inputs.changes_7}}" >> $GITHUB_ENV; fi if [[ -n "${{github.event.inputs.changes_8}}" ]]; then echo "- ${{github.event.inputs.changes_8}}" >> $GITHUB_ENV; fi echo "" >> $GITHUB_ENV echo 'EOF' >> $GITHUB_ENV - name: Update version numbers run: | sed -i -E 's/ version=.+,/ version='\'${{github.event.inputs.version}}\'',/' setup.py sed -i -E 's/__version__ = .+/__version__ = "'${{github.event.inputs.version}}'"/' src/redfish/__init__.py - name: Update the changelog run: | ex CHANGELOG.md <" git add CHANGELOG.md setup.py src/redfish/__init__.py git commit -s -m "${{github.event.inputs.version}} versioning" git push origin main - name: Make the release env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} run: | gh release create ${{github.event.inputs.version}} -t ${{github.event.inputs.version}} -n "Changes since last release:"$'\n\n'"$CHANGES" - name: Build the distribution run: | python setup.py sdist bdist_wheel - name: Upload to pypi uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_API_TOKEN }} python-redfish-3.2.7/.gitignore000066400000000000000000000014701473240251500164630ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *__pycache__* *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # pyenv .python-version # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # Editors *.swa *.swp *~ python-redfish-3.2.7/.project000066400000000000000000000005721473240251500161440ustar00rootroot00000000000000 redfish org.python.pydev.PyDevBuilder org.python.pydev.pythonNature python-redfish-3.2.7/.pydevproject000066400000000000000000000006631473240251500172150ustar00rootroot00000000000000 /${PROJECT_DIR_NAME}/src python 2.7 Default python-redfish-3.2.7/.travis.yml000066400000000000000000000003341473240251500166020ustar00rootroot00000000000000dist: focal language: python cache: - pip python: - '3.8' - '3.9' - '3.11' before_install: - pip install -U pip - pip install -U setuptools - pip install -U wheel install: - pip install tox-travis .[devel] script: - tox python-redfish-3.2.7/AUTHORS.md000066400000000000000000000010011473240251500161300ustar00rootroot00000000000000# Original Contribution: * [Jack Garcia](//github.com/lumbajack) - HPE - Hewlett Packard Enterprise Restful API Group * [Matthew Kocurek](//github.com/Yergidy) - HPE - Hewlett Packard Enterprise Restful API Group * [Prithvi Subrahmanya](//github.com/PrithviBS) - HPE - Hewlett Packard Enterprise Restful API Group # Other Key Contributions: * For a list of people who have contributed to the codebase, see [GitHub's list of contributors](https://github.com/DMTF/python-redfish-library/contributors).* python-redfish-3.2.7/CHANGELOG.md000066400000000000000000000124071473240251500163060ustar00rootroot00000000000000# Change Log ## [3.2.7] - 2024-12-24 - Added JSON formatting of responses to debug logs ## [3.2.6] - 2024-11-15 - Added workaround for services incorrectly responding with 401 when accessing the service root ## [3.2.5] - 2024-09-13 - Added new 'check_connectivity' option when creating the client object ## [3.2.4] - 2024-08-09 - No code changes; releasing again for PyPI publication ## [3.2.3] - 2024-08-09 - Removed clearing of username and password to allow for sessions to be re-established ## [3.2.2] - 2024-01-19 - Minor changes to fix Python 3.12 warnings with usage of raw strings ## [3.2.1] - 2023-08-04 - Added 'timeout' and 'max_retry' parameters to all REST methods - Added exception to the method when a response contains a message indicating a password change is required ## [3.2.0] - 2023-07-27 - Adding missing newline to M-SEARCH requests - Fixed the inspection of the USN response header from M-SEARCH requests to allow for a multi-digit minor version ## [3.1.9] - 2023-01-13 - Improved usage of the ServerDownOrUnreachableError exception to not lose the original message ## [3.1.8] - 2022-12-02 - Added request headers to debug log output - Added redacting of 'Password' properties from request bodies from debug logs ## [3.1.7] - 2022-09-09 - Added handling for extracting error information when a session could not be created ## [3.1.6] - 2022-05-12 - Fixed issue where the 'read' method on response objects always return strings - Modified query parameter encoding to not percent-encode characters allowed in query strings per RFC3986 ## [3.1.5] - 2022-04-01 - Added methods for specifying proxies directly with a new 'proxies' parameter ## [3.1.4] - 2022-03-25 - Removed enforcement of trailing '/' in the 'default_prefix' ## [3.1.3] - 2022-03-21 - Added support for Unix sockets ## [3.1.2] - 2022-03-10 - Corrected usage of header storage and retrieval for static response objects ## [3.1.1] - 2022-01-18 - Corrected 'import' statements to support Python 3.10 ## [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 ## [3.0.2] - 2021-08-30 - Added support for prepending 'https://' when the provided URI of the service does not contain a scheme ## [3.0.1] - 2021-06-04 - Provided additional handling for HTTP 301 and 302 redirects - Changed session creation to not follow redirects in order to ensure the session token and location are not lost - Enhanced invalid JSON response handling to better highlight a service error ## [3.0.0] - 2021-02-20 - Removed Python2 support ## [2.2.0] - 2021-02-15 - Added support for `NO_PROXY` environment variable ## [2.1.9] - 2020-12-04 - Added handling for HTTP 303 responses as part of redirect handling ## [2.1.8] - 2020-08-10 - Added option to SSDP discover to bind to a specified address - Added ability to override built-in HTTP headers - Fixed issue where the location of a session was not being tracked properly for HTTP connections ## [2.1.7] - 2020-07-06 - Added support for setting the 'Content-Type' header to 'application/octet-stream' when binary data is provided in a request ## [2.1.6] - 2020-06-12 - Added support for leveraging the 'HTTP_PROXY' and 'HTTPS_PROXY' environment variables to set up proxy information ## [2.1.5] - 2020-02-03 - Removed urlparse2 dependency - Updated jsonpatch requirements; jsonpatch 1.25 dropped Python 3.4 support ## [2.1.4] - 2020-01-10 - Added fallback to using '/redfish/v1/SessionService/Sessions' if the service root does not contains the 'Links/Sessions' property for login - Added Python version checks to use time.perf_counter() in favor of time.clock() ## [2.1.3] - 2019-10-11 - Added IPv6 support to SSDP discovery - Enhanced handling of poorly formatted URIs to not throw an exception ## [2.1.2] - 2019-09-16 - Fixed usage of capath and cafile when setting them to None ## [2.1.1] - 2019-08-16 - Added option in SSDP discovery to specify a particular interface - Added sanitization to the Base URL to remove trailing slashes ## [2.1.0] - 2019-07-12 - Changed default authentication to be Session based - Removed unnecessary closing of sockets ## [2.0.9] - 2019-06-28 - Added various unit tests and other cleanup - Added example for how to use the 'with' statement to perform automatically log out of a service - Made change to include the original trace when RetriesExhaustedError is encountered ## [2.0.8] - 2019-05-17 - Added helper functions for Task processing ## [2.0.7] - 2019-02-08 - Added optional timeout and max retry arguments ## [2.0.6] - 2019-01-11 - Removed usage of setting the Content-Type header to application/x-www-form-urlencoded for PUT, POST, and PATCH methods ## [2.0.5] - 2018-11-30 - Fixed handling of gzip content encoding ## [2.0.4] - 2018-10-26 - Added discovery module with SSDP support ## [2.0.3] - 2018-10-19 - Fixed handling of other successful HTTP responses (201, 202, and 204) - Added support for being able to check the certificate of a service ## [2.0.2] - 2018-09-07 - Added handling for bad or dummy delete requests when logging out of a service ## [2.0.1] - 2018-05-25 - Adjusting setup.py to contain correct information ## [2.0.0] - 2017-07-28 - Python 3 Compatible Release ## [1.0.0] - 2017-01-12 - Initial Public Release -- supports Redfish 1.0 features python-redfish-3.2.7/CONTRIBUTING.md000066400000000000000000000050141473240251500167220ustar00rootroot00000000000000# Contributing ## Overview This repository is maintained by the [DMTF](https://www.dmtf.org/ "https://www.dmtf.org/"). All contributions are reviewed and approved by members of the organization. ## Submitting Issues Bugs, feature requests, and questions are all submitted in the "Issues" section for the project. DMTF members are responsible for triaging and addressing issues. ## Contribution Process 1. Fork the repository. 2. Make and commit changes. 3. Make a pull request. All contributions must adhere to the BSD 3-Clause License described in the LICENSE.md file, and the [Developer Certificate of Origin](#developer-certificate-of-origin). Pull requests are reviewed and approved by DMTF members. ## Developer Certificate of Origin All contributions must adhere to the [Developer Certificate of Origin (DCO)](http://developercertificate.org "http://developercertificate.org"). The DCO is an attestation attached to every contribution made by every developer. In the commit message of the contribution, the developer adds a "Signed-off-by" statement and thereby agrees to the DCO. This can be added by using the `--signoff` parameter with `git commit`. Full text of the DCO: ``` Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` python-redfish-3.2.7/LICENSE.md000066400000000000000000000030341473240251500160750ustar00rootroot00000000000000BSD 3-Clause License Copyright (c) 2016-2024, Contributing Member(s) of Distributed Management Task Force, Inc.. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-redfish-3.2.7/README.rst000066400000000000000000000260721473240251500161670ustar00rootroot00000000000000python-redfish-library ====================== .. image:: https://img.shields.io/pypi/v/redfish.svg?maxAge=2592000 :target: https://pypi.python.org/pypi/redfish .. image:: https://img.shields.io/github/release/DMTF/python-redfish-library.svg?maxAge=2592000 :target: https://github.com/DMTF/python-redfish-library/releases .. image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg :target: https://raw.githubusercontent.com/DMTF/python-redfish-library/main/LICENSE .. image:: https://img.shields.io/pypi/pyversions/redfish.svg?maxAge=2592000 :target: https://pypi.python.org/pypi/redfish .. contents:: :depth: 1 Description ----------- As of version 3.0.0, Python2 is no longer supported. If Python2 is required, ``redfish<3.0.0`` can be specified in a requirements file. REST (Representational State Transfer) is a web based software architectural style consisting of a set of constraints that focuses on a system's resources. The Redfish library performs GET, POST, PUT, PATCH and DELETE HTTP operations on resources within a Redfish service. Go to the `wiki <../../wiki>`_ for more details. Installing ---------- .. code-block:: console pip install redfish Building from zip file source ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: console python setup.py sdist --formats=zip (this will produce a .zip file) cd dist pip install redfish-x.x.x.zip Requirements ------------ Ensure the system does not have the OpenStack "python-redfish" module installed on the target system. This module is using a conflicting package name that this library already uses. The module in question can be found here: https://pypi.org/project/python-redfish/ Required external packages: .. code-block:: console jsonpatch<=1.24 ; python_version == '3.4' jsonpatch ; python_version >= '3.5' jsonpath_rw jsonpointer requests requests-toolbelt requests-unixsocket If installing from GitHub, you may install the external packages by running: .. code-block:: console pip install -r requirements.txt Usage ---------- A set of examples is provided under the examples directory of this project. In addition to the directives present in this paragraph, you will find valuable implementation tips and tricks in those examples. Import the relevant Python module ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For a Redfish conformant application import the relevant Python module. For Redfish conformant application: .. code-block:: python import redfish Create a Redfish object ~~~~~~~~~~~~~~~~~~~~~~~ The Redfish object contains three required parameters: * ``base_url``: The address of the Redfish service (with scheme). Example: ``https://192.168.1.100``. For Unix sockets, use the scheme ``http+unix://``, followed by the percent-encoded filepath to the socket. * ``username``: The username for authentication. * ``password``: The password for authentication. There are several optional parameters: * ``default_prefix``: The path to the Redfish service root. This is only used for initial connection and authentication with the service. The default value is ``/redfish/v1/``. * ``sessionkey``: The session key to use with subsequent requests. This can be used to bypass the login step. The default value is ``None``. * ``cafile``: The file path to the CA certificate that issued the Redfish service's certificate. The default value is ``None``. * ``timeout``: The number of seconds to wait for a response before closing the connection. The default value is ``None``. * ``max_retry``: The number of retries to perform an operation before giving up. The default value is ``10``. * ``proxies``: A dictionary containing protocol to proxy URL mappings. The default value is ``None``. See `Using proxies`_. * ``check_connectivity``: A boolean value to determine whether the client immediately attempts a connection to the base_url. The default is ``True``. To create a Redfish object, call the ``redfish_client`` method: .. code-block:: python REDFISH_OBJ = redfish.redfish_client(base_url=login_host, username=login_account, \ password=login_password, default_prefix='/redfish/v1/') Login to the service ~~~~~~~~~~~~~~~~~~~~ After creating the REDFISH_OBJ, perform the ``login`` operation to authenticate with the service. The ``auth`` parameter allows you to specify the login method. Possible values are: * ``session``: Creates a Redfish session with a session token. * ``basic``: Uses HTTP Basic authentication for all requests. .. code-block:: python REDFISH_OBJ.login(auth="session") Perform a GET operation ~~~~~~~~~~~~~~~~~~~~~~~ A simple GET operation can be performed to obtain the data present in any valid path. An example of GET operation on the path "/redfish/v1/Systems/1" is shown below: .. code-block:: python response = REDFISH_OBJ.get("/redfish/v1/Systems/1") Perform a POST operation ~~~~~~~~~~~~~~~~~~~~~~~~ A POST operation can be performed to create a resource or perform an action. An example of a POST operation on the path "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset" is shown below: .. code-block:: python body = {"ResetType": "GracefulShutdown"} response = REDFISH_OBJ.post("/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", body=body) Notes about HTTP methods and arguments ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The previous sections showed example GET and POST requests. The following is a list of the different methods supported: * ``get``: Performs an HTTP GET operation to retrieve a resource from a URI. * ``head``: Performs an HTTP HEAD operation to retrieve response headers from a URI, but no body. * ``post``: Performs an HTTP POST operation to perform an action or create a new resource. * ``put``: Performs an HTTP PUT operation to replace an existing resource. * ``patch``: Performs an HTTP PATCH operation to update an existing resource. * ``delete``: Performs an HTTP DELETE operation to remove a resource. Each of the previous methods allows for the following arguments: * ``path``: **Required**. String. The URI in which to invoke the operation. - Example: ``"/redfish/v1/Systems/1"`` * ``args``: Dictionary. Query parameters to supply with the request. - The key-value pairs in the dictionary are the query parameter name and the query parameter value to supply. - Example: ``{"$select": "Reading,Status"}`` * ``body``: Dictionary, List, Bytes, or String. The request body to provide with the request. - Not supported for ``get``, ``head``, or ``delete`` methods. - The data type supplied will dictate the encoding. - A dictionary is the most common usage, which results in a JSON body. - Example: ``{"ResetType": "GracefulShutdown"}`` - A list is used to supply multipart forms, which is useful for multipart HTTP push updates. - Bytes is used to supply an octet stream. - A string is used to supply an unstructed body, which may be used in some OEM cases. * ``headers``: Dictionary. Additional HTTP headers to supply with the request. - The key-value pairs in the dictionary are the HTTP header name and the HTTP header value to supply. - Example: ``{"If-Match": etag_value}`` * ``timeout``: Number. The number of seconds to wait for a response before closing the connection for this request. - Overrides the timeout value specified when the Redfish object is created for this request. - This can be useful when a particular URI is known to take a long time to respond, such as with firmware updates. - The default value is ``None``, which indicates the object-defined timeout is used. * ``max_retry``: Number. The number of retries to perform an operation before giving up for this request. - Overrides the max retry value specified when the Redfish object is created for this request. - This can be useful when a particular URI is known to take multiple retries. - The default value is ``None``, which indicates the object-defined max retry count is used. Working with tasks ~~~~~~~~~~~~~~~~~~ POST, PATCH, PUT, and DELETE operations may result in a task, describing an operation with a duration greater than the span of a single request. The action message object that ``is_processing`` will return a task that can be accessed reviewed when polled with monitor. An example of a POST operation with a possible task is shown below. .. code-block:: python body = {"ResetType": "GracefulShutdown"} response = REDFISH_OBJ.post("/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", body=body) if(response.is_processing): task = response.monitor(REDFISH_OBJ) while(task.is_processing): retry_time = task.retry_after task_status = task.dict['TaskState'] time.sleep(retry_time if retry_time else 5) task = response.monitor(REDFISH_OBJ) Logout the created session ~~~~~~~~~~~~~~~~~~~~~~~~~~ Ensure you perform a ``logout`` operation when done interacting with the Redfish service. If this step isn't performed, the session will remain active until the Redfish service decides to close it. .. code-block:: python REDFISH_OBJ.logout() The ``logout`` operation deletes the current sesssion from the service. The ``redfish_client`` object destructor includes a logout statement. Using proxies ~~~~~~~~~~~~~ There are two methods for using proxies: configuring environment variables or directly providing proxy information. Environment variables ^^^^^^^^^^^^^^^^^^^^^ You can use a proxy by specifying the ``HTTP_PROXY`` and ``HTTPS_PROXY`` environment variables. Hosts to be excluded from the proxy can be specified using the NO_PROXY environment variable. .. code-block:: shell export HTTP_PROXY="http://192.168.1.10:8888" export HTTPS_PROXY="http://192.168.1.10:8888" Directly provided ^^^^^^^^^^^^^^^^^ You can use a proxy by building a dictionary containing the proxy information and providing it to the ``proxies`` argument when creating the ``redfish_client`` object. The key-value pairs of the dictionary contain the protocol and the proxy URL for the protocol. .. code-block:: python proxies = { 'http': 'http://192.168.1.10:8888', 'https': 'http://192.168.1.10:8888', } REDFISH_OBJ = redfish.redfish_client(base_url=login_host, username=login_account, \ password=login_password, proxies=proxies) SOCKS proxy support ^^^^^^^^^^^^^^^^^^^ An additional package is required to use SOCKS proxies. .. code-block:: console pip install -U requests[socks] Once installed, the proxy can be configured using environment variables or directly provided like any other proxy. For example: .. code-block:: shell export HTTP_PROXY="socks5h://localhost:8123" export HTTPS_PROXY="socks5h://localhost:8123" Release Process --------------- 1. Go to the "Actions" page 2. Select the "Release and Publish" workflow 3. Click "Run workflow" 4. Fill out the form 5. Click "Run workflow" Copyright and License --------------------- Copyright Notice: Copyright 2016-2022 DMTF. All rights reserved. License: BSD 3-Clause License. For full text see link: `https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md `_ python-redfish-3.2.7/examples/000077500000000000000000000000001473240251500163075ustar00rootroot00000000000000python-redfish-3.2.7/examples/context_manager.py000066400000000000000000000013461473240251500220430ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md import sys import redfish # When running remotely connect using the address, account name, # and password to send https requests login_host = "https://192.168.1.100" login_account = "admin" login_password = "password" ## Create a REDFISH object with redfish.redfish_client(base_url=login_host, username=login_account, password=login_password) as REDFISH_OBJ: # Do a GET on a given path response = REDFISH_OBJ.get("/redfish/v1/systems/1", None) # Print out the response sys.stdout.write("%s\n" % response) python-redfish-3.2.7/examples/discover.py000066400000000000000000000006241473240251500205010ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md import redfish # Invoke the discovery routine for SSDP and print the responses services = redfish.discover_ssdp() for service in services: print( '{}: {}'.format(service, services[service])) python-redfish-3.2.7/examples/multipart_push.py000066400000000000000000000031331473240251500217410ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md import sys import json import redfish # When running remotely connect using the address, account name, # and password to send https requests login_host = "https://192.168.1.100" login_account = "admin" login_password = "password" ## Create a REDFISH object REDFISH_OBJ = redfish.redfish_client(base_url=login_host, username=login_account, password=login_password, default_prefix='/redfish/v1') # Login into the server and create a session REDFISH_OBJ.login(auth="session") # Format parts of the Update headers = {'Content-Type': 'multipart/form-data'} body = {} body['UpdateParameters'] = (None, json.dumps({'Targets': ['/redfish/v1/Managers/1'], 'Oem': {}}), 'application/json') body['UpdateFile'] = ('flash.bin', open('flash.bin', 'rb'), 'application/octet-stream') # The "OemXXX" part is optional in the specification # Must be formatted as 3-tuple: # ('filename' or None, content, content-type), body['OemXXX'] = (None, '{"test": "value"}', 'application/json') body['OemXXX'] = ('extra.bin', open('extra.txt', 'rb'), 'application/octet-stream') body['OemXXX'] = ('optional.txt', open('optional.txt', 'r').read(), 'text/plain') # Perform the POST operation response = REDFISH_OBJ.post('/redfish/v1/upload', body=body, headers=headers) # Print out the response sys.stdout.write("%s\n" % response) # Logout of the current session REDFISH_OBJ.logout() python-redfish-3.2.7/examples/quickstart.py000066400000000000000000000016251473240251500210570ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md import sys import redfish # When running remotely connect using the address, account name, # and password to send https requests login_host = "https://192.168.1.100" login_account = "admin" login_password = "password" ## Create a REDFISH object REDFISH_OBJ = redfish.redfish_client(base_url=login_host, username=login_account, password=login_password, default_prefix='/redfish/v1') # Login into the server and create a session REDFISH_OBJ.login(auth="session") # Do a GET on a given path response = REDFISH_OBJ.get("/redfish/v1/systems/1", None) # Print out the response sys.stdout.write("%s\n" % response) # Logout of the current session REDFISH_OBJ.logout()python-redfish-3.2.7/examples/quickstart_rmc.py000066400000000000000000000031011473240251500217070ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md import os import sys import json import logging from redfish import redfish_logger from redfish.ris import RmcApp, JSONEncoder # Config logger used by Restful library LOGGERFILE = "RedfishApiExamples.log" LOGGERFORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" LOGGER = redfish_logger(LOGGERFILE, LOGGERFORMAT, logging.ERROR) LOGGER.info("Redfish API examples") # When running remotely connect using the address, account name, # and password to send https requests login_host = "https://192.168.1.100" login_account = "admin" login_password = "password" # Creating RMC object RMCOBJ = RmcApp([]) # Create cache directory config_dir = r'C:\DATA\redfish' RMCOBJ.config.set_cachedir(os.path.join(config_dir, 'cache')) cachedir = RMCOBJ.config.get_cachedir() # If current cache exist try to log it out if os.path.isdir(cachedir): RMCOBJ.logout # Login into the server and create a session RMCOBJ.login(username=login_account, password=login_password, \ base_url=login_host) # Select ComputerSystems RMCOBJ.select(['ComputerSystem.']) # Get selected type response = RMCOBJ.get() # Print out the response for item in response: sys.stdout.write(json.dumps(item, indent=2, cls=JSONEncoder)) sys.stdout.write('\n') # Logout of the current session RMCOBJ.logout() python-redfish-3.2.7/makeandinstallzip.bat000066400000000000000000000001251473240251500206710ustar00rootroot00000000000000python setup.py sdist --formats=zip cd dist pip install --upgrade redfish-2.0.0.zippython-redfish-3.2.7/requirements.txt000066400000000000000000000002341473240251500177540ustar00rootroot00000000000000jsonpatch<=1.24 ; python_version == '3.4' jsonpatch ; python_version >= '3.5' jsonpath_rw jsonpointer requests requests-toolbelt requests-unixsocket python-redfish-3.2.7/setup.py000066400000000000000000000027531473240251500162120ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md from setuptools import setup, find_packages from codecs import open from os import path here = path.abspath(path.dirname(__file__)) with open(path.join(here, 'README.rst'), encoding='utf-8') as f: long_description = f.read() setup(name='redfish', version='3.2.7', description='Redfish Python Library', long_description=long_description, long_description_content_type='text/x-rst', author = 'DMTF, https://www.dmtf.org/standards/feedback', license='BSD 3-clause "New" or "Revised License"', classifiers=[ 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 3', 'Topic :: Communications' ], keywords='Redfish', url='https://github.com/DMTF/python-redfish-library', packages=find_packages('src'), package_dir={'': 'src'}, install_requires=[ 'jsonpath_rw', 'jsonpointer', "requests", 'requests_toolbelt', 'requests-unixsocket' ], extras_require={ ':python_version == "3.4"': [ 'jsonpatch<=1.24' ], ':python_version >= "3.5"': [ 'jsonpatch' ] }) python-redfish-3.2.7/src/000077500000000000000000000000001473240251500152605ustar00rootroot00000000000000python-redfish-3.2.7/src/redfish/000077500000000000000000000000001473240251500167045ustar00rootroot00000000000000python-redfish-3.2.7/src/redfish/__init__.py000066400000000000000000000014531473240251500210200ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md """ Redfish restful library """ __all__ = ['rest', 'ris', 'discovery', 'messages'] __version__ = "3.2.7" from redfish.rest.v1 import redfish_client from redfish.rest.v1 import AuthMethod from redfish.discovery.discovery import discover_ssdp from redfish.messages import * import logging def redfish_logger(file_name, log_format, log_level=logging.ERROR): formatter = logging.Formatter(log_format) fh = logging.FileHandler(file_name) fh.setFormatter(formatter) logger = logging.getLogger(__name__) logger.addHandler(fh) logger.setLevel(log_level) return logger python-redfish-3.2.7/src/redfish/discovery/000077500000000000000000000000001473240251500207135ustar00rootroot00000000000000python-redfish-3.2.7/src/redfish/discovery/__init__.py000066400000000000000000000003101473240251500230160ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md python-redfish-3.2.7/src/redfish/discovery/discovery.py000066400000000000000000000077621473240251500233100ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md # -*- coding: utf-8 -*- """Discovers Redfish services""" import http.client import re import socket from io import BytesIO class FakeSocket: """Helper class to force raw data into an HTTP Response structure""" def __init__(self, response_str): self._file = BytesIO(response_str) def makefile(self, *args, **kwargs): return self._file def sanitize(number, minimum, maximum=None): """ Sanity check a given number. :param number: the number to check :param minimum: the minimum acceptable number :param maximum: the maximum acceptable number (optional) if maximum is not given sanitize return the given value superior at minimum :returns: an integer who respect the given allowed minimum and maximum """ if number < minimum: number = minimum elif maximum is not None and number > maximum: number = maximum return number def discover_ssdp(port=1900, ttl=2, response_time=3, iface=None, protocol="ipv4", address=None): """Discovers Redfish services via SSDP :param port: the port to use for the SSDP request :type port: int :param ttl: the time-to-live value for the request :type ttl: int :param response_time: the number of seconds in which a service can respond :type response_time: int :param iface: the interface to use for the request; None for all :type iface: string :param protocol: the type of protocol to use for the request; either 'ipv4' or 'ipv6' :type protocol: string :param address: the address to use for the request; None for all :type address: string :returns: a set of discovery data """ # Sanity check the inputs valid_protocols = ("ipv4", "ipv6") if protocol not in valid_protocols: raise ValueError("Invalid protocol type. Expected one of: {}".format(valid_protocols)) ttl = sanitize(ttl, minimum=1, maximum=255) response_time = sanitize(response_time, minimum=1) if protocol == "ipv6": mcast_ip = "ff02::c" mcast_connection = (mcast_ip, port, 0, 0) af_type = socket.AF_INET6 elif protocol == "ipv4": mcast_ip = "239.255.255.250" mcast_connection = (mcast_ip, port) af_type = socket.AF_INET # Initialize the multicast data msearch_str = ( "M-SEARCH * HTTP/1.1\r\n" "Host: {}:{}\r\n" 'Man: "ssdp:discover"\r\n' "ST: urn:dmtf-org:service:redfish-rest:1\r\n" "MX: {}\r\n\r\n" ).format(mcast_ip, port, response_time) socket.setdefaulttimeout(response_time + 2) # Set up the socket and send the request sock = socket.socket(af_type, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) if address: sock.bind((address, 0)) if iface: sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, str(iface+"\0").encode("utf-8")) sock.sendto(bytearray(msearch_str, "utf-8"), mcast_connection) # On the same socket, wait for responses discovered_services = {} pattern = re.compile( r"^uuid:([a-f0-9\-]*)::urn:dmtf-org:service:redfish-rest:1(:\d+)?$") # noqa while True: try: response = http.client.HTTPResponse(FakeSocket(sock.recv(1024))) response.begin() uuid_search = pattern.search(response.getheader("USN").lower()) if uuid_search: discovered_services[uuid_search.group(1)] = response.getheader( "AL" ) except socket.timeout: # We hit the timeout; done waiting for responses break sock.close() return discovered_services python-redfish-3.2.7/src/redfish/messages/000077500000000000000000000000001473240251500205135ustar00rootroot00000000000000python-redfish-3.2.7/src/redfish/messages/__init__.py000066400000000000000000000003331473240251500226230ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md from .messages import *python-redfish-3.2.7/src/redfish/messages/messages.py000066400000000000000000000116361473240251500227030ustar00rootroot00000000000000#! /usr/bin/python # Copyright Notice: # Copyright 2019-2020 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Tacklebox/blob/main/LICENSE.md """ Messages Module File : messages.py Brief : This file contains the definitions and functionalities for interacting with Messages for a given Redfish service """ import re class RedfishOperationFailedError( Exception ): """ Raised when an operation has failed (HTTP Status >= 400) """ pass class RedfishPasswordChangeRequiredError( Exception ): """ Raised when password change required """ def __str__(self): return "\n{}\nURL: {}\n".format( str(self.args[0]), str(self.args[1]) ) def get_messages_detail( response ): """ Builds messages detail dict in the payload Args: response: The response to parser Returns: The dict containing messages_detail messages_detail["status"]: http status code messages_detail["successful"]: response successful (http status code < 400) messages_detail["code"]: redfish message response code field messages_detail["@Message.ExtendedInfo"]: redfish message response code field """ messages_detail = {} messages_detail["status"] = response.status messages_detail["text"] = response.text messages_detail["successful"] = False messages_detail["@Message.ExtendedInfo"] = [] if response.status >= 400: messages_detail["successful"] = False else: messages_detail["successful"] = True try: message_body = response.dict messages_detail["body"] = response.dict if not "@Message.ExtendedInfo" in message_body: message_body = response.dict["error"] check_message_field = True if "@Message.ExtendedInfo" in message_body: messages_detail["@Message.ExtendedInfo"] = message_body["@Message.ExtendedInfo"] for index in range(len(messages_detail["@Message.ExtendedInfo"])): messages_item = messages_detail["@Message.ExtendedInfo"][index] if not "MessageId" in messages_item: messages_item["MessageId"] = "" if not "Message" in messages_item: messages_item["Message"] = "" messages_detail["@Message.ExtendedInfo"][index] = messages_item check_message_field = False if check_message_field is True: messages_detail["@Message.ExtendedInfo"] = [] messages_item = {} if "code" in message_body: messages_item["MessageId"] = message_body["code"] else: messages_item["MessageId"] = "" if "message" in message_body: messages_item["Message"] = message_body["message"] else: messages_item["Message"] = "" messages_detail["@Message.ExtendedInfo"].insert(0, messages_item) except: messages_detail["@Message.ExtendedInfo"] = [] messages_detail["body"] = {} return messages_detail def search_message(response, message_registry_group, message_registry_id): """ search message in the payload Args: response: The response to parser message_registry_group: target message_registry_group message_registry_id: target message_registry_id Returns: The dict containing target message detail """ if isinstance(response, dict) and "@Message.ExtendedInfo" in response: messages_detail = response else: messages_detail = get_messages_detail(response) message_registry_id_search = "^" + message_registry_group + r"\.[0-9]+\.[0-9]+\." + message_registry_id +"$" for messages_item in messages_detail["@Message.ExtendedInfo"]: if "MessageId" in messages_item: resault = re.search(message_registry_id_search, messages_item["MessageId"]) if resault: return messages_item return None def get_error_messages( response ): """ Builds a string based on the error messages in the payload Args: response: The response to print Returns: The string containing error messages """ # Pull out the error payload and the messages out_string = "" try: if isinstance(response, dict) and "@Message.ExtendedInfo" in response: messages_detail = response else: messages_detail = get_messages_detail(response) if "@Message.ExtendedInfo" in messages_detail: for message in messages_detail["@Message.ExtendedInfo"]: if "Message" in message: out_string = out_string + "\n" + message["Message"] else: out_string = out_string + "\n" + message["MessageId"] out_string = out_string + "\n" except: # No response body out_string = "" return out_string python-redfish-3.2.7/src/redfish/rest/000077500000000000000000000000001473240251500176615ustar00rootroot00000000000000python-redfish-3.2.7/src/redfish/rest/__init__.py000066400000000000000000000004071473240251500217730ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md """ Utilities to simplify interaction with Redfish data """ python-redfish-3.2.7/src/redfish/rest/v1.py000066400000000000000000001331101473240251500205600ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md # -*- coding: utf-8 -*- """Helper module for working with REST technology.""" #---------Imports--------- import sys import time import gzip import json import base64 import logging import warnings import re import requests import requests_unixsocket from redfish.messages import * from collections import (OrderedDict) from urllib.parse import urlparse, urlencode, quote from io import StringIO 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--------- LOGGER = logging.getLogger(__name__) #---------End of debug logger--------- class RetriesExhaustedError(Exception): """Raised when retry attempts have been exhausted.""" pass class InvalidCredentialsError(Exception): """Raised when invalid credentials have been provided.""" pass class SessionCreationError(Exception): """Raised when a session could not be created.""" pass class ServerDownOrUnreachableError(Exception): """Raised when server is unreachable.""" def __init__(self,message,*,response=None): super().__init__(message) self.response = response class DecompressResponseError(Exception): """Raised when decompressing response failed.""" pass class JsonDecodingError(Exception): """Raised when the JSON response data is malformed.""" pass class BadRequestError(Exception): """Raised when bad request made to server.""" pass class RisObject(dict): """Converts a JSON/Rest dict into a object so you can use .property notation""" __getattr__ = dict.__getitem__ def __init__(self, d): """Initialize RisObject :param d: dictionary to be parsed :type d: dict """ super(RisObject, self).__init__() self.update(**dict((k, self.parse(value)) \ for k, value in d.items())) @classmethod def parse(cls, value): """Parse for RIS value :param cls: class referenced from class method :type cls: RisObject :param value: value to be parsed :type value: data type :returns: returns parsed value """ if isinstance(value, dict): return cls(value) elif isinstance(value, list): return [cls.parse(i) for i in value] else: return value class RestRequest(object): """Holder for Request information""" def __init__(self, path, method='GET', body=''): """Initialize RestRequest :param path: path within tree :type path: str :param method: method to be implemented :type method: str :param body: body payload for the rest call :type body: dict """ self._path = path self._body = body self._method = method def _get_path(self): """Return object path""" return self._path path = property(_get_path, None) def _get_method(self): """Return object method""" return self._method method = property(_get_method, None) def _get_body(self): """Return object body""" return self._body body = property(_get_body, None) def __str__(self): """Format string""" strvars = dict(body=self.body, method=self.method, path=self.path) # set None to '' for strings if not strvars['body']: strvars['body'] = '' try: strvars['body'] = str(str(self._body)) except BaseException: strvars['body'] = '' return "%(method)s %(path)s\n\n%(body)s" % strvars class RestResponse(object): """Returned by Rest requests""" def __init__(self, rest_request, http_response): """Initialize RestResponse :params rest_request: Holder for request information :type rest_request: RestRequest object :params http_response: Response from HTTP :type http_response: requests.Response """ self._read = None self._status = None self._session_key = None self._session_location = None self._task_location = None self._rest_request = rest_request self._http_response = http_response if http_response is not None: self._read = http_response.content self._status = http_response.status_code @property def read(self): """Property for accessing raw content as an array of bytes (unless overridden) TODO: Need to review usage elsewhwere; by default _read is an array of bytes, but applying a new value with a setter routine will make it a string. We might want to consider deprecating the setters. """ return self._read @read.setter def read(self, read): """Property for setting _read :param read: The data to set to read. :type read: str. """ if read is not None: if isinstance(read, dict): read = json.dumps(read, indent=4) self._read = read def getheaders(self): """Property for accessing the headers""" # 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 :param name: The header name to retrieve. :type name: str. :returns: returns a header from HTTP response """ return self._http_response.headers.get(name.lower(), None) def json(self, newdict): """Property for setting JSON data :param newdict: The string data to set as JSON data. :type newdict: str. """ self._read = json.dumps(newdict, indent=4) @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 @text.setter def text(self, value): """Property for setting text unparsed data :param value: The unparsed data to set as text. :type value: str. """ self.read = value @property def dict(self): """Property for accessing the data as an dict""" try: return json.loads(self.text) except: str = "Service responded with invalid JSON at URI {}\n{}".format( self._rest_request.path, self.text) LOGGER.error(str) raise JsonDecodingError(str) from None @property def obj(self): """Property for accessing the data as an object""" return RisObject.parse(self.dict) @property def status(self): """Property for accessing the status code""" return self._status @property def session_key(self): """Property for accessing the saved session key""" if self._session_key: return self._session_key self._session_key = self.getheader('x-auth-token') return self._session_key @property def session_location(self): """Property for accessing the saved session location""" if self._session_location: return self._session_location self._session_location = self.getheader('location') return self._session_location @property def task_location(self): """Return if we're a PATCH/POST in with a task link """ if self._task_location: return self._task_location self._task_location = self.getheader('location') return self._task_location @property def is_processing(self): """Check if we're a PATCH/POST in progress """ return self.status == 202 @property def retry_after(self): """Retry After header""" 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""" my_href = self.task_location if self.is_processing: if my_href: my_content = context.get(my_href, None) return my_content elif my_href is None: raise ValueError('We are processing a 202, but provide no location') return self @property def request(self): """Property for accessing the saved http request""" return self._rest_request def __str__(self): """Class string formatter""" headerstr = '' for header in self.getheaders(): headerstr += '%s %s\n' % (header[0], header[1]) return "%(status)s\n%(headerstr)s\n\n%(body)s" % \ {'status': self.status, 'headerstr': headerstr, 'body': self.text} class JSONEncoder(json.JSONEncoder): """JSON Encoder class""" def default(self, obj): """Set defaults in JSON encoder class :param obj: object to be encoded into JSON. :type obj: RestResponse object. :returns: returns a JSON ordered dict """ if isinstance(obj, RestResponse): jsondict = OrderedDict() jsondict['Status'] = obj.status jsondict['Headers'] = list() for hdr in obj.getheaders(): headerd = dict() headerd[hdr[0]] = hdr[1] jsondict['Headers'].append(headerd) if obj.text: jsondict['Content'] = obj.dict return jsondict return json.JSONEncoder.default(self, obj) class JSONDecoder(json.JSONDecoder): """Custom JSONDecoder that understands our types""" def decode(self, json_string): """Decode JSON string :param json_string: The JSON string to be decoded into usable data. :type json_string: str. :returns: returns a parsed dict """ parsed_dict = super(JSONDecoder, self).decode(json_string) return parsed_dict class StaticRestResponse(RestResponse): """A RestResponse object used when data is being cached.""" def __init__(self, **kwargs): restreq = None if 'restreq' in kwargs: restreq = kwargs['restreq'] super(StaticRestResponse, self).__init__(restreq, None) if 'Status' in kwargs: self._status = kwargs['Status'] if 'Headers' in kwargs: self._headers = kwargs['Headers'] if 'session_key' in kwargs: self._session_key = kwargs['session_key'] if 'session_location' in kwargs: self._session_location = kwargs['session_location'] if 'Content' in kwargs: content = kwargs['Content'] if isinstance(content, str): self._read = content else: self._read = json.dumps(content) else: self._read = '' def getheaders(self): """Function for accessing the headers""" returnlist = list() if isinstance(self._headers, dict): for key, value in self._headers.items(): returnlist.append((key, value)) else: for item in self._headers: returnlist.append(item) return returnlist def getheader(self, name): """Property for accessing an individual header :param name: The header name to retrieve. :type name: str. :returns: returns a header from HTTP response """ returnheader = None if isinstance(self._headers, dict): for key, value in self._headers.items(): if key.lower() == name.lower(): returnheader = self._headers[key] break else: for item in self._headers: if item[0].lower() == name.lower(): returnheader = item[1] break return returnheader class AuthMethod(object): """AUTH Method class""" BASIC = 'basic' SESSION = 'session' class RestClientBase(object): """Base class for RestClients""" def __init__(self, base_url, username=None, password=None, default_prefix='/redfish/v1/', sessionkey=None, capath=None, cafile=None, timeout=None, max_retry=None, proxies=None, check_connectivity=True): """Initialization of the base class RestClientBase :param base_url: The URL of the remote system :type base_url: str :param username: The user name used for authentication :type username: str :param password: The password used for authentication :type password: str :param default_prefix: The default root point :type default_prefix: str :param sessionkey: session key for the current login of base_url :type sessionkey: str :param capath: Path to a directory containing CA certificates :type capath: str :param cafile: Path to a file of CA certs :type cafile: str :param timeout: Timeout in seconds for the initial connection :type timeout: int :param max_retry: Number of times a request will retry after a timeout :type max_retry: int :param proxies: Dictionary containing protocol to proxy URL mappings :type proxies: dict :param check_connectivity: A boolean to determine whether the client immediately checks for connectivity to the base_url or not. :type check_connectivity: bool """ self.__base_url = base_url.rstrip('/') self.__username = username self.__password = password self.__session_key = sessionkey self.__authorization_key = None self.__session_location = None if self.__base_url.startswith('http+unix://'): self._session = requests_unixsocket.Session() else: self._session = requests.Session() self._timeout = timeout self._max_retry = max_retry if max_retry is not None else 10 self._proxies = proxies self.login_url = None self.default_prefix = default_prefix self.capath = capath self.cafile = cafile if check_connectivity: self.get_root_object() def __enter__(self): self.login() return self def __exit__(self, exc_type, exc_value, exc_traceback): self.logout() def get_username(self): """Return used user name""" return self.__username def set_username(self, username): """Set user name :param username: The user name to be set. :type username: str """ self.__username = username def get_password(self): """Return used password""" return self.__password def set_password(self, password): """Set password :param password: The password to be set. :type password: str """ self.__password = password def get_base_url(self): """Return used URL""" return self.__base_url def set_base_url(self, url): """Set based URL :param url: The URL to be set. :type url: str """ self.__base_url = url.rstrip('/') def get_session_key(self): """Return session key""" return self.__session_key def set_session_key(self, session_key): """Set session key :param session_key: The session_key to be set. :type session_key: str """ self.__session_key = session_key def get_session_location(self): """Return session location""" return self.__session_location def set_session_location(self, session_location): """Set session location :param session_location: The session_location to be set. :type session_location: str """ self.__session_location = session_location def get_authorization_key(self): """Return authorization key""" return self.__authorization_key def set_authorization_key(self, authorization_key): """Set authorization key :param authorization_key: The authorization_key to be set. :type authorization_key: str """ self.__authorization_key = authorization_key def get_root_object(self): """Perform an initial get and store the result""" try: resp = self.get(self.default_prefix) except Exception as excp: raise excp if resp.status == 401 and self.__authorization_key is None and self.__session_key is None: # Workaround where the service incorrectly rejects access to service # root when no credentials are provided warnings.warn("Service incorrectly responded with HTTP 401 Unauthorized for the service root. Contact your vendor.") self.root = {} self.root_resp = resp return if resp.status != 200: raise ServerDownOrUnreachableError("Server not reachable, " \ "return code: %d" % resp.status,response=resp) content = resp.text try: root_data = json.loads(content) except: str = 'Service responded with invalid JSON at URI {}\n{}'.format( self.default_prefix, content) LOGGER.error(str) raise JsonDecodingError(str) from None self.root = RisObject.parse(root_data) self.root_resp = resp def get(self, path, args=None, headers=None, timeout=None, max_retry=None): """Perform a GET request :param path: The URI to access :type path: str :param args: The query parameters to provide with the request :type args: dict, optional :param headers: Additional HTTP headers to provide in the request :type headers: dict, optional :param timeout: Timeout in seconds for the initial connection for this specific request :type timeout: int, optional :param max_retry: Number of times a request will retry after a timeout for this specific request :type max_retry: int, optional :returns: returns a rest request with method 'Get' """ try: return self._rest_request(path, method='GET', args=args, headers=headers, timeout=timeout, max_retry=max_retry) except ValueError: str = "Service responded with invalid JSON at URI {}".format(path) LOGGER.error(str) raise JsonDecodingError(str) from None def head(self, path, args=None, headers=None, timeout=None, max_retry=None): """Perform a HEAD request :param path: The URI to access :type path: str :param args: The query parameters to provide with the request :type args: dict, optional :param headers: Additional HTTP headers to provide in the request :type headers: dict, optional :param timeout: Timeout in seconds for the initial connection for this specific request :type timeout: int, optional :param max_retry: Number of times a request will retry after a timeout for this specific request :type max_retry: int, optional :returns: returns a rest request with method 'Head' """ return self._rest_request(path, method='HEAD', args=args, headers=headers, timeout=timeout, max_retry=max_retry) def post(self, path, args=None, body=None, headers=None, timeout=None, max_retry=None): """Perform a POST request :param path: The URI to access :type path: str :param args: The query parameters to provide with the request :type args: dict, optional :param body: The request body to provide; use a dict for a JSON body, list for multipart forms, bytes for an octet stream, or str for an unstructured request :type body: dict or list or bytes or str, optional :param headers: Additional HTTP headers to provide in the request :type headers: dict, optional :param timeout: Timeout in seconds for the initial connection for this specific request :type timeout: int, optional :param max_retry: Number of times a request will retry after a timeout for this specific request :type max_retry: int, optional :returns: returns a rest request with method 'Post' """ return self._rest_request(path, method='POST', args=args, body=body, headers=headers, timeout=timeout, max_retry=max_retry) def put(self, path, args=None, body=None, headers=None, timeout=None, max_retry=None): """Perform a PUT request :param path: The URI to access :type path: str :param args: The query parameters to provide with the request :type args: dict, optional :param body: The request body to provide; use a dict for a JSON body, list for multipart forms, bytes for an octet stream, or str for an unstructured request :type body: dict or list or bytes or str, optional :param headers: Additional HTTP headers to provide in the request :type headers: dict, optional :param timeout: Timeout in seconds for the initial connection for this specific request :type timeout: int, optional :param max_retry: Number of times a request will retry after a timeout for this specific request :type max_retry: int, optional :returns: returns a rest request with method 'Put' """ return self._rest_request(path, method='PUT', args=args, body=body, headers=headers, timeout=timeout, max_retry=max_retry) def patch(self, path, args=None, body=None, headers=None, timeout=None, max_retry=None): """Perform a PATCH request :param path: The URI to access :type path: str :param args: The query parameters to provide with the request :type args: dict, optional :param body: The request body to provide; use a dict for a JSON body, list for multipart forms, bytes for an octet stream, or str for an unstructured request :type body: dict or list or bytes or str, optional :param headers: Additional HTTP headers to provide in the request :type headers: dict, optional :param timeout: Timeout in seconds for the initial connection for this specific request :type timeout: int, optional :param max_retry: Number of times a request will retry after a timeout for this specific request :type max_retry: int, optional :returns: returns a rest request with method 'Patch' """ return self._rest_request(path, method='PATCH', args=args, body=body, headers=headers, timeout=timeout, max_retry=max_retry) def delete(self, path, args=None, headers=None, timeout=None, max_retry=None): """Perform a DELETE request :param path: The URI to access :type path: str :param args: The query parameters to provide with the request :type args: dict, optional :param headers: Additional HTTP headers to provide in the request :type headers: dict, optional :param timeout: Timeout in seconds for the initial connection for this specific request :type timeout: int, optional :param max_retry: Number of times a request will retry after a timeout for this specific request :type max_retry: int, optional :returns: returns a rest request with method 'Delete' """ return self._rest_request(path, method='DELETE', args=args, headers=headers, timeout=timeout, max_retry=max_retry) def _get_req_headers(self, headers=None): """Get the request headers :param headers: additional headers to be utilized :type headers: dict :returns: returns headers """ headers = headers if isinstance(headers, dict) else dict() if self.__session_key: headers['X-Auth-Token'] = self.__session_key elif self.__authorization_key: headers['Authorization'] = self.__authorization_key headers_keys = set(k.lower() for k in headers) if 'accept' not in headers_keys: headers['Accept'] = '*/*' return headers def _rest_request(self, path, method='GET', args=None, body=None, headers=None, allow_redirects=True, timeout=None, max_retry=None): """Rest request main function :param path: The URI to access :type path: str :param method: The HTTP method to invoke on the URI; GET if not provided :type method: str, optional :param args: The query parameters to provide with the request :type args: dict, optional :param body: The request body to provide; use a dict for a JSON body, list for multipart forms, bytes for an octet stream, or str for an unstructured request :type body: dict or list or bytes or str, optional :param headers: Additional HTTP headers to provide in the request :type headers: dict, optional :param allow_redirects: Controls whether redirects are followed :type allow_redirects: bool, optional :param timeout: Timeout in seconds for the initial connection for this specific request :type timeout: int, optional :param max_retry: Number of times a request will retry after a timeout for this specific request :type max_retry: int, optional :returns: returns a RestResponse object """ if timeout is None: timeout = self._timeout if max_retry is None: max_retry = self._max_retry headers = self._get_req_headers(headers) reqpath = path.replace('//', '/') if body is not None: if isinstance(body, dict) or isinstance(body, list): if headers.get('Content-Type', None) == 'multipart/form-data': # Body contains part values, either as # - dict (where key is part name, and value is string) # - list of tuples (if the order is important) # - dict (where values are tuples as they would # be provided to requests' `files` parameter) # See https://toolbelt.readthedocs.io/en/latest/uploading-data.html#requests_toolbelt.multipart.encoder.MultipartEncoder # # Redfish specification requires two parts: # (1) UpdateParameters (JSON formatted, # adhering to the UpdateService Schema) # (2) UpdateFile (binary file to use for this update) # # The third part is optional: OemXXX encoder = MultipartEncoder(body) body = encoder.to_string() # Overwrite Content-Type, because we have to include # the boundary that the encoder generated. # Will be of the form: "multipart/form-data; boundary=abc' # where the boundary value is a UUID. headers['Content-Type'] = encoder.content_type else: headers['Content-Type'] = 'application/json' body = json.dumps(body) elif isinstance(body, bytes): headers['Content-Type'] = 'application/octet-stream' body = body else: headers['Content-Type'] = 'application/x-www-form-urlencoded' body = urlencode(body) if method == 'PUT': resp = self._rest_request(path=path, timeout=timeout, max_retry=max_retry) try: if resp.getheader('content-encoding') == 'gzip': buf = StringIO() gfile = gzip.GzipFile(mode='wb', fileobj=buf) try: gfile.write(str(body)) finally: gfile.close() compresseddata = buf.getvalue() if compresseddata: data = bytearray() data.extend(memoryview(compresseddata)) body = data except BaseException as excp: LOGGER.error('Error occur while compressing body: %s', excp) raise query_str = None if args: if method == 'GET': # 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: if args[query] is None: none_list.append(query) else: args_copy[query] = args[query] query_str = urlencode(args_copy, quote_via=quote, safe="/?:!$'()*+,;\\=") for query in none_list: if len(query_str) == 0: query_str += query else: query_str += '&' + query elif method == 'PUT' or method == 'POST' or method == 'PATCH': LOGGER.warning('For POST, PUT and PATCH methods, the provided "args" parameter "{}" is ignored.' .format(args)) if not body: LOGGER.warning('Use the "body" parameter to supply the request payload.') restreq = RestRequest(reqpath, method=method, body=body) attempts = 0 restresp = None cause_exception = None while attempts <= max_retry: if LOGGER.isEnabledFor(logging.DEBUG): headerstr = '' if headers is not None: for header in headers: if header.lower() == "authorization": headerstr += '\t{}: \n'.format(header) else: headerstr += '\t{}: {}\n'.format(header, headers[header]) try: logbody = 'No request body' if restreq.body: if restreq.body[0] == '{': # Mask password properties # NOTE: If the password itself contains a double quote, it will not redact the entire password logbody = re.sub(r'"Password"\s*:\s*".*?"', '"Password": ""', restreq.body) else: raise ValueError('Body of message is binary') LOGGER.debug('HTTP REQUEST (%s) for %s:\nHeaders:\n%s\nBody: %s\n'% \ (restreq.method, restreq.path, headerstr, logbody)) except: LOGGER.debug('HTTP REQUEST (%s) for %s:\nHeaders:\n%s\nBody: %s\n'% \ (restreq.method, restreq.path, headerstr, 'binary body')) attempts = attempts + 1 LOGGER.info('Attempt %s of %s', attempts, path) try: if sys.version_info < (3, 3): inittime = time.clock() else: inittime = time.perf_counter() # 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=timeout, allow_redirects=allow_redirects, verify=verify, proxies=self._proxies, params=query_str) 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) except Exception as excp: if not cause_exception: cause_exception = excp LOGGER.info('Retrying %s [%s]'% (path, excp)) time.sleep(1) continue else: break if attempts <= self._max_retry: if LOGGER.isEnabledFor(logging.DEBUG): headerstr = '' if restresp is not None: for header in restresp.getheaders(): headerstr += '\t' + header[0] + ': ' + header[1] + '\n' try: try: restrespstr = json.dumps(json.loads(restresp.read), indent=4) except: restrespstr = restresp.read LOGGER.debug('HTTP RESPONSE for %s:\nCode: %s\n\nHeaders:\n' \ '%s\nBody Response of %s:\n%s\n'%\ (restresp.request.path, str(restresp._http_response.status_code)+ ' ' + \ restresp._http_response.reason, headerstr, restresp.request.path, restrespstr)) except: LOGGER.debug('HTTP RESPONSE:\nCode:%s', restresp) else: LOGGER.debug('HTTP RESPONSE: ') return restresp else: raise RetriesExhaustedError() from cause_exception def login(self, username=None, password=None, auth=AuthMethod.SESSION): """Login and start a REST session. Remember to call logout() when""" """ you are done. :param username: the user name. :type username: str. :param password: the password. :type password: str. :param auth: authentication method :type auth: object/instance of class AuthMethod """ if getattr(self, "root_resp", None) is None: self.get_root_object() self.__username = username if username else self.__username self.__password = password if password else self.__password if auth == AuthMethod.BASIC: auth_key = base64.b64encode(('%s:%s' % (self.__username, self.__password)).encode('utf-8')).decode('utf-8') self.__authorization_key = 'Basic %s' % auth_key headers = dict() headers['Authorization'] = self.__authorization_key respvalidate = self._rest_request(self.login_url, headers=headers) if respvalidate.status == 401: # Invalid credentials supplied raise InvalidCredentialsError('HTTP 401 Unauthorized returned: Invalid credentials supplied') elif auth == AuthMethod.SESSION: data = dict() data['UserName'] = self.__username data['Password'] = self.__password headers = dict() resp = self._rest_request(self.login_url, method="POST",body=data, headers=headers, allow_redirects=False) LOGGER.info('Login returned code %s: %s', resp.status, resp.text) self.__session_key = resp.session_key self.__session_location = resp.session_location message_item = search_message(resp, "Base", "PasswordChangeRequired") if not message_item is None: raise RedfishPasswordChangeRequiredError("Password Change Required\n", message_item["MessageArgs"][0]) if not self.__session_key and resp.status not in [200, 201, 202, 204]: if resp.status == 401: # Invalid credentials supplied raise InvalidCredentialsError('HTTP 401 Unauthorized returned: Invalid credentials supplied') else: # Other type of error during session creation error_str = resp.text try: error_str = resp.dict["error"]["@Message.ExtendedInfo"][0]["Message"] except: try: error_str = resp.dict["error"]["message"] except: pass raise SessionCreationError('HTTP {}: Failed to created the session\n{}'.format(resp.status, error_str)) else: pass def logout(self): """ Logout of session. YOU MUST CALL THIS WHEN YOU ARE DONE TO FREE""" """ UP SESSIONS""" if self.__session_key: session_loc = urlparse(self.__session_location).path resp = self.delete(session_loc) if resp.status not in [200, 202, 204]: raise BadRequestError("Invalid session resource: %s, "\ "return code: %d" % (session_loc, resp.status)) LOGGER.info("User logged out: %s", resp.text) self.__session_key = None self.__session_location = None self.__authorization_key = None self._session.close() class HttpClient(RestClientBase): """A client for Rest""" def __init__(self, base_url, username=None, password=None, default_prefix='/redfish/v1/', sessionkey=None, capath=None, cafile=None, timeout=None, max_retry=None, proxies=None, check_connectivity=True): """Initialize HttpClient :param base_url: The url of the remote system :type base_url: str :param username: The user name used for authentication :type username: str :param password: The password used for authentication :type password: str :param default_prefix: The default root point :type default_prefix: str :param sessionkey: session key for the current login of base_url :type sessionkey: str :param capath: Path to a directory containing CA certificates :type capath: str :param cafile: Path to a file of CA certs :type cafile: str :param timeout: Timeout in seconds for the initial connection :type timeout: int :param max_retry: Number of times a request will retry after a timeout :type max_retry: int :param proxies: Dictionary containing protocol to proxy URL mappings :type proxies: dict :param check_connectivity: A boolean to determine whether the client immediately checks for connectivity to the base_url or not. :type check_connectivity: bool """ super(HttpClient, self).__init__(base_url, username=username, password=password, default_prefix=default_prefix, sessionkey=sessionkey, capath=capath, cafile=cafile, timeout=timeout, max_retry=max_retry, proxies=proxies, check_connectivity=check_connectivity) try: self.login_url = self.root.Links.Sessions['@odata.id'] except (KeyError, AttributeError): # While the "Links/Sessions" property is required, we can fallback # on the URI hardened in 1.6.0 of the specification if not found LOGGER.debug('"Links/Sessions" not found in Service Root.') self.login_url = '/redfish/v1/SessionService/Sessions' def _rest_request(self, path='', method="GET", args=None, body=None, headers=None, allow_redirects=True, timeout=None, max_retry=None): """Rest request for HTTP client :param path: path within tree :type path: str :param method: method to be implemented :type method: str :param args: the arguments for method :type args: dict :param body: body payload for the rest call :type body: dict :param headers: provide additional headers :type headers: dict :param allow_redirects: controls whether redirects are followed :type allow_redirects: bool :param timeout: Timeout in seconds for the initial connection for this specific request :type timeout: int :param max_retry: Number of times a request will retry after a timeout for this specific request :type max_retry: int :returns: returns a rest request """ return super(HttpClient, self)._rest_request(path=path, method=method, args=args, body=body, headers=headers, allow_redirects=allow_redirects, timeout=timeout, max_retry=max_retry) def _get_req_headers(self, headers=None, providerheader=None): """Get the request headers for HTTP client :param headers: additional headers to be utilized :type headers: dict :returns: returns request headers """ headers = super(HttpClient, self)._get_req_headers(headers) headers_keys = set(k.lower() for k in headers) if 'odata-version' not in headers_keys: headers['OData-Version'] = '4.0' return headers def redfish_client(base_url=None, username=None, password=None, default_prefix='/redfish/v1/', sessionkey=None, capath=None, cafile=None, timeout=None, max_retry=None, proxies=None, check_connectivity=True): """Create and return appropriate REDFISH client instance.""" """ Instantiates appropriate Redfish object based on existing""" """ configuration. Use this to retrieve a pre-configured Redfish object :param base_url: rest host or ip address. :type base_url: str. :param username: user name required to login to server :type: str :param password: password credentials required to login :type password: str :param default_prefix: default root to extract tree :type default_prefix: str :param sessionkey: session key credential for current login :type sessionkey: str :param capath: Path to a directory containing CA certificates :type capath: str :param cafile: Path to a file of CA certs :type cafile: str :param timeout: Timeout in seconds for the initial connection :type timeout: int :param max_retry: Number of times a request will retry after a timeout :type max_retry: int :param proxies: Dictionary containing protocol to proxy URL mappings :type proxies: dict :param check_connectivity: A boolean to determine whether the client immediately checks for connectivity to the base_url or not. :type check_connectivity: bool :returns: a client object. """ if "://" not in base_url: warnings.warn("Scheme not specified for '{}'; adding 'https://'".format(base_url)) base_url = "https://" + base_url return HttpClient(base_url=base_url, username=username, password=password, default_prefix=default_prefix, sessionkey=sessionkey, capath=capath, cafile=cafile, timeout=timeout, max_retry=max_retry, proxies=proxies, check_connectivity=check_connectivity) python-redfish-3.2.7/src/redfish/ris/000077500000000000000000000000001473240251500175015ustar00rootroot00000000000000python-redfish-3.2.7/src/redfish/ris/__init__.py000066400000000000000000000014051473240251500216120ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md # -*- coding: utf-8 -*- """ RIS implementation """ from .sharedtypes import ( JSONEncoder ) from .ris import ( RisMonolithMemberBase, RisMonolithMember_v1_0_0, RisMonolith_v1_0_0, RisMonolith, ) from .rmc_helper import ( UndefinedClientError, InstanceNotFoundError, CurrentlyLoggedInError, NothingSelectedError, NothingSelectedSetError, InvalidSelectionError, SessionExpired, RmcClient, RmcConfig, RmcCacheManager, RmcFileCacheManager, ) from .rmc import ( RmcApp ) python-redfish-3.2.7/src/redfish/ris/config.py000066400000000000000000000105761473240251500213310ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md # -*- coding: utf-8 -*- """Module for working with global configuration options.""" #---------Imports--------- import os import re import logging import configparser #---------End of imports--------- #---------Debug logger--------- LOGGER = logging.getLogger(__name__) #---------End of debug logger--------- class AutoConfigParser(object): """Auto configuration parser""" # properties starting with _ac__ are automatically # serialized to config file _config_pattern = re.compile(r'_ac__(?P.*)') def __init__(self, filename=None): """Initialize AutoConfigParser :param filename: file name to be used for config loading. :type filename: str. """ self._sectionname = 'globals' self._configfile = filename def _get_ac_keys(self): """Retrieve parse option keys""" result = [] for key in self.__dict__: match = AutoConfigParser._config_pattern.search(key) if match: result.append(match.group('confkey')) return result def _get(self, key): """Retrieve parse option key :param key: key to retrieve. :type key: str. """ ackey = '_ac__%s' % key.replace('-', '_') if ackey in self.__dict__: return self.__dict__[ackey] return None def _set(self, key, value): """Set parse option key :param key: key to be set. :type key: str. :param value: value to be given to key. :type value: str. """ ackey = '_ac__%s' % key.replace('-', '_') if ackey in self.__dict__: self.__dict__[ackey] = value return None def load(self, filename=None): """Load configuration settings from the file filename, if filename""" """ is None then the value from get_configfile() is used :param filename: file name to be used for config loading. :type filename: str. """ fname = self.get_configfile() if filename is not None and len(filename) > 0: fname = filename if fname is None or not os.path.isfile(fname): return try: config = configparser.RawConfigParser() config.read(fname) for key in self._get_ac_keys(): configval = None try: configval = config.get(self._sectionname, key) except configparser.NoOptionError: # also try with - instead of _ try: configval = config.get(self._sectionname, key.replace('_', '-')) except configparser.NoOptionError: pass if configval is not None and len(configval) > 0: ackey = '_ac__%s' % key self.__dict__[ackey] = configval except configparser.NoOptionError: pass except configparser.NoSectionError: pass def save(self, filename=None): """Save configuration settings from the file filename, if filename""" """ is None then the value from get_configfile() is used :param filename: file name to be used for config saving. :type filename: str. """ fname = self.get_configfile() if filename is not None and len(filename) > 0: fname = filename if fname is None or len(fname) == 0: return config = configparser.RawConfigParser() try: config.add_section(self._sectionname) except configparser.DuplicateSectionError: pass # ignored for key in self._get_ac_keys(): ackey = '_ac__%s' % key config.set(self._sectionname, key, str(self.__dict__[ackey])) fileh = open(self._configfile, 'w') config.write(fileh) fileh.close() def get_configfile(self): """ The current configuration file location""" return self._configfile python-redfish-3.2.7/src/redfish/ris/ris.py000066400000000000000000000512211473240251500206510ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md # -*- coding: utf-8 -*- """RIS implementation""" # ---------Imports--------- import abc import logging import sys import threading from queue import Queue from collections import OrderedDict import jsonpath_rw import redfish.rest.v1 from redfish.ris.sharedtypes import Dictable from urllib.parse import urlparse, urlunparse # ---------End of imports--------- # ---------Debug logger--------- LOGGER = logging.getLogger(__name__) # ---------End of debug logger--------- class SessionExpiredRis(Exception): """Raised when session has expired""" pass class RisMonolithMemberBase(Dictable): """RIS monolith member base class""" __metaclass__ = abc.ABCMeta pass class RisMonolithMember_v1_0_0(RisMonolithMemberBase): """Wrapper around RestResponse that adds the monolith data""" def __init__(self, restresp): self._resp = restresp self._patches = list() self._type = None self._typestring = "@odata.type" def _get_type(self): """Return type from monolith""" if self._typestring in self._resp.dict: return self._resp.dict[self._typestring] elif "type" in self._resp.dict: return self._resp.dict["type"] return None type = property(_get_type, None) def _get_maj_type(self): """Return maj type from monolith""" if self.type: return self.type[:-4] return None maj_type = property(_get_maj_type, None) def _get_resp(self): """Return resp from monolith""" return self._resp resp = property(_get_resp, None) def _get_patches(self): """Return patches from monolith""" return self._patches patches = property(_get_patches, None) def to_dict(self): """Convert monolith to dict""" result = OrderedDict() if self.type: result["Type"] = self.type if (self.maj_type == "Collection.1" and "MemberType" in self._resp.dict): result["MemberType"] = self._resp.dict["MemberType"] result["links"] = OrderedDict() result["links"]["href"] = "" headers = dict() for header in self._resp.getheaders(): headers[header[0]] = header[1] result["Headers"] = headers if "etag" in headers: result["ETag"] = headers["etag"] result["OriginalUri"] = self._resp.request.path result["Content"] = self._resp.dict result["Patches"] = self._patches return result def load_from_dict(self, src): """Load variables from dict monolith""" """ :param src: source to load from :type src: dict """ if "Type" in src: self._type = src["Type"] restreq = redfish.rest.v1.RestRequest(method="GET", path=src["OriginalUri"]) src["restreq"] = restreq self._resp = redfish.rest.v1.StaticRestResponse(**src) self._patches = src["Patches"] def _reducer(self, indict, breadcrumbs=None, outdict=OrderedDict()): """Monolith reducer :param indict: input dictionary. :type indict: dict. :param breadcrumbs: breadcrumbs from previous operations. :type breadcrumbs: dict. :param outdict: expected output format. :type outdict: dictionary type. :returns: returns outdict """ if breadcrumbs is None: breadcrumbs = [] if isinstance(indict, dict): for key, val in list(indict.items()): breadcrumbs.append(key) # push if isinstance(val, dict): self._reducer(val, breadcrumbs, outdict) elif isinstance(val, list) or isinstance(val, tuple): for i in range(0, len(val)): breadcrumbs.append("%s" % i) # push self._reducer(val[i], breadcrumbs, outdict) del breadcrumbs[-1] # pop elif isinstance(val, tuple): self._reducer(val, breadcrumbs, outdict) else: self._reducer(val, breadcrumbs, outdict) del breadcrumbs[-1] # pop else: outkey = "/".join(breadcrumbs) outdict[outkey] = indict return outdict def _jsonpath_reducer(self, indict, breadcrumbs=None, outdict=OrderedDict()): """JSON Path Reducer :param indict: input dictionary. :type indict: dict. :param breadcrumbs: breadcrumbs from previous operations. :type breadcrumbs: dict. :param outdict: expected output format. :type outdict: dictionary type. :returns: returns outdict """ if breadcrumbs is None: breadcrumbs = [] if isinstance(indict, dict): for key, val in list(indict.items()): breadcrumbs.append(key) # push if isinstance(val, dict): self._reducer(val, breadcrumbs, outdict) elif isinstance(val, list) or isinstance(val, tuple): for i in range(0, len(val)): breadcrumbs.append("[%s]" % i) # push self._reducer(val[i], breadcrumbs, outdict) del breadcrumbs[-1] # pop elif isinstance(val, tuple): self._reducer(val, breadcrumbs, outdict) else: self._reducer(val, breadcrumbs, outdict) del breadcrumbs[-1] # pop else: outkey = ".".join(breadcrumbs) outkey = outkey.replace(".[", "[") outdict[outkey] = indict return outdict def reduce(self): """Returns a "flatten" dict with nested data represented in""" """ JSONpath notation""" result = OrderedDict() if self.type: result["Type"] = self.type if (self.maj_type == "Collection.1" and "MemberType" in self._resp.dict): result["MemberType"] = self._resp.dict["MemberType"] self._reducer(self._resp.dict) result["OriginalUri"] = self._resp.request.path result["Content"] = self._reducer(self._resp.dict) return result class RisMonolith_v1_0_0(Dictable): """Monolithic cache of RIS data""" def __init__(self, client): """Initialize RisMonolith :param client: client to utilize :type client: RmcClient object """ self._client = client self.name = "Monolithic output of RIS Service" self.types = OrderedDict() self._visited_urls = list() self._current_location = "/" # "root" self.queue = Queue() self._type = None self._name = None self.progress = 0 self.reload = False self._typestring = "@odata.type" self._hrefstring = "@odata.id" def _get_type(self): """Return monolith version type""" return "Monolith.1.0.0" type = property(_get_type, None) def update_progress(self): """Simple function to increment the dot progress""" if self.progress % 6 == 0: sys.stdout.write(".") def get_visited_urls(self): """Return the visited URLS""" return self._visited_urls def set_visited_urls(self, visited_urls): """Set visited URLS to given list.""" self._visited_urls = visited_urls def load( self, path=None, includelogs=False, skipinit=False, skipcrawl=False, loadtype="href", loadcomplete=False, ): """Walk entire RIS model and cache all responses in self. :param path: path to start load from. :type path: str. :param includelogs: flag to determine if logs should be downloaded also. :type includelogs: boolean. :param skipinit: flag to determine if first run of load. :type skipinit: boolean. :param skipcrawl: flag to determine if load should traverse found links. :type skipcrawl: boolean. :param loadtype: flag to determine if load is meant for only href items. :type loadtype: str. :param loadcomplete: flag to download the entire monolith :type loadcomplete: boolean """ # noqa if not skipinit: if LOGGER.getEffectiveLevel() == 40: sys.stdout.write("Discovering data...") else: LOGGER.info("Discovering data...") self.name = self.name + " at %s" % self._client.base_url if not self.types: self.types = OrderedDict() if not threading.active_count() >= 6: for _ in range(5): workhand = SuperDuperWorker(self.queue) workhand.setDaemon(True) workhand.start() selectivepath = path if not selectivepath: selectivepath = self._client._rest_client.default_prefix self._load( selectivepath, skipcrawl=skipcrawl, includelogs=includelogs, skipinit=skipinit, loadtype=loadtype, loadcomplete=loadcomplete, ) self.queue.join() if not skipinit: if LOGGER.getEffectiveLevel() == 40: sys.stdout.write("Done\n") else: LOGGER.info("Done\n") def _load( self, path, skipcrawl=False, originaluri=None, includelogs=False, skipinit=False, loadtype="href", loadcomplete=False, ): """Helper function to main load function. :param path: path to start load from. :type path: str. :param skipcrawl: flag to determine if load should traverse found links. :type skipcrawl: boolean. :param originaluri: variable to assist in determining originating path. :type originaluri: str. :param includelogs: flag to determine if logs should be downloaded also. :type includelogs: boolean. :param skipinit: flag to determine if first run of load. :type skipinit: boolean. :param loadtype: flag to determine if load is meant for only href items. :type loadtype: str. :param loadcomplete: flag to download the entire monolith :type loadcomplete: boolean """ # noqa if path.endswith("?page=1"): return elif not includelogs: if "/Logs/" in path: return # catch any exceptions during URL parsing or GET requests try: # remove fragments newpath = urlparse(path) newpath = list(newpath[:]) newpath[-1] = "" path = urlunparse(tuple(newpath)) LOGGER.debug("_loading %s", path) if not self.reload: if path.lower() in self._visited_urls: return resp = self._client.get(path) if resp.status != 200: path = path + "/" resp = self._client.get(path) if resp.status == 401: raise SessionExpiredRis("Invalid session. Please logout and " "log back in or include credentials.") elif resp.status != 200: return except SessionExpiredRis: raise except Exception as e: cause = e.__cause__ if e.__cause__ else e LOGGER.error("Resource '{}' skipped due to exception: {}" .format(path, repr(cause))) return self.queue.put((resp, path, skipinit, self)) if loadtype == "href": # follow all the href attributes jsonpath_expr = jsonpath_rw.parse("$..'@odata.id'") matches = jsonpath_expr.find(resp.dict) if "links" in resp.dict and "NextPage" in resp.dict["links"]: if originaluri: next_link_uri = ( originaluri + "?page=" + str(resp.dict["links"]["NextPage"]["page"])) href = "%s" % next_link_uri self._load( href, originaluri=originaluri, includelogs=includelogs, skipcrawl=skipcrawl, skipinit=skipinit, ) else: next_link_uri = ( path + "?page=" + str(resp.dict["links"]["NextPage"]["page"])) href = "%s" % next_link_uri self._load( href, originaluri=path, includelogs=includelogs, skipcrawl=skipcrawl, skipinit=skipinit, ) if not skipcrawl: for match in matches: if (str(match.full_path) == "Registries.@odata.id" or str( match.full_path) == "JsonSchemas.@odata.id"): continue if match.value == path: continue href = "%s" % match.value self._load( href, skipcrawl=skipcrawl, originaluri=originaluri, includelogs=includelogs, skipinit=skipinit, ) if loadcomplete: for match in matches: self._load( match.value, skipcrawl=skipcrawl, originaluri=originaluri, includelogs=includelogs, skipinit=skipinit, ) def branch_worker(self, resp, path, skipinit): """Helper for load function, creates threaded worker :param resp: response received. :type resp: str. :param path: path correlating to the response. :type path: str. :param skipinit: flag to determine if progress bar should be updated. :type skipinit: boolean. """ self._visited_urls.append(path.lower()) member = RisMonolithMember_v1_0_0(resp) if not member.type: return self.update_member(member) if not skipinit: self.progress += 1 if LOGGER.getEffectiveLevel() == 40: self.update_progress() def update_member(self, member): """Adds member to this monolith. If the member already exists the""" """ data is updated in place. :param member: Ris monolith member object made by branch worker. :type member: RisMonolithMember_v1_0_0. """ if member.maj_type not in self.types: self.types[member.maj_type] = OrderedDict() self.types[member.maj_type]["Instances"] = list() found = False for indices in range(len(self.types[member.maj_type]["Instances"])): inst = self.types[member.maj_type]["Instances"][indices] if inst.resp.request.path == member.resp.request.path: self.types[member.maj_type]["Instances"][indices] = member self.types[ member.maj_type]["Instances"][indices].patches.extend( [patch for patch in inst.patches]) found = True break if not found: self.types[member.maj_type]["Instances"].append(member) def load_from_dict(self, src): """Load data to monolith from dict :param src: data receive from rest operation. :type src: str. """ self._type = src["Type"] self._name = src["Name"] self.types = OrderedDict() for typ in src["Types"]: for inst in typ["Instances"]: member = RisMonolithMember_v1_0_0(None) member.load_from_dict(inst) self.update_member(member) return def to_dict(self): """Convert data to monolith from dict""" result = OrderedDict() result["Type"] = self.type result["Name"] = self.name types_list = list() for typ in list(self.types.keys()): type_entry = OrderedDict() type_entry["Type"] = typ type_entry["Instances"] = list() for inst in self.types[typ]["Instances"]: type_entry["Instances"].append(inst.to_dict()) types_list.append(type_entry) result["Types"] = types_list return result def reduce(self): """Reduce monolith data""" result = OrderedDict() result["Type"] = self.type result["Name"] = self.name types_list = list() for typ in list(self.types.keys()): type_entry = OrderedDict() type_entry["Type"] = typ for inst in self.types[typ]["Instances"]: type_entry["Instances"] = inst.reduce() types_list.append(type_entry) result["Types"] = types_list return result def _jsonpath2jsonpointer(self, instr): """Convert json path to json pointer :param instr: input path to be converted to pointer. :type instr: str. """ outstr = instr.replace(".[", "[") outstr = outstr.replace("[", "/") outstr = outstr.replace("]", "/") if outstr.endswith("/"): outstr = outstr[:-1] return outstr def _get_current_location(self): """Return current location""" return self._current_location def _set_current_location(self, newval): """Set current location""" self._current_location = newval location = property(_get_current_location, _set_current_location) def list(self, lspath=None): """Function for list command :param lspath: path list. :type lspath: list. """ results = list() path_parts = ["Types"] # Types is always assumed if isinstance(lspath, list) and len(lspath) > 0: lspath = lspath[0] path_parts.extend(lspath.split("/")) elif not lspath: lspath = "/" else: path_parts.extend(lspath.split("/")) currpos = self.to_dict() for path_part in path_parts: if not path_part: continue if isinstance(currpos, RisMonolithMember_v1_0_0): break elif isinstance(currpos, dict) and path_part in currpos: currpos = currpos[path_part] elif isinstance(currpos, list): for positem in currpos: if "Type" in positem and path_part == positem["Type"]: currpos = positem break results.append(currpos) return results def killthreads(self): """Function to kill threads on logout""" threads = [] for thread in threading.enumerate(): if isinstance(thread, SuperDuperWorker): self.queue.put(("KILL", "KILL", "KILL", "KILL")) threads.append(thread) for thread in threads: thread.join() class RisMonolith(RisMonolith_v1_0_0): """Latest implementation of RisMonolith""" def __init__(self, client): """Initialize Latest RisMonolith :param client: client to utilize :type client: RmcClient object """ super(RisMonolith, self).__init__(client) class SuperDuperWorker(threading.Thread): """Recursive worker implementation""" def __init__(self, queue): """Initialize SuperDuperWorker :param queue: queue for worker :type queue: Queue object """ threading.Thread.__init__(self) self.queue = queue def run(self): """Thread creator""" while True: (resp, path, skipinit, thobj) = self.queue.get() if (resp == "KILL" and path == "KILL" and skipinit == "KILL" and thobj == "KILL"): break thobj.branch_worker(resp, path, skipinit) self.queue.task_done() python-redfish-3.2.7/src/redfish/ris/rmc.py000066400000000000000000002037101473240251500206370ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md # -*- coding: utf-8 -*- """RMC implementation """ #---------Imports--------- import os import re import sys import six import time import copy import shutil import logging from collections import OrderedDict from collections.abc import Mapping import jsonpatch import jsonpath_rw import jsonpointer from redfish.ris.rmc_helper import (UndefinedClientError, \ InstanceNotFoundError, CurrentlyLoggedInError, \ NothingSelectedError, InvalidSelectionError, \ RmcClient, RmcConfig, RmcFileCacheManager, \ NothingSelectedSetError, LoadSkipSettingError, \ InvalidCommandLineError, FailureDuringCommitError, \ SessionExpired) #---------End of imports--------- #---------Debug logger--------- LOGGER = logging.getLogger(__name__) #---------End of debug logger--------- class RmcApp(object): """Application level implementation of RMC""" def __init__(self, Args=None): """Initialize RmcApp :param Args: arguments to be passed to RmcApp :type Args: str """ self._rmc_clients = [] configfile = None self.logger = LOGGER foundsomething = False for item in Args: if foundsomething: configfile = item break if item == "-c": foundsomething = True elif item.startswith("--config="): configfile = item.split("=", 1)[1] break elif item == "--config": foundsomething = True # use the default config file if configfile is None: if os.name == 'nt': configfile = os.path.join(os.path.dirname(sys.executable), 'redfish.conf') else: configfile = '/etc/redfish/redfish.conf' if not os.path.isfile(configfile): self.warn("Config file '%s' not found\n\n" % configfile) self._config = RmcConfig() self.config_file = configfile self._cm = RmcFileCacheManager(self) self._monolith = None if not "--showwarnings" in Args: self.logger.setLevel(logging.WARNING) if self.logger.handlers and self.logger.handlers[0].name == 'lerr': self.logger.handlers.remove(self.logger.handlers[0]) def restore(self): """Restore monolith from cache""" self._cm.uncache_rmc() def deletelogoutfunction(self, url=None): """Wrapper function for logout helper function :param url: The URL to perform a logout request on. :type url: str. """ return self._cm.logout_del_function(url) def save(self): """Cache current monolith build""" self._cm.cache_rmc() def out(self): """Helper function for runtime error""" raise RuntimeError("You must override this method in your derived" \ " class") def err(self, msg, inner_except=None): """Helper function for runtime error :param msg: The error message. :type msg: str. :param inner_except: The internal exception. :type inner_except: str. """ LOGGER.error(msg) if inner_except is not None: LOGGER.error(inner_except) def warning_handler(self, msg): """Helper function for handling warning messages appropriately :param msg: The warning message. :type msg: str. """ if LOGGER.getEffectiveLevel() == 40: sys.stderr.write(msg) else: LOGGER.warning(msg) def warn(self, msg, inner_except=None): """Helper function for runtime warning :param msg: The warning message. :type msg: str. :param inner_except: The internal exception. :type inner_except: str. """ LOGGER.warning(msg) if inner_except is not None: LOGGER.warning(inner_except) def get_config(self): """Return config""" return self._config config = property(get_config, None) def get_cache(self): """Return config""" return self._config config = property(get_cache, None) def config_from_file(self, filename): """Get config from file :param filename: The config file name. :type filename: str. """ self._config = RmcConfig(filename=filename) self._config.load() def add_rmc_client(self, client): """Add new RMC client :param client: The client to be added. :type client: str. """ for i in range(0, len(self._rmc_clients)): if client.get_base_url() == self._rmc_clients[i].get_base_url(): self._rmc_clients[i] = client return # not found so add it self._rmc_clients.append(client) def remove_rmc_client(self, url=None): """Remove RMC client :param url: The URL to perform the removal to. :type url: str. """ if url: for i in range(0, len(self._rmc_clients)): if url in self._rmc_clients[i].get_base_url(): del self._rmc_clients[i] else: if self._rmc_clients and len(self._rmc_clients) > 0: self._rmc_clients = self._rmc_clients[:-1] def get_rmc_client(self, url): """Return rmc_client with the provided URL. :param url: The URL of the client you are searching for. :type url: str. """ for i in range(0, len(self._rmc_clients)): if url == self._rmc_clients[i].get_base_url(): return self._rmc_clients[i] return None def check_current_rmc_client(self, url): """Return if RMC client already exists :param url: The URL to perform a check on. :type url: str. """ if not len(self._rmc_clients): return True for i in range(0, len(self._rmc_clients)): if url == self._rmc_clients[i].get_base_url(): return True return False def update_rmc_client(self, url, **kwargs): """Do update to passed client :param url: The URL for the update request. :type url: str. """ for i in range(0, len(self._rmc_clients)): if url == self._rmc_clients[i].get_base_url(): if 'username' in kwargs: self._rmc_clients[i].set_username(kwargs['username']) if 'password' in kwargs: self._rmc_clients[i].set_password(kwargs['password']) def get_current_client(self): """Get the current client""" if len(self._rmc_clients) > 0: return self._rmc_clients[-1] raise UndefinedClientError() current_client = property(get_current_client, None) def login(self, username=None, password=None, base_url=None, verbose=False, path=None, skipbuild=False, includelogs=False, proxies=None): """Main worker function for login command :param username: user name required to login to server. :type: str. :param password: password credentials required to login. :type password: str. :param base_url: redfish host name or ip address. :type base_url: str. :param verbose: flag to determine additional output. :type verbose: boolean. :param path: path to initiate login to. :type path: str. :param skipbuild: flag to determine whether to start monolith download. :type skipbuild: boolean. :param includelogs: flag to determine id logs should be downloaded. :type includelogs: boolean. :param proxies: Dictionary containing protocol to proxy URL mappings :type proxies: dict """ if not self.check_current_rmc_client(url=base_url): raise CurrentlyLoggedInError("Currently logged into another " "server. \nPlease log out out first " "before logging in to another.") existing_client = self.get_rmc_client(url=base_url) if existing_client: self.update_rmc_client(url=base_url, username=username, password=password) else: try: self.add_rmc_client(RmcClient(username=username, password=password, url=base_url, proxies=proxies)) except Exception as excp: raise excp try: self.current_client.login() except Exception as excp: raise excp if not skipbuild: self.build_monolith(verbose=verbose, path=path, includelogs=includelogs) self.save() def build_monolith(self, verbose=False, path=None, includelogs=False): """Run through the RIS tree to build monolith :param verbose: flag to determine additional output. :type verbose: boolean. :param path: path to initiate login to. :type path: str. :param includelogs: flag to determine id logs should be downloaded. :type includelogs: boolean. """ monolith = self.current_client.monolith if sys.version_info < (3, 3): inittime = time.clock() else: inittime = time.perf_counter() monolith.load(path=path, includelogs=includelogs) if sys.version_info < (3, 3): endtime = time.clock() else: endtime = time.perf_counter() if verbose: sys.stdout.write("Monolith build process time: %s\n" % \ (endtime - inittime)) def logout(self, url=None): """Main function for logout command :param url: the URL for the logout request. :type url: str. """ sessionlocs = [] try: self.current_client.monolith.killthreads() except Exception: pass try: self.current_client.logout() except Exception: sessionlocs = self.deletelogoutfunction(url) else: self.deletelogoutfunction(url) for session in sessionlocs: try: self.delete_handler(session[0], url=session[1], sessionid=session[2], silent=True, service=True) except: pass self.remove_rmc_client(url) self.save() cachedir = self.config.get_cachedir() if cachedir: try: shutil.rmtree(cachedir) except Exception: pass def get(self, selector=None): """Main function for get command :param selector: the type selection for the get operation. :type selector: str. :returns: returns a list from get operation """ results = list() instances = self.get_selection() if not instances or len(instances) == 0: raise NothingSelectedError() for instance in instances: currdict = instance.resp.dict # apply patches to represent current edits for patch in instance.patches: currdict = jsonpatch.apply_patch(currdict, patch) if selector: jsonpath_expr = jsonpath_rw.parse('%s' % selector) matches = jsonpath_expr.find(currdict) temp_dict = OrderedDict() for match in matches: json_pstr = '/%s' % match.full_path json_node = jsonpointer.resolve_pointer(currdict, json_pstr) temp_dict[str(match.full_path)] = json_node results.append(temp_dict) else: results.append(currdict) return results def get_save(self, selector=None, currentoverride=False, pluspath=False, onlypath=None): """Special main function for get in save command :param selector: the type selection for the get operation. :type selector: str. :param currentoverride: flag to override current selection. :type currentoverride: boolean. :param pluspath: flag to add path to the results. :type pluspath: boolean. :param onlypath: flag to enable only that path selection. :type onlypath: boolean. :returns: returns a list from the get command """ results = list() instances = self.get_selection() if not instances or len(instances) == 0: raise NothingSelectedError() for instance in instances: if self.get_save_helper(instance.resp.request.path, instances)\ and not currentoverride: continue elif onlypath: if not onlypath == instance.resp.request.path: continue currdict = instance.resp.dict # apply patches to represent current edits for patch in instance.patches: currdict = jsonpatch.apply_patch(currdict, patch) if selector: for item in six.iterkeys(currdict): if selector.lower() == item.lower(): selector = item break try: jsonpath_expr = jsonpath_rw.parse('"%s"' % selector) except Exception as excp: raise InvalidCommandLineError(excp) matches = jsonpath_expr.find(currdict) temp_dict = OrderedDict() for match in matches: json_pstr = '/%s' % match.full_path json_node = jsonpointer.resolve_pointer(currdict, json_pstr) temp_dict[str(match.full_path)] = json_node results.append(temp_dict) else: if pluspath: results.append({instance.resp.request.path: currdict}) else: results.append(currdict) return results def get_save_helper(self, path, instances): """helper function for save helper to remove non /settings section :param path: originating path for the current instance. :type path: str. :param instances: current retrieved instances. :type instances: dict. :returns: returns skip boolean """ skip = False for item in instances: if (path + "/settings").lower() == item.resp.request.path.lower(): skip = True break elif (path + "settings/").lower() == \ item.resp.request.path.lower(): skip = True break return skip def set(self, selector=None, val=None): """Main function for set command :param selector: the type selection for the set operation. :type selector: str. :param val: value for the property to be modified. :type val: str. :returns: returns a status or list of changes set """ results = list() nochangesmade = False patchremoved = False instances = self.get_selection() if not instances or len(instances) == 0: raise NothingSelectedSetError() if selector: for instance in instances: if self.validate_headers(instance): continue else: nochangesmade = True currdict = instance.resp.dict for item in six.iterkeys(currdict): if selector.lower() == item.lower(): selector = item break newdict = currdict.copy() jsonpath_expr = jsonpath_rw.parse(u'%s' % selector) matches = jsonpath_expr.find(currdict) if not matches: self.warning_handler("Property not found in selection " \ "'%s', skipping '%s'\n" % (instance.type, selector)) nochangesmade = False for match in matches: listfound = False newdict = currdict.copy() json_pstr = u'/%s' % match.full_path if val: if str(val)[0] == "[" and str(val)[-1] == "]": json_node = jsonpointer.set_pointer(newdict, json_pstr, '"' + str(val) + '"', inplace=True) else: listfound = True else: listfound = True if listfound: json_node = jsonpointer.set_pointer(newdict, json_pstr, val, inplace=True) json_node = jsonpointer.resolve_pointer(newdict, json_pstr) patch = jsonpatch.make_patch(currdict, newdict) if patch: for item in instance.patches: if patch == item: return try: if item[0]["path"] == patch.patch[0]["path"]: instance.patches.remove(item) except Exception: if item.patch[0]["path"] == \ patch.patch[0]["path"]: instance.patches.remove(item) instance.patches.append(patch) results.append({selector:json_node}) if not patch: for item in instance.patches: try: entry = item.patch[0]["path"].replace('/', '') value = item.patch[0]["value"] except Exception: entry = item[0]["path"].replace('/', '') value = item[0]["value"] if entry == selector and str(value) not in str(val): if currdict[selector] == val: instance.patches.remove(item) patchremoved = True nochangesmade = True if not nochangesmade: return "No entries found" if patchremoved: return "reverting" else: return results def validate_headers(self, instance, verbose=False): skip = False try: headervals = instance.resp._http_response.headers.keys() if headervals is not None and len(headervals): allow = list(filter(lambda x:x.lower()=="allow", headervals)) if len(allow): if not "PATCH" in instance.resp._http_response.headers\ [allow[0]]: skip = True return skip except: pass try: if not any("PATCH" in x for x in instance.resp._http_response.msg.\ headers): if verbose: self.warning_handler('Skipping read-only path: %s\n' % \ instance.resp.request.path) skip = True except: try: for item in instance.resp._headers: if list(item.keys())[0] == "allow": if not "PATCH" in list(item.values())[0]: if verbose: self.warning_handler('Skipping read-only ' \ 'path: %s' % instance.resp.request.path) skip = True break except: if not ("allow" in instance.resp._headers and "PATCH" in instance.resp._headers["allow"]): if verbose: self.warning_handler('Skipping read-only path: ' \ '%s\n' % instance.resp.request.path) skip = True elif not "allow" in instance.resp._headers: if verbose: self.warning_handler('Skipping read-only path: %s\n' \ % instance.resp.request.path) skip = True return skip def loadset(self, dicttolist=None, selector=None, val=None, newargs=None): """Optimized version of the old style of set properties :param selector: the type selection for the set operation. :type selector: str. :param val: value for the property to be modified. :type val: str. :param newargs: list of multi level properties to be modified. :type newargs: list. :returns: returns a status or a list of set properties """ results = list() if (selector and val) and not dicttolist: dicttolist = [(selector, val)] elif dicttolist is None and not newargs: return results elif (selector and val and dicttolist) or (newargs and not val): return False nochangesmade = False patchremoved = False settingskipped = False instances = self.get_selection() if not instances or len(instances) == 0: raise NothingSelectedSetError() newarg = None if newargs: (name, value) = newargs[-1].split('=') outputline = '/'.join(newargs[:-1]) + "/" + name newarg = newargs[:-1] newarg.append(name) dicttolist = [(name, value)] for instance in instances: if self.validate_headers(instance): continue else: nochangesmade = True currdict = instance.resp.dict currdictcopy = copy.deepcopy(currdict) templist = [] if newargs and len(dicttolist)==1 : for i in range(len(newargs)): for item in six.iterkeys(currdictcopy): if newarg[i].lower() == item.lower(): newarg[i] = item if not i == (len(newargs) - 1): currdictcopy = currdictcopy[item] else: dicttolist = [(item, dicttolist[0][1])] break else: items = list(currdict.keys()) items = sorted(items) itemslower = [x.lower() for x in items] try: for ind, item in enumerate(dicttolist): try: if not isinstance(item[1], list): dicttolist[ind] = items[itemslower.index( item[0].lower())], item[1] else: templist.append(item[0]) except ValueError as excp: self.warning_handler("Skipping property {0}, not " \ "found in current server.\n".format(item[0])) templist.append(item[0]) settingskipped = True if templist: dicttolist = [x for x in dicttolist if x not in \ templist] except Exception as excp: raise excp if len(dicttolist) < 1: return results newdict = copy.deepcopy(currdict) patch = None if newargs and len(dicttolist)==1 : matches = self.setmultiworker(newargs, dicttolist, newdict) if not matches: self.warning_handler("Property not found in selection " \ "'%s', skipping '%s'\n" % (instance.type, outputline)) dicttolist = [] for (itersel, iterval) in dicttolist: jsonpath_expr = jsonpath_rw.parse('%s' % itersel) matches = jsonpath_expr.find(currdict) if not matches: self.warning_handler("Property not found in selection " \ "'%s', skipping '%s'\n" % (instance.type, itersel)) nochangesmade = False for match in matches: listfound = False json_pstr = '/%s' % match.full_path if iterval: if str(iterval)[0] == "[" and str(iterval)[-1] == "]": json_node = jsonpointer.set_pointer(newdict, json_pstr, '"' + str(iterval) + \ '"', inplace=True) else: listfound = True else: listfound = True if listfound: json_node = jsonpointer.set_pointer(newdict, json_pstr, iterval, inplace=True) json_node = jsonpointer.resolve_pointer(newdict, json_pstr) patch = jsonpatch.make_patch(currdict, newdict) if patch: for item in instance.patches: try: if item[0]["path"] == patch.patch[0]["path"]: instance.patches.remove(item) except Exception: if item.patch[0]["path"] == \ patch.patch[0]["path"]: instance.patches.remove(item) instance.patches.append(patch) results.append({itersel:json_node}) currdict = newdict.copy() if newargs and not dicttolist: patch = jsonpatch.make_patch(currdict, newdict) if patch: for item in instance.patches: try: if item[0]["path"] == patch.patch[0]["path"]: instance.patches.remove(item) except Exception: if item.patch[0]["path"] == \ patch.patch[0]["path"]: instance.patches.remove(item) instance.patches.append(patch) results.append({outputline:val}) if not patch: for item in instance.patches: try: entry = item.patch[0]["path"].split('/')[1:] except Exception: entry = item[0]["path"].split('/')[1:] if len(entry) == len(newarg): check = 0 for ind, elem in enumerate(entry): if elem == newarg[ind]: check += 1 if check == len(newarg): instance.patches.remove(item) patchremoved = True nochangesmade = True if not nochangesmade: return results if patchremoved: return "reverting" elif settingskipped is True: raise LoadSkipSettingError() else: return results def setmultilevel(self, val=None, newargs=None): """Main function for set multi level command :param val: value for the property to be modified. :type val: str. :param newargs: list of multi level properties to be modified. :type newargs: list. :returns: returns a status or a list of set multi level properties """ results = list() selector = None nochangesmade = False patchremoved = False (name, _) = newargs[-1].split('=', 1) outputline = '/'.join(newargs[:-1]) + "/" + name instances = self.get_selection() if not instances or len(instances) == 0: raise NothingSelectedError() if newargs: for instance in instances: currdict = instance.resp.dict currdictcopy = currdict newarg = newargs[:-1] newarg.append(name) for i in range(len(newargs)): for item in six.iterkeys(currdictcopy): if newarg[i].lower() == item.lower(): selector = item newarg[i] = item if not newarg[i].lower() == newarg[-1].lower(): currdictcopy = currdictcopy[item] break if not selector: continue if self.validate_headers(instance): continue else: nochangesmade = True newdict = copy.deepcopy(currdict) self._multilevelbuffer = newdict matches = self.setmultiworker(newargs, self._multilevelbuffer) if not matches: self.warning_handler("Property not found in selection " \ "'%s', skipping '%s'\n" % (instance.type, outputline)) else: patch = jsonpatch.make_patch(currdict, newdict) if patch: for item in instance.patches: if patch == item: return try: if item[0]["path"] == patch.patch[0]["path"]: instance.patches.remove(item) except Exception: if item.patch[0]["path"] == \ patch.patch[0]["path"]: instance.patches.remove(item) instance.patches.append(patch) results.append({outputline:val}) if not patch: for item in instance.patches: try: entry = item.patch[0]["path"].split('/')[1:] except Exception: entry = item[0]["path"].split('/')[1:] if len(entry) == len(newarg): check = 0 for ind, elem in enumerate(entry): if elem == newarg[ind]: check += 1 if check == len(newarg): instance.patches.remove(item) patchremoved = True nochangesmade = True if not nochangesmade: return "No entries found" if patchremoved: return "reverting" else: return results def setmultiworker(self, newargs, change, currdict, current=0): """Helper function for multi level set function :param newargs: list of multi level properties to be modified. :type newargs: list. :param currdict: current selection dictionary. :type currdict: dict. :param current: current location holder. :type current: list. :returns: returns boolean on whether properties are found """ found = False if not newargs[current] == newargs[-1]: for attr, val in six.iteritems(currdict): if attr.lower() == newargs[current].lower(): current += 1 found = self.setmultiworker(newargs, change, val, current) continue else: continue else: for attr, val in six.iteritems(currdict): if attr.lower() == change[0][0].lower(): currdict[attr] = change[0][1] found = True return found def status(self): """Main function for status command""" finalresults = list() monolith = self.current_client.monolith for ristype in monolith.types: if 'Instances' in monolith.types[ristype]: for instance in monolith.types[ristype]['Instances']: results = list() if instance.patches and len(instance.patches) > 0: if isinstance(instance.patches[0], list): results.extend(instance.patches) else: if instance.patches[0]: for item in instance.patches: results.extend(item) itemholder = list() for mainitem in results: item = copy.deepcopy(mainitem) itemholder.append(item) if itemholder: finalresults.append({instance.type: itemholder}) return finalresults def capture(self): """Build and return the entire monolith""" monolith = self.current_client.monolith vistedurls = monolith.get_visited_urls() monolith.set_visited_urls(list()) monolith.load(includelogs=True, skipcrawl=False, loadcomplete=True) monolith.set_visited_urls(vistedurls) return monolith def commitworkerfunc(self, patch): """Helper function for the commit command :param patch: dictionary containing all patches to be applied. :type patch: dict. :returns: returns a dictionary of patches applied """ try: entries = patch.patch[0]["path"][1:].split("/") except Exception: entries = patch[0]["path"][1:].split("/") counter = 0 results = dict() for item in reversed(entries): if counter == 0: boolfound = False try: boolfound = isinstance(patch.patch[0]["value"], bool) except Exception: boolfound = isinstance(patch[0]["value"], bool) try: intfound = isinstance(patch.patch[0]["value"], int) except Exception: intfound = isinstance(patch[0]["value"], int) if boolfound or intfound: try: results = {item:patch.patch[0]["value"]} except Exception: results = {item:patch[0]["value"]} else: try: if patch.patch[0]["value"][0] == '"' and\ patch.patch[0]["value"][-1] == '"': results = {item:patch.patch[0]["value"][1:-1]} else: results = {item:patch.patch[0]["value"]} except Exception: if patch[0]["value"][0] == '"' and\ patch[0]["value"][-1] == '"': results = {item:patch[0]["value"][1:-1]} else: results = {item:patch[0]["value"]} counter += 1 else: results = {item:results} return results def commit(self, out=sys.stdout, verbose=False): """Main function for commit command :param out: output type for verbosity. :type out: output type. :param verbose: flag to determine additional output. :type verbose: boolean. :returns: returns boolean of whether changes were made """ changesmade = False instances = self.get_commit_selection() if not instances or len(instances) == 0: raise NothingSelectedError() for instance in instances: if self.validate_headers(instance, verbose=verbose): continue currdict = dict() # apply patches to represent current edits for patch in instance.patches: if hasattr(patch, 'patch'): if len(patch.patch): if "/" in patch.patch[0]["path"][1:]: newdict = self.commitworkerfunc(patch) if newdict: self.merge_dict(currdict, newdict) else: if isinstance(patch.patch[0]["value"], int): currdict[patch.patch[0]["path"][1:]] = \ patch.patch[0]["value"] elif not isinstance(patch.patch[0]["value"], bool): if patch.patch[0]["value"]: if patch.patch[0]["value"][0] == '"' and\ patch.patch[0]["value"][-1] == '"' and\ len(patch.patch[0]["value"]) == 2: currdict[patch.patch[0]["path"][1:]] = \ '' elif patch.patch[0]["value"][0] == '"' and\ patch.patch[0]["value"][-1] == '"': line = patch.patch[0]["value"]\ [2:-2].replace("'", "") line = line.replace(", ", ",") currdict[patch.patch[0]["path"]\ [1:]] = line.split(',') else: currdict[patch.patch[0]["path"][1:]] = \ patch.patch[0]["value"] else: currdict[patch.patch[0]["path"][1:]] = \ patch.patch[0]["value"] else: currdict[patch.patch[0]["path"][1:]] = \ patch.patch[0]["value"] else: if "/" in patch[0]["path"][1:]: newdict = self.commitworkerfunc(patch) if newdict: self.merge_dict(currdict, newdict) else: if isinstance(patch[0]["value"], int): currdict[patch[0]["path"][1:]] = patch[0]["value"] elif not isinstance(patch[0]["value"], bool): if patch[0]["value"]: if patch[0]["value"][0] == '"' and\ patch[0]["value"][-1] == '"' and \ len(patch[0]["value"]) == 2: currdict[patch[0]["path"][1:]] = '' elif patch[0]["value"][0] == '"' and\ patch[0]["value"][-1] == '"': currdict[patch[0]["path"][1:]] = \ patch[0]["value"][2:-2].split(',') else: currdict[patch[0]["path"][1:]] = \ patch[0]["value"] else: currdict[patch[0]["path"][1:]] = \ patch[0]["value"] else: currdict[patch[0]["path"][1:]] = patch[0]["value"] if currdict: changesmade = True if verbose: out.write('Changes made to path: %s\n' % \ instance.resp.request.path) put_path = instance.resp.request.path results = self.current_client.set(put_path, body=currdict) self.invalid_return_handler(results) if not results.status == 200: raise FailureDuringCommitError("Failed to commit with " \ "error code %d" % results.status) return changesmade def merge_dict(self, currdict, newdict): """Helper function to merge dictionaries :param currdict: current selection dictionary. :type currdict: dict. :param newdict: new selection dictionary. :type newdict: dict. """ for k, itemv2 in list(newdict.items()): itemv1 = currdict.get(k) if isinstance(itemv1, Mapping) and\ isinstance(itemv2, Mapping): self.merge_dict(itemv1, itemv2) else: currdict[k] = itemv2 def patch_handler(self, put_path, body, verbose=False, url=None, sessionid=None, headers=None, response=False, silent=False): """Main worker function for raw patch command :param put_path: the URL path. :type put_path: str. :param body: the body to the sent. :type body: str. :param verbose: flag to determine additional output. :type verbose: boolean. :param url: originating URL. :type url: str. :param sessionid: session id to be used instead of credentials. :type sessionid: str. :param headers: additional headers to be added to the request. :type headers: str. :param response: flag to return the response. :type response: str. :param silent: flag to disable output. :type silent: boolean. :returns: returns RestResponse object containing response data """ if sessionid: results = RmcClient(url=url, sessionkey=sessionid).set(put_path, body=body, headers=headers) else: results = self.current_client.set(put_path, body=body, headers=headers) if not silent: self.invalid_return_handler(results, verbose=verbose) elif results.status == 401: raise SessionExpired() if response: return results def get_handler(self, put_path, silent=False, verbose=False, url=None, sessionid=None, uncache=False, headers=None, response=False): """main worker function for raw get command :param put_path: the URL path. :type put_path: str. :param silent: flag to determine if no output should be done. :type silent: boolean. :param verbose: flag to determine additional output. :type verbose: boolean. :param url: originating URL. :type url: str. :param sessionid: session id to be used instead of credentials. :type sessionid: str. :param uncache: flag to not store the data downloaded into cache. :type uncache: boolean. :param headers: additional headers to be added to the request. :type headers: str. :param response: flag to return the response. :type response: str. :returns: returns a RestResponse object from client's get command """ if sessionid: results = RmcClient(url=url, sessionkey=sessionid).get(put_path, headers=headers) else: results = self.current_client.get(put_path, uncache=uncache, headers=headers) if not silent: self.invalid_return_handler(results, verbose=verbose) elif results.status == 401: raise SessionExpired() if results.status == 200 or response: return results else: return None def post_handler(self, put_path, body, verbose=False, url=None, sessionid=None, headers=None, response=False, silent=False): """Main worker function for raw post command :param put_path: the URL path. :type put_path: str. :param body: the body to the sent. :type body: str. :param verbose: flag to determine additional output. :type verbose: boolean. :param url: originating URL. :type url: str. :param sessionid: session id to be used instead of credentials. :type sessionid: str. :param headers: additional headers to be added to the request. :type headers: str. :param response: flag to return the response. :type response: str. :param silent: flag to disable output. :type silent: boolean. :returns: returns a RestResponse from client's Post command """ if sessionid: results = RmcClient(url=url, sessionkey=sessionid).toolpost( put_path, body=body, headers=headers) else: results = self.current_client.toolpost(put_path, body=body, headers=headers) if not silent: self.invalid_return_handler(results, verbose=verbose) elif results.status == 401: raise SessionExpired() if response: return results def put_handler(self, put_path, body, verbose=False, url=None, sessionid=None, headers=None, response=False, silent=False): """Main worker function for raw put command :param put_path: the URL path. :type put_path: str. :param body: the body to the sent. :type body: str. :param verbose: flag to determine additional output. :type verbose: boolean. :param url: originating URL. :type url: str. :param sessionid: session id to be used instead of credentials. :type sessionid: str. :param headers: additional headers to be added to the request. :type headers: str. :param response: flag to return the response. :type response: str. :param silent: flag to disable output. :type silent: boolean. :returns: returns a RestResponse object from client's Put command """ if sessionid: results = RmcClient(url=url, sessionkey=sessionid).toolput( put_path, body=body, headers=headers) else: results = self.current_client.toolput(put_path, body=body, headers=headers) if not silent: self.invalid_return_handler(results, verbose=verbose) elif results.status == 401: raise SessionExpired() if response: return results def delete_handler(self, put_path, verbose=False, url=None, sessionid=None, headers=None, silent=True): """Main worker function for raw delete command :param put_path: the URL path. :type put_path: str. :param verbose: flag to determine additional output. :type verbose: boolean. :param url: originating URL. :type url: str. :param sessionid: session id to be used instead of credentials. :type sessionid: str. :param headers: additional headers to be added to the request. :type headers: str. :param silent: flag to disable output. :type silent: boolean. :returns: returns a RestResponse object from client's Delete command """ if sessionid: results = RmcClient(url=url, sessionkey=sessionid).tooldelete( put_path, headers=headers) else: results = self.current_client.tooldelete(put_path, headers=headers) if not silent: self.invalid_return_handler(results, verbose=verbose) elif results.status == 401: raise SessionExpired() return results def head_handler(self, put_path, verbose=False, url=None, sessionid=None, silent=False): """Main worker function for raw head command :param put_path: the URL path. :type put_path: str. :param verbose: flag to determine additional output. :type verbose: boolean. :param url: originating URL. :type url: str. :param sessionid: session id to be used instead of credentials. :type sessionid: str. :param silent: flag to disable output. :type silent: boolean. :returns: returns a RestResponse object from client's Head command """ if sessionid: results = RmcClient(url=url, sessionkey=sessionid).head(put_path) else: results = self.current_client.head(put_path) if not silent: self.invalid_return_handler(results, verbose=verbose) elif results.status == 401: raise SessionExpired() if results.status == 200: return results else: return None _QUERY_PATTERN = re.compile(r'(?P[\w\.]+)(:(?P.*))?') def _parse_query(self, querystr): """Parse query and return as a dict. TODO probably need to move""" """ this into its own class if it gets too complicated :param querystr: query string. :type querystr: str. :returns: returns a dict of parsed query """ qmatch = RmcApp._QUERY_PATTERN.search(querystr) if not qmatch: raise InvalidSelectionError("Unable to locate instance for " \ "'%s'" % querystr) qgroups = qmatch.groupdict() return dict(instance=qgroups['instance'], xpath=qgroups.get('xpath', None)) def invalid_return_handler(self, results, verbose=False): """Main worker function for handling all error messages :param results: dict of the results. :type results: sict. :param verbose: flag to enable additional verbosity. :type verbose: boolean. """ if results.status == 401: raise SessionExpired() else: if results.status == 200 or results.status == 201: if verbose: self.warning_handler("[%d] The operation completed " \ "successfully.\n" % results.status) else: self.warning_handler("The operation completed " "successfully.\n") else: self.warning_handler("[%d] No message returned.\n" % \ results.status) def select(self, query, sel=None, val=None): """Main function for select command :param query: query string. :type query: str. :param sel: the type selection for the select operation. :type sel: str. :param val: value for the property to be modified. :type val: str. :returns: returns a list of selected items """ if query: if isinstance(query, list): if len(query) == 0: raise InstanceNotFoundError("Unable to locate instance " \ "for '%s'" % query) else: query = query[0] if val: if (str(val)[0] == str(val)[-1]) and \ str(val).endswith(("'", '"')): val = val[1:-1] selection = self.get_selection(selector=query, sel=sel, val=val) if selection and len(selection) > 0: self.current_client.selector = query if not sel is None and not val is None: self.current_client.filter_attr = sel self.current_client.filter_value = val else: self.current_client.filter_attr = None self.current_client.filter_value = None self.save() return selection if not sel is None and not val is None: raise InstanceNotFoundError("Unable to locate instance for" \ " '%s' and filter '%s=%s'" % (query, sel, val)) else: raise InstanceNotFoundError("Unable to locate instance for" \ " '%s'" % query) def filter(self, query, sel, val): """Main function for filter command :param query: query string. :type query: str. :param sel: the type selection for the select operation. :type sel: str. :param val: value for the property to be modified. :type val: str. :returns: returns a list of selected items """ if query: if isinstance(query, list): if len(query) == 0: raise InstanceNotFoundError("Unable to locate instance " \ "for '%s'" % query) else: query = query[0] selection = self.get_selection(selector=query, sel=sel, val=val) if selection and len(selection) > 0: self.current_client.selector = query self.current_client.filter_attr = sel self.current_client.filter_value = val self.save() return selection def filter_output(self, output, sel, val): """Filters a list of dictionaries based on a key:value pair :param output: output list. :type output: list. :param sel: the key for the property to be filtered by. :type sel: str. :param val: value for the property be filtered by. :type val: str. :returns: returns an filtered list from output parameter """ newoutput = [] if isinstance(output, list): for entry in output: if isinstance(entry, dict): if '/' in sel: sellist = sel.split('/') newentry = copy.copy(entry) for item in sellist: if item in list(newentry.keys()): if item == sellist[-1] and str(newentry[item])\ == val: newoutput.append(entry) else: newentry = newentry[item] else: if sel in list(entry.keys()) and entry[sel] == val: newoutput.append(entry) else: return output return newoutput def types(self): """Main function for types command :returns: returns a list of type strings """ instances = list() monolith = self.current_client.monolith for ristype in monolith.types: if 'Instances' in monolith.types[ristype]: for instance in monolith.types[ristype][u'Instances']: instances.append(instance.type) return instances def get_selection(self, selector=None, sel=None, val=None): """Special main function for set/filter with select command :param selector: the type selection for the get operation. :type selector: str. :param sel: property to be modified. :type sel: str. :param val: value for the property to be modified. :type val: str. :returns: returns a list of selected items """ if not sel and not val: (sel, val) = self.get_filter_settings() monolith = self.current_client.monolith instances = list() if not selector: selector = self.current_client.selector if not selector: return instances xpath = None odata = '' if not selector == '"*"': qvars = self._parse_query(selector) qinstance = qvars['instance'] xpath = qvars['xpath'] else: qinstance = selector for ristype in monolith.types: if 'Instances' in monolith.types[ristype]: for instance in monolith.types[ristype]['Instances']: try: odata = instance.resp.dict['@odata.type'].lower() except Exception: odata = '' if qinstance.lower() in instance.type.lower() \ or qinstance == '"*"' or qinstance.lower() in odata: if not sel is None and not val is None: currdict = instance.resp.dict try: if not "/" in sel: if val[-1] == "*": if not val[:-1] in str(currdict[sel]): continue else: if not str(currdict[sel]).\ startswith(val): continue else: newargs = sel.split("/") content = copy.deepcopy(currdict) if self.filterworkerfunction(workdict=\ content, sel=sel, val=val, newargs=newargs, loopcount=0): instances.append(instance) continue except Exception: continue if xpath: raise RuntimeError("Not implemented") else: instances.append(instance) return instances def filterworkerfunction(self, workdict=None, sel=None, val=None, newargs=None, loopcount=0): """Helper function for filter application :param workdict: working copy of current dictionary. :type workdict: dict. :param sel: property to be modified. :type sel: str. :param val: value for the property to be modified. :type val: str. :param newargs: list of multi level properties to be modified. :type newargs: list. :param loopcount: loop count tracker. :type loopcount: int. :returns: returns boolean based on val parameter being found in newargs """ if workdict and sel and val and newargs: if isinstance(workdict, list): for item in workdict: if self.filterworkerfunction(workdict=item, sel=sel, val=val, newargs=newargs, loopcount=loopcount): return True return False keys = list(workdict.keys()) keyslow = [x.lower() for x in keys] if newargs[loopcount].lower() in keyslow: if loopcount == (len(newargs) - 1): if val == str(workdict[newargs[loopcount]]): return True return False if not (isinstance(workdict[newargs[loopcount]], list) or isinstance(workdict[newargs[loopcount]], dict)): return False workdict = workdict[newargs[loopcount]] loopcount += 1 if self.filterworkerfunction(workdict=workdict, sel=sel, val=val, newargs=newargs, loopcount=loopcount): return True return False def get_commit_selection(self): """Special main function for commit command""" instances = list() monolith = self.current_client.monolith for ristype in monolith.types: if 'Instances' in monolith.types[ristype]: for instance in monolith.types[ristype]['Instances']: instances.append(instance) return instances def get_save_header(self, selector=None): """Special function for save file headers :param selector: the type selection for the get save operation. :type selector: str. :returns: returns an header ordered dictionary """ instances = OrderedDict() monolith = self.current_client.monolith if not selector: selector = self.current_client.selector if not selector: return instances instances["Comments"] = OrderedDict() for ristype in monolith.types: if 'Instances' in monolith.types[ristype]: for instance in monolith.types[ristype]['Instances']: if "computersystem." in instance.type.lower(): try: if instance.resp.obj["Manufacturer"]: instances["Comments"]["Manufacturer"] = \ instance.resp.obj["Manufacturer"] if instance.resp.obj["Model"]: instances["Comments"]["Model"] = \ instance.resp.obj["Model"] except Exception: pass return instances def get_selector(self): """Helper function to return current select option""" if self.current_client: if self.current_client.selector: return self.current_client.selector return None def get_filter_settings(self): """Helper function to return current select option""" if self.current_client: if not self.current_client.filter_attr is None and not \ self.current_client.filter_value is None: return (self.current_client.filter_attr, self.current_client.filter_value) return (None, None) def erase_filter_settings(self): """Helper function to return current select option""" if self.current_client: if not self.current_client.filter_attr is None or \ not self.current_client.filter_value is None: self.current_client.filter_attr = None self.current_client.filter_value = None python-redfish-3.2.7/src/redfish/ris/rmc_helper.py000066400000000000000000000561211473240251500222000ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md # -*- coding: utf-8 -*- """RMC helper implementation""" #---------Imports--------- import os import json import errno import logging import hashlib import redfish.rest from urllib.parse import urlparse from .ris import (RisMonolith) from .sharedtypes import (JSONEncoder) from .config import (AutoConfigParser) #---------End of imports--------- #---------Debug logger--------- LOGGER = logging.getLogger(__name__) #---------End of debug logger--------- class RdmcError(Exception): """Base class for all RDMC Exceptions""" errcode = 1 def __init__(self, message): Exception.__init__(self, message) class InvalidCommandLineError(RdmcError): """Raised when user enter incorrect command line arguments""" pass class FailureDuringCommitError(RdmcError): """Raised when there is an error while updating firmware""" pass class UndefinedClientError(Exception): """Raised when there are no clients active (usually when user hasn't logged in""" pass class InstanceNotFoundError(Exception): """Raised when attempting to select an instance that does not exist""" pass class CurrentlyLoggedInError(Exception): """Raised when attempting to select an instance that does not exist""" pass class NothingSelectedError(Exception): """Raised when attempting to access an object without first selecting it""" pass class NothingSelectedSetError(Exception): """Raised when attempting to access an object without first selecting it""" pass class InvalidSelectionError(Exception): """Raised when selection argument fails to match anything""" pass class SessionExpired(Exception): """Raised when the session has expired""" pass class LoadSkipSettingError(Exception): """Raised when one or more settings are absent in given server""" pass class InvalidPathError(Exception): """Raised when requested path is not found""" pass class RmcClient(object): """RMC client base class""" def __init__(self, url=None, username=None, password=None, sessionkey=None, proxies=None): """Initialized RmcClient :param url: redfish host name or IP address. :type url: str. :param username: user name required to login to server. :type: str. :param password: password credentials required to login. :type password: str. :param sessionkey: session key credential for current login :type sessionkey: str :param proxies: Dictionary containing protocol to proxy URL mappings :type proxies: dict """ self._rest_client = redfish.rest.v1.redfish_client( base_url=url, username=username, password=password, sessionkey=sessionkey, proxies=proxies) self._get_cache = dict() self._monolith = RisMonolith(self) self._selector = None self._filter_attr = None self._filter_value = None def get_username(self): """The rest client's current user name""" return self._rest_client.get_username() def set_username(self, username): """Sets the rest client's user name :param username: user name to set for login :type username: str """ self._rest_client.set_username(username) def get_password(self): """The rest client's current password""" return self._rest_client.get_password() def set_password(self, password): """Sets the rest clients's password :param password: password to set for login :type password: str """ self._rest_client.set_password(password) def get_session_key(self): """The rest client's current session key""" return self._rest_client.get_session_key() def get_session_location(self): """The rest client's current session location""" return self._rest_client.get_session_location() def get_authorization_key(self): """The rest client's current authorization key""" return self._rest_client.get_authorization_key() def get_base_url(self): """The rest client's current base URL""" return self._rest_client.get_base_url() base_url = property(get_base_url, None) def get_cache_dirname(self): """The rest client's current base URL converted to path""" parts = urlparse(self.get_base_url()) pathstr = '%s/%s' % (parts.netloc, parts.path) return pathstr.replace('//', '/') def get_monolith(self): """The rest client's current monolith""" return self._monolith monolith = property(get_monolith, None) def _get_selector(self): """The rest client's current selector""" return self._selector def _set_selector(self, selectorval): """Sets the rest client's selector :param selectorval: the type selection for the set operation. :type selectorval: str """ self._selector = selectorval selector = property(_get_selector, _set_selector) def _get_filter_attr(self): """The rest client's current filter""" return self._filter_attr def _set_filter_attr(self, filterattr): """Sets the rest client's filter :param filterattr: the type selection for the select operation. :type filterattr: str. """ self._filter_attr = filterattr filter_attr = property(_get_filter_attr, _set_filter_attr) def _get_filter_value(self): """The rest client's current filter value""" return self._filter_value def _set_filter_value(self, filterval): """Sets the rest client's filter value :param filterval: value for the property to be modified. :type filterval: str. """ self._filter_value = filterval filter_value = property(_get_filter_value, _set_filter_value) def login(self): """Login using rest client""" self._rest_client.login(auth="session") def logout(self): """Logout using rest client""" self._rest_client.logout() def get(self, path, args=None, uncache=False, headers=None): """Perform a GET request on path argument. :param path: The URL to perform a GET request on. :type path: str. :param args: GET arguments. :type args: str. :returns: a RestResponse object containing the response data. :rtype: redfish.rest.v1.RestResponse. :param uncache: flag to not store the data downloaded into cache. :type uncache: boolean. :param headers: dict of headers to be appended. :type headers: dict. """ resp = self._rest_client.get(path=path, args=args, headers=headers) if uncache is False: self._get_cache[path] = resp return resp def head(self, path, args=None, headers=None): """Perform a HEAD request on path argument. :param path: The URL to perform a GET request on. :type path: str. :param args: GET arguments. :type args: str. :param headers: dict of headers to be appended. :type headers: dict. :returns: a RestResponse object containing the response data. :rtype: redfish.rest.v1.RestResponse. """ resp = self._rest_client.head(path=path, args=args, headers=headers) return resp def set(self, path, args=None, body=None, headers=None): """Perform a PATCH request on path argument. :param path: The URL to perform a PATCH request on. :type path: str. :param args: GET arguments. :type args: str. :param body: contents of the PATCH request. :type body: str. :param headers: list of headers to be appended. :type headers: list. :returns: a RestResponse object containing the response data. :rtype: redfish.rest.v1.RestResponse. """ resp = self._rest_client.get(path=path, args=args) self._get_cache[path] = resp return self._rest_client.patch(path=path, args=args, body=body, headers=headers) def toolpost(self, path, args=None, body=None, headers=None): """Perform a POST request on path argument. :param path: The URL to perform a POST request on. :type path: str. :param args: POST arguments. :type args: str. :param body: contents of the POST request. :type body: str. :param headers: list of headers to be appended. :type headers: list. :returns: a RestResponse object containing the response data. :rtype: redfish.rest.v1.RestResponse. """ resp = self._rest_client.get(path=path, args=args) self._get_cache[path] = resp return self._rest_client.post(path=path, args=args, body=body, headers=headers) def toolput(self, path, args=None, body=None, headers=None): """ Perform a PUT request on path argument. :param path: The URL to perform a PUT request on. :type path: str. :param args: PUT arguments. :type args: str. :param body: contents of the PUT request. :type body: str. :param headers: list of headers to be appended. :type headers: list. :returns: a RestResponse object containing the response data. :rtype: redfish.rest.v1.RestResponse. """ resp = self._rest_client.get(path=path, args=args) self._get_cache[path] = resp return self._rest_client.put(path=path, args=args, body=body, headers=headers) def tooldelete(self, path, args=None, headers=None): """Perform a PUT request on path argument. :param path: The URL to perform a DELETE request on. :type path: str. :param args: DELETE arguments. :type args: str. :param headers: list of headers to be appended. :type headers: list. :returns: a RestResponse object containing the response data. :rtype: redfish.rest.v1.RestResponse. """ resp = self._rest_client.get(path=path, args=args) self._get_cache[path] = resp return self._rest_client.delete(path=path, args=args, headers=headers) class RmcConfig(AutoConfigParser): """RMC config object""" def __init__(self, filename=None): """Initialize RmcConfig :param filename: file name to be used for Rmcconfig loading. :type filename: str """ AutoConfigParser.__init__(self, filename=filename) self._sectionname = 'redfish' self._configfile = filename self._ac__logdir = os.getcwd() self._ac__cache = True self._ac__url = '' self._ac__username = '' self._ac__password = '' self._ac__commit = '' self._ac__format = '' self._ac__cachedir = '' self._ac__savefile = '' self._ac__loadfile = '' def get_configfile(self): """The current configuration file""" return self._configfile def set_configfile(self, config_file): """Set the current configuration file :param config_file: file name to be used for Rmcconfig loading. :type config_file: str """ self._configfile = config_file def get_logdir(self): """Get the current log directory""" return self._get('logdir') def set_logdir(self, value): """Set the current log directory :param value: current working directory for logging :type value: str """ return self._set('logdir', value) def get_cache(self): """Get the config file cache status""" if isinstance(self._get('cache'), bool): return self._get('cache') else: return self._get('cache').lower() in ("yes", "true", "t", "1") def set_cache(self, value): """Get the config file cache status :param value: status of config file cache :type value: bool """ return self._set('cache', value) def get_url(self): """Get the config file URL""" return self._get('url') def set_url(self, value): """Set the config file URL :param value: URL path for the config file :type value: str """ return self._set('url', value) def get_username(self): """Get the config file user name""" return self._get('username') def set_username(self, value): """Set the config file user name :param value: user name for config file :type value: str """ return self._set('username', value) def get_password(self): """Get the config file password""" return self._get('password') def set_password(self, value): """Set the config file password :param value: password for config file :type value: str """ return self._set('password', value) def get_commit(self): """Get the config file commit status""" return self._get('commit') def set_commit(self, value): """Set the config file commit status :param value: commit status :type value: str """ return self._set('commit', value) def get_format(self): """Get the config file default format""" return self._get('format') def set_format(self, value): """Set the config file default format :param value: set the config file format :type value: str """ return self._set('format', value) def get_cachedir(self): """Get the config file cache directory""" return self._get('cachedir') def set_cachedir(self, value): """Set the config file cache directory :param value: config file cache directory :type value: str """ return self._set('cachedir', value) def get_defaultsavefilename(self): """Get the config file default save name""" return self._get('savefile') def set_defaultsavefilename(self, value): """Set the config file default save name :param value: config file save name :type value: str """ return self._set('savefile', value) def get_defaultloadfilename(self): """Get the config file default load name""" return self._get('loadfile') def set_defaultloadfilename(self, value): """Set the config file default load name :param value: name of config file to load by default :type value: str """ return self._set('loadfile', value) class RmcCacheManager(object): """Manages caching/uncaching of data for RmcApp""" def __init__(self, rmc): """Initialize RmcCacheManager :param rmc: RmcApp to be managed :type rmc: RmcApp object """ self._rmc = rmc class RmcFileCacheManager(RmcCacheManager): """RMC file cache manager""" def __init__(self, rmc): super(RmcFileCacheManager, self).__init__(rmc) def logout_del_function(self, url=None): """Helper function for logging out a specific URL :param url: The URL to perform a logout request on. :type url: str. """ cachedir = self._rmc.config.get_cachedir() indexfn = '%s/index' % cachedir sessionlocs = [] if os.path.isfile(indexfn): try: indexfh = open(indexfn, 'r') index_cache = json.load(indexfh) indexfh.close() for index in index_cache: if url: if url in index['url']: os.remove(os.path.join(cachedir, index['href'])) break else: if os.path.isfile(cachedir + '/' + index['href']): monolith = open(cachedir + '/' + index['href'], 'r') data = json.load(monolith) monolith.close() for item in data: if 'login' in item and 'session_location' in \ item['login']: loc = item['login']['session_location'].\ split(item['login']['url'])[-1] sesurl = item['login']['url'] sessionlocs.append((loc, sesurl, item['login']['session_key'])) os.remove(os.path.join(cachedir, index['href'])) except BaseException as excp: self._rmc.warn('Unable to read cache data %s' % excp) return sessionlocs def uncache_rmc(self): """Simple monolith uncache function""" cachedir = self._rmc.config.get_cachedir() indexfn = '%s/index' % cachedir if os.path.isfile(indexfn): try: indexfh = open(indexfn, 'r') index_cache = json.load(indexfh) indexfh.close() for index in index_cache: clientfn = index['href'] self._uncache_client(clientfn) except BaseException as excp: self._rmc.warn('Unable to read cache data %s' % excp) def _uncache_client(self, cachefn): """Complex monolith uncache function :param cachefn: The cache file name. :type cachefn: str. """ cachedir = self._rmc.config.get_cachedir() clientsfn = '%s/%s' % (cachedir, cachefn) if os.path.isfile(clientsfn): try: clientsfh = open(clientsfn, 'r') clients_cache = json.load(clientsfh) clientsfh.close() for client in clients_cache: if 'login' not in client: continue login_data = client['login'] if 'url' not in login_data: continue rmc_client = RmcClient( username=login_data.get('username', 'Administrator'), password=login_data.get('password', None), url=login_data.get('url', None), sessionkey=login_data.get('session_key', None)) rmc_client._rest_client.set_authorization_key( login_data.get('authorization_key')) rmc_client._rest_client.set_session_key( login_data.get('session_key')) rmc_client._rest_client.set_session_location( login_data.get('session_location')) if 'selector' in client: rmc_client.selector = client['selector'] if 'filter_attr' in client: rmc_client.filter_attr = client['filter_attr'] if 'filter_value' in client: rmc_client.filter_value = client['filter_value'] getdata = client['get'] rmc_client._get_cache = dict() for key in list(getdata.keys()): restreq = redfish.rest.v1.RestRequest( method='GET', path=key) getdata[key]['restreq'] = restreq rmc_client._get_cache[key] = ( redfish.rest.v1.StaticRestResponse( **getdata[key])) rmc_client._monolith = RisMonolith(rmc_client) rmc_client._monolith.load_from_dict(client['monolith']) self._rmc._rmc_clients.append(rmc_client) except BaseException as excp: self._rmc.warn('Unable to read cache data %s' % excp) def cache_rmc(self): """Caching function for monolith""" if not self._rmc.config.get_cache(): return cachedir = self._rmc.config.get_cachedir() if not os.path.isdir(cachedir): try: os.makedirs(cachedir) except OSError as ex: if ex.errno == errno.EEXIST: pass else: raise index_map = dict() index_cache = list() for client in self._rmc._rmc_clients: md5 = hashlib.md5() md5.update(client.get_base_url().encode('utf-8')) md5str = md5.hexdigest() index_map[client.get_base_url()] = md5str index_data = dict(url=client.get_base_url(), href='%s' % md5str,) index_cache.append(index_data) indexfh = open('%s/index' % cachedir, 'w') json.dump(index_cache, indexfh, indent=2, cls=JSONEncoder) indexfh.close() clients_cache = list() if self._rmc._rmc_clients: for client in self._rmc._rmc_clients: login_data = dict( username=client.get_username(), password=client.get_password(), url=client.get_base_url(), session_key=client.get_session_key(), session_location=client.get_session_location(), authorization_key=client.get_authorization_key()) clients_cache.append( dict(selector=client.selector, login=login_data, filter_attr=client._filter_attr, filter_value=client._filter_value, monolith=client.monolith, get=client._get_cache)) clientsfh = open('%s/%s' % (cachedir, index_map[client.get_base_url()]), 'w') json.dump(clients_cache, clientsfh, indent=2, cls=JSONEncoder) clientsfh.close() python-redfish-3.2.7/src/redfish/ris/sharedtypes.py000066400000000000000000000023711473240251500224110ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md # -*- coding: utf-8 -*- """ Shared types used in this module """ #---------Imports--------- import logging import jsonpatch from redfish.rest.v1 import JSONEncoder #---------End of imports--------- #---------Debug logger--------- LOGGER = logging.getLogger(__name__) #---------End of debug logger--------- class JSONEncoder(JSONEncoder): """Custom JSONEncoder that understands our types""" def default(self, obj): """Set defaults :param obj: json object. :type obj: str. """ if isinstance(obj, Dictable): return obj.to_dict() elif isinstance(obj, jsonpatch.JsonPatch): return obj.patch return super(JSONEncoder, self).default(obj) class Dictable(object): """A base class which adds the to_dict method used during json encoding""" def to_dict(self): """Overridable funciton""" raise RuntimeError("You must override this method in your derived" " class") python-redfish-3.2.7/tests/000077500000000000000000000000001473240251500156335ustar00rootroot00000000000000python-redfish-3.2.7/tests/__init__.py000066400000000000000000000003031473240251500177400ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.mdpython-redfish-3.2.7/tests/discovery/000077500000000000000000000000001473240251500176425ustar00rootroot00000000000000python-redfish-3.2.7/tests/discovery/__init__.py000066400000000000000000000003031473240251500217470ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.mdpython-redfish-3.2.7/tests/discovery/test_discovery.py000066400000000000000000000015701473240251500232650ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md # -*- encoding: utf-8 -*- import unittest from redfish.discovery.discovery import FakeSocket from redfish.discovery.discovery import sanitize from io import BytesIO class TestFakeSocket(unittest.TestCase): def test_init(self): fake = FakeSocket(b"foo") self.assertTrue(isinstance(fake, FakeSocket)) self.assertTrue(isinstance(fake._file, BytesIO)) class TestDiscover(unittest.TestCase): def test_sanitize(self): self.assertEqual(sanitize(257, 1, 255), 255) self.assertEqual(sanitize(0, 1, 255), 1) self.assertEqual(sanitize(0, 1), 1) self.assertEqual(sanitize(2000, 1), 2000) self.assertEqual(sanitize(-1, 1), 1) python-redfish-3.2.7/tests/rest/000077500000000000000000000000001473240251500166105ustar00rootroot00000000000000python-redfish-3.2.7/tests/rest/__init__.py000066400000000000000000000003031473240251500207150ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.mdpython-redfish-3.2.7/tests/rest/test_v1.py000066400000000000000000000064031473240251500205520ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md # -*- encoding: utf-8 -*- import json import unittest from unittest import mock from redfish.rest.v1 import HttpClient, RetriesExhaustedError, redfish_client class TestRedFishClient(unittest.TestCase): def setUp(self) -> None: self.base_url = "http://foo.bar" self.username = "rstallman" self.password = "123456" self.default_prefix = "/custom/redfish/v1/" self.sessionkey = "fg687glgkf56vlgkf" self.capath = "/path/to/the/dir" self.cafile = "filename.test" self.timeout = 666 self.max_retry = 42 def test_redfish_client(self) -> None: # NOTE(hberaud) the client try to connect when we initialize the # http client object so we need to catch the retries exception first. # In a second time we need to mock the six.http_client to simulate # server responses and do some other tests with self.assertRaises(RetriesExhaustedError): client = redfish_client(base_url=self.base_url) # Check the object type self.assertTrue(isinstance(client, HttpClient)) # Check the object attributes values. # Here we check if the client object is properly initialized self.assertEqual(client.base_url, self.base_url) self.assertEqual(client.username, self.username) self.assertEqual(client.password, self.password) self.assertEqual(client.default_prefix, self.default_prefix) self.assertEqual(client.sessionkey, self.sessionkey) self.assertEqual(client.capath, self.capath) self.assertEqual(client.cafile, self.cafile) self.assertEqual(client.timeout, self.timeout) self.assertEqual(client.max_retry, self.max_retry) def test_redfish_client_no_root_resp(self) -> None: client = redfish_client(base_url=self.base_url, check_connectivity=False) self.assertIsNone(getattr(client, "root_resp", None)) @mock.patch("requests.Session.request") def test_redfish_client_root_object_initialized_after_login( self, mocked_request: mock.Mock ) -> None: dummy_root_data = '{"Links": {"Sessions": {"@data.id": "/redfish/v1/SessionService/Sessions"}}}' dummy_session_response = ( '{"@odata.type": "#Session.v1_1_2.Session", ' '"@odata.id": "/redfish/v1/SessionService/Sessions/1", ' '"Id": "1", "Name": "User Session", "Description": "Manager User Session", ' '"UserName": "user", "Oem": {}}' ) root_resp = mock.Mock(content=dummy_root_data, status_code=200) auth_resp = mock.Mock( content=dummy_session_response, status_code=200, headers={"location": "fake", "x-auth-token": "fake"}, ) mocked_request.side_effect = [ root_resp, auth_resp, ] client = redfish_client(base_url=self.base_url, check_connectivity=False) client.login() self.assertEqual(client.root, json.loads(dummy_root_data)) if __name__ == "__main__": unittest.main() python-redfish-3.2.7/tests/ris/000077500000000000000000000000001473240251500164305ustar00rootroot00000000000000python-redfish-3.2.7/tests/ris/__init__.py000066400000000000000000000003031473240251500205350ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.mdpython-redfish-3.2.7/tests/ris/test_config.py000066400000000000000000000033621473240251500213120ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md # -*- encoding: utf-8 -*- import tempfile import textwrap import unittest from redfish.ris.config import AutoConfigParser CONFIG = textwrap.dedent( """ [DEFAULT] ServerAliveInterval = 45 Compression = yes CompressionLevel = 9 ForwardX11 = yes [bitbucket.org] User = hg [topsecret.server.com] Port = 50022 ForwardX11 = no """ ) class TestAutoConfigParser(unittest.TestCase): def test_init(self): acp = AutoConfigParser() self.assertEqual(acp._configfile, None) with tempfile.TemporaryDirectory() as tmpdirname: cfgfile = "{tmpdir}/config.ini".format(tmpdir=tmpdirname) with open(cfgfile, "w+") as config: config.write(CONFIG) acp = AutoConfigParser(cfgfile) self.assertEqual(acp._configfile, cfgfile) def test_load(self): with tempfile.TemporaryDirectory() as tmpdirname: cfgfile = "{tmpdir}/config.ini".format(tmpdir=tmpdirname) with open(cfgfile, "w+") as config: config.write(CONFIG) acp = AutoConfigParser() acp.load(cfgfile) def test_save(self): with tempfile.TemporaryDirectory() as tmpdirname: cfgfile = "{tmpdir}/config.ini".format(tmpdir=tmpdirname) with open(cfgfile, "w+") as config: config.write(CONFIG) acp = AutoConfigParser(cfgfile) acp.load() acp.save() dump = "{tmpdir}/config2.ini".format(tmpdir=tmpdirname) acp.save(dump) python-redfish-3.2.7/tests/ris/test_ris.py000066400000000000000000000016611473240251500206420ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md # -*- encoding: utf-8 -*- import unittest from redfish.ris import RisMonolithMember_v1_0_0 from redfish.ris import RisMonolithMemberBase from redfish.ris.sharedtypes import Dictable class TestRisMonolithMemberBase(unittest.TestCase): def test_init(self): RisMonolithMemberBase() self.assertTrue(issubclass(RisMonolithMemberBase, Dictable)) class TestRisMonolithMember_v1_0_0(unittest.TestCase): def test_init(self): with self.assertRaises(TypeError): RisMonolithMember_v1_0_0() RisMonolithMember_v1_0_0("test") self.assertTrue( issubclass(RisMonolithMember_v1_0_0, RisMonolithMemberBase) ) self.assertTrue(issubclass(RisMonolithMember_v1_0_0, Dictable)) python-redfish-3.2.7/tests/ris/test_rmc_helper.py000066400000000000000000000015241473240251500221630ustar00rootroot00000000000000# Copyright Notice: # Copyright 2016-2021 DMTF. All rights reserved. # License: BSD 3-Clause License. For full text see link: # https://github.com/DMTF/python-redfish-library/blob/main/LICENSE.md import unittest try: from unittest import mock except ImportError: import mock from redfish.ris import rmc_helper class RmcHelper(unittest.TestCase): def setUp(self): super(RmcHelper, self).setUp() @mock.patch('redfish.rest.v1.HttpClient') def test_get_cache_dirname(self, mock_http_client): url = 'http://example.com' helper = rmc_helper.RmcClient(url=url, username='oper', password='xyz') mock_http_client.return_value.get_base_url.return_value = url dir_name = helper.get_cache_dirname() self.assertEqual(dir_name, 'example.com/') if __name__ == '__main__': unittest.main() python-redfish-3.2.7/tox.ini000066400000000000000000000005121473240251500160020ustar00rootroot00000000000000[tox] envlist = py38,py39,py311 [testenv] usedevelop = True install_command = pip install {opts} {packages} deps = coverage fixtures pytest -rrequirements.txt commands = pytest -v [testenv:pep8] basepython = python3 deps = flake8 commands = flake8 tests/ src/redfish/discovery [travis] python = 3.11: py311