├── requirements.txt ├── test_conf.json ├── AUTHORS.md ├── Dockerfile ├── LICENSE.md ├── CONTRIBUTING.md ├── CHANGELOG.md ├── .github └── workflows │ └── release.yml ├── README.md └── redfishMockupCreate.py /requirements.txt: -------------------------------------------------------------------------------- 1 | redfish>=2.1.0 2 | -------------------------------------------------------------------------------- /test_conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "command": "$interpreter redfishMockupCreate.py -r $target_system -u $username -p $password $secure -D mockup -d 'test mockup creation from Redfish-Test-Framework'" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Original Contribution: 2 | 3 | * Paul Vancil - Dell Inc. -- Dell Extreme Scale Infrastructure (ESI) Architecture Team 4 | 5 | # Other Key Contributions: 6 | 7 | * Joshua Giles - Dell Inc. -- Dell Extreme Scale Infrastructure (ESI) 8 | * Abhiram Ampabathina - Dell Inc. -- Dell Extreme Scale Infrastructure (ESI) Software Developer 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-slim 2 | 3 | # Install python requirements 4 | COPY requirements.txt /tmp/ 5 | RUN pip install --upgrade pip && \ 6 | pip install --no-cache-dir -r /tmp/requirements.txt 7 | 8 | # Copy server files 9 | COPY redfishMockupCreate.py /usr/src/app/ 10 | 11 | # Env settings 12 | WORKDIR /usr/src/app 13 | ENTRYPOINT ["python", "/usr/src/app/redfishMockupCreate.py", "-D", "/mockup"] 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2016-2024, Contributing Member(s) of Distributed Management Task 4 | Force, Inc.. All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its contributors 17 | may be used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Overview 4 | 5 | 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. 6 | 7 | ## Submitting Issues 8 | 9 | Bugs, feature requests, and questions are all submitted in the "Issues" section for the project. DMTF members are responsible for triaging and addressing issues. 10 | 11 | ## Contribution Process 12 | 13 | 1. Fork the repository. 14 | 2. Make and commit changes. 15 | 3. Make a pull request. 16 | 17 | 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). 18 | 19 | Pull requests are reviewed and approved by DMTF members. 20 | 21 | ## Developer Certificate of Origin 22 | 23 | All contributions must adhere to the [Developer Certificate of Origin (DCO)](http://developercertificate.org "http://developercertificate.org"). 24 | 25 | 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`. 26 | 27 | Full text of the DCO: 28 | 29 | ``` 30 | Developer Certificate of Origin 31 | Version 1.1 32 | 33 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 34 | 35 | Everyone is permitted to copy and distribute verbatim copies of this 36 | license document, but changing it is not allowed. 37 | 38 | 39 | Developer's Certificate of Origin 1.1 40 | 41 | By making a contribution to this project, I certify that: 42 | 43 | (a) The contribution was created in whole or in part by me and I 44 | have the right to submit it under the open source license 45 | indicated in the file; or 46 | 47 | (b) The contribution is based upon previous work that, to the best 48 | of my knowledge, is covered under an appropriate open source 49 | license and I have the right under that license to submit that 50 | work with modifications, whether created in whole or in part 51 | by me, under the same open source license (unless I am 52 | permitted to submit under a different license), as indicated 53 | in the file; or 54 | 55 | (c) The contribution was provided directly to me by some other 56 | person who certified (a), (b) or (c) and I have not modified 57 | it. 58 | 59 | (d) I understand and agree that this project and the contribution 60 | are public and that a record of the contribution (including all 61 | personal information I submit with it, including my sign-off) is 62 | maintained indefinitely and may be redistributed consistent with 63 | this project or the open source license(s) involved. 64 | ``` 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.2.0] - 2023-08-22 4 | - Added 'forcefolderrename' argument to replace characters that are disallowed in Windows folder names with underscores 5 | 6 | ## [1.1.9] - 2022-07-08 7 | - Made change to allow for the scheme to be included in the 'rhost' argument 8 | 9 | ## [1.1.8] - 2022-05-13 10 | - Version change only 11 | 12 | ## [1.1.4] - 2022-05-13 13 | - Add '@Redfish.ActionInfo' as a search item when crawling the service 14 | 15 | ## [1.1.3] - 2022-02-05 16 | - Added detection for Windows to rename directories to not use characters that conflict with folder name limitations 17 | 18 | ## [1.1.2] - 2021-09-17 19 | - Added --trace option to provide Redfish operation tracing between the tool and the service in the output directory 20 | - Added --maxlogentries option to limit the number of log entries obtained from each log service discovered 21 | 22 | ## [1.1.1] - 2021-04-23 23 | - Added capturing of response information when a resource cannot be parsed 24 | 25 | ## [1.1.0] - 2021-04-13 26 | - Allow for any encoding type 27 | 28 | ## [1.0.9] - 2021-01-11 29 | - Added Dockerfile 30 | 31 | ## [1.0.8] - 2020-12-04 32 | - Replaced internal copy of redfishtoollib to leverage the python-redfish-library 33 | 34 | ## [1.0.7] - 2020-10-30 35 | - Made enhancement to skip resources not containing `@odata.id` instead of crashing 36 | - Removed password from readme file generation 37 | 38 | ## [1.0.6] - 2020-10-19 39 | - Made enhancements to be able to create a mockup of a server with a malformed `/redfish` resource 40 | 41 | ## [1.0.5] - 2019-08-09 42 | - Added check to avoid parsing the resources that are referenced by Location properties 43 | 44 | ## [1.0.4] - 2019-06-21 45 | - Made change to allow for `/redfish` to not be present on the service, but report an error if not found 46 | 47 | ## [1.0.3] - 2018-11-30 48 | - Fixed checking of Uri tag in CSDL files hosted by a service 49 | 50 | ## [1.0.2] - 2018-09-21 51 | - Fixed bug in nextLink handling 52 | - Synched redfishtoolTransport.py with latest version from redfishtool 53 | 54 | ## [1.0.1] - 2018-08-03 55 | - Fixed print statement usage for when an error is encountered while accessing $metadata 56 | - Reformatted code to be PEP8 conformant 57 | 58 | ## [1.0.0] - 2018-02-02 59 | - Added support for getting time statistics when collecting resources 60 | - Added support for pulling the $metadata resource 61 | - Added support for pulling the JSON Schema repository 62 | - Added support for pulling OEM resources 63 | - Fixed @odata.type parsing to include support for no versioning 64 | 65 | ## [0.9.3] - 2017-06-05 66 | - Cleaning help options 67 | - Create directory if specified by "-D" if it is not empty 68 | - Add optional copyright, header and time information to the /redfish, /redfish/v1 and /redfish/v1/odata resources 69 | - Print status code if there is an error in retrieving redfish data with the transport 70 | - Wait time and timeout for Redfish transport have been changed to 5 and 20 respectively 71 | 72 | ## [0.9.2] - 2017-04-28 73 | - Support to recursively navigate tree instead of using static navigation structure 74 | - Added "--custom" option to support chosing custom navigation and updated structure to include all remaining navigation properties 75 | - Captures GET headers in headers.json for each api 76 | - Captures execution time in time.json for each api 77 | - Adds @redfish.copyright property to each resource; Can specify copyright with "-C ``" and longform "--Copyright" 78 | - Updated redfishtool library to 0.9.3 79 | 80 | ## [0.9.1] - 2016-09-06 81 | - Initial Public Release 82 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release and Publish 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: 'Version number' 7 | required: true 8 | changes_1: 9 | description: 'Change entry' 10 | required: true 11 | changes_2: 12 | description: 'Change entry' 13 | required: false 14 | changes_3: 15 | description: 'Change entry' 16 | required: false 17 | changes_4: 18 | description: 'Change entry' 19 | required: false 20 | changes_5: 21 | description: 'Change entry' 22 | required: false 23 | changes_6: 24 | description: 'Change entry' 25 | required: false 26 | changes_7: 27 | description: 'Change entry' 28 | required: false 29 | changes_8: 30 | description: 'Change entry' 31 | required: false 32 | jobs: 33 | release_build: 34 | name: Build the release 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v2 38 | with: 39 | token: ${{secrets.GITHUB_TOKEN}} 40 | - name: Build the changelog text 41 | run: | 42 | echo 'CHANGES<> $GITHUB_ENV 43 | echo "## [${{github.event.inputs.version}}] - $(date +'%Y-%m-%d')" >> $GITHUB_ENV 44 | echo "- ${{github.event.inputs.changes_1}}" >> $GITHUB_ENV 45 | if [[ -n "${{github.event.inputs.changes_2}}" ]]; then echo "- ${{github.event.inputs.changes_2}}" >> $GITHUB_ENV; fi 46 | if [[ -n "${{github.event.inputs.changes_3}}" ]]; then echo "- ${{github.event.inputs.changes_3}}" >> $GITHUB_ENV; fi 47 | if [[ -n "${{github.event.inputs.changes_4}}" ]]; then echo "- ${{github.event.inputs.changes_4}}" >> $GITHUB_ENV; fi 48 | if [[ -n "${{github.event.inputs.changes_5}}" ]]; then echo "- ${{github.event.inputs.changes_5}}" >> $GITHUB_ENV; fi 49 | if [[ -n "${{github.event.inputs.changes_6}}" ]]; then echo "- ${{github.event.inputs.changes_6}}" >> $GITHUB_ENV; fi 50 | if [[ -n "${{github.event.inputs.changes_7}}" ]]; then echo "- ${{github.event.inputs.changes_7}}" >> $GITHUB_ENV; fi 51 | if [[ -n "${{github.event.inputs.changes_8}}" ]]; then echo "- ${{github.event.inputs.changes_8}}" >> $GITHUB_ENV; fi 52 | echo "" >> $GITHUB_ENV 53 | echo 'EOF' >> $GITHUB_ENV 54 | - name: Update version numbers 55 | run: | 56 | sed -i -E 's/tool_version = .+/tool_version = "'${{github.event.inputs.version}}'"/' redfishMockupCreate.py 57 | - name: Update the changelog 58 | run: | 59 | ex CHANGELOG.md <" 69 | git add CHANGELOG.md redfishMockupCreate.py 70 | git commit -s -m "${{github.event.inputs.version}} versioning" 71 | git push origin main 72 | - name: Make the release 73 | env: 74 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 75 | run: | 76 | gh release create ${{github.event.inputs.version}} -t ${{github.event.inputs.version}} -n "Changes since last release:"$'\n\n'"$CHANGES" 77 | - name: Set up QEMU 78 | uses: docker/setup-qemu-action@v1 79 | - name: Set up Docker Buildx 80 | uses: docker/setup-buildx-action@v1 81 | - name: Login to Docker Hub 82 | uses: docker/login-action@v1 83 | with: 84 | username: ${{ secrets.DOCKER_USERNAME }} 85 | password: ${{ secrets.DOCKER_PASSWORD }} 86 | - name: Build Docker image and push 87 | id: docker_build 88 | uses: docker/build-push-action@v2 89 | with: 90 | context: . 91 | push: true 92 | tags: dmtf/redfish-mockup-creator:${{ github.event.inputs.version }},dmtf/redfish-mockup-creator:latest 93 | platforms: linux/arm64,linux/amd64 94 | - name: Image digest 95 | run: echo ${{ steps.docker_build.outputs.digest }} 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redfish Mockup Creator 2 | 3 | Copyright 2016-2020 DMTF. All rights reserved. 4 | 5 | ## About 6 | 7 | The Redfish Mockup Creator is a tool that creates a Redfish mockup from a live Redfish service. 8 | The mockup created can be used with the [Redfish Mockup Server](https://github.com/DMTF/Redfish-Mockup-Server). 9 | 10 | ## Requirements 11 | 12 | To run the mockup creator natively on your system: 13 | 14 | * Install [Python 3](https://www.python.org/downloads/ "https://www.python.org/downloads/") and [pip](https://pip.pypa.io/en/stable/installing/ "https://pip.pypa.io/en/stable/installing"). 15 | * Install required Python packages: `pip install -r requirements.txt` 16 | 17 | To run the mockup server as a Docker container: 18 | 19 | * Install [Docker](https://www.docker.com/get-started "https://www.docker.com/get-started"). 20 | 21 | ## Usage 22 | 23 | ``` 24 | usage: redfishMockupCreate.py [-h] --user USER --password PASSWORD --rhost 25 | RHOST [--Secure] [--Auth {None,Basic,Session}] 26 | [--Headers] [--Time] [--Dir DIR] 27 | [--Copyright COPYRIGHT] 28 | [--description DESCRIPTION] [--quiet] 29 | 30 | A tool to walk a Redfish service and create a mockup from all resources 31 | 32 | required arguments: 33 | --user USER, -u USER The user name for authentication 34 | --password PASSWORD, -p PASSWORD 35 | The password for authentication 36 | --rhost RHOST, -r RHOST 37 | The IP address (and port) of the Redfish service 38 | 39 | optional arguments: 40 | -h, --help show this help message and exit 41 | --Dir DIR, -D DIR Output directory for the mockup; defaults to 42 | 'rfMockUpDfltDir' 43 | --Secure, -S Use HTTPS for all operations 44 | --Auth {None,Basic,Session}, -A {None,Basic,Session} 45 | Authentication mode 46 | --Headers, -H Captures the response headers in the mockup 47 | --Time, -T Capture the time of each GET in the mockup 48 | --Copyright COPYRIGHT, -C COPYRIGHT 49 | Copyright string to add to each resource 50 | --description DESCRIPTION, -d DESCRIPTION 51 | Mockup description to add to the output readme file 52 | --quiet, -q Quiet mode; progress messages suppressed 53 | --trace, -trace Enable tracing; creates the file rf-mockup-create.log 54 | in the output directory to capture Redfish traces with 55 | the service 56 | --maxlogentries MAXLOGENTRIES, -maxlogentries MAXLOGENTRIES 57 | The maximum number of log entries to collect in each 58 | log service 59 | --forcefolderrename, -forcefolderrename 60 | Indicates if URIs containing characters that are 61 | disallowed in Windows folder names are renamed to 62 | replace the characters with underscores 63 | ``` 64 | 65 | Example: `python redfishMockupCreate.py -u root -p root -r 192.168.1.100 -S -D /output` 66 | 67 | The tool will log into the service specified by the *rhost* argument using the credentials provided by the *user* and *password* arguments. 68 | It will then walk the service to find all resources and place each resource in directory specified by the *Dir* argument. 69 | If *Dir* is not specified, the output will be "rfMockUpDfltDir". 70 | For every CSDL file discovered on the service, it will create an "index.xml" file in the output directory. 71 | For every JSON response from a URI, such as resources or error responses, it will create an "index.json" file in the output directory. 72 | For every non-JSON response from a URI, it will create an "error.txt" file in the output directory. 73 | If the *Headers* argument is specified, it will save the response headers for each resource in a "headers.json" file. 74 | If the *Time* argument is specified, it will save the time elapsed for each resource in a "time.json" file. 75 | 76 | Some implementations use URIs that contain characters that are disallowed in Windows folder names. 77 | The tool attempts to discover the OS to determine if these characters should be replaced by underscore characters. 78 | However, in some situations, such as a Docker container running under Windows, it's not possible for the tool to detect this condition. 79 | The *forcefolderrename* argument can be used in these cases to perform this replacement regardless of the detected OS. 80 | 81 | ## Docker container example 82 | 83 | To run as a Docker container, use one of these actions to pull or build the container: 84 | 85 | * Pull the container from Docker Hub: 86 | 87 | ```bash 88 | docker pull dmtf/redfish-mockup-creator:latest 89 | ``` 90 | * Build a container from local source: 91 | 92 | ```bash 93 | docker build -t dmtf/redfish-mockup-creator:latest . 94 | ``` 95 | * Build a container from GitHub: 96 | 97 | ```bash 98 | docker build -t dmtf/redfish-mockup-creator:latest https://github.com/DMTF/Redfish-Mockup-Creator.git#main 99 | ``` 100 | 101 | This command runs the container with a specified mockup, where `` is the path to the mockup directory: 102 | 103 | ```bash 104 | docker run --rm --user="$(id -u):$(id -g)" -v :/mockup dmtf/redfish-mockup-creator:latest -u root -p root -r 192.168.1.100 -S 105 | ``` 106 | 107 | ## Release Process 108 | 109 | 1. Go to the "Actions" page 110 | 2. Select the "Release and Publish" workflow 111 | 3. Click "Run workflow" 112 | 4. Fill out the form 113 | 5. Click "Run workflow" 114 | -------------------------------------------------------------------------------- /redfishMockupCreate.py: -------------------------------------------------------------------------------- 1 | # Copyright Notice: 2 | # Copyright 2016-2020 DMTF. All rights reserved. 3 | # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Mockup-Creator/blob/main/LICENSE.md 4 | 5 | """ 6 | Redfish Mockup Creator 7 | 8 | File : redfishMockupCreate.py 9 | 10 | Brief : This tool walks a service and creates a mockup from all resources 11 | """ 12 | 13 | import argparse 14 | import datetime 15 | import json 16 | import os 17 | import redfish 18 | import sys 19 | import time 20 | import xml.etree.ElementTree as ET 21 | import logging 22 | import copy 23 | import gc 24 | from redfish import redfish_logger 25 | 26 | # Version info 27 | tool_version = "1.2.0" 28 | 29 | # For Windows, there are restricted characters in folder names that could be used in URIs 30 | disallowed_folder_characters_win = [ ":", "*", "?", "\"", "<", ">", "|" ] 31 | folder_name_fix = False 32 | if sys.platform == "win32" or sys.platform == "cygwin": 33 | folder_name_fix = True 34 | 35 | def main(): 36 | """ 37 | Main entry point for the script 38 | """ 39 | 40 | # Get the input arguments 41 | argget = argparse.ArgumentParser( description = "A tool to walk a Redfish service and create a mockup from all resources" ) 42 | argget.add_argument( "--user", "-u", type = str, required = True, help = "The user name for authentication" ) 43 | argget.add_argument( "--password", "-p", type = str, required = True, help = "The password for authentication" ) 44 | argget.add_argument( "--rhost", "-r", type = str, required = True, help = "The IP address (and port) of the Redfish service" ) 45 | argget.add_argument( "--Dir", "-D", type = str, help = "Output directory for the mockup; defaults to 'rfMockUpDfltDir'", default = "rfMockUpDfltDir" ) 46 | argget.add_argument( "--Secure", "-S", action = "store_true", help = "Use HTTPS for all operations" ) 47 | argget.add_argument( "--Auth", "-A", type = str, help = "Authentication mode", choices = [ "None", "Basic", "Session" ], default = "Session" ) 48 | argget.add_argument( "--Headers", "-H", action = "store_true", help = "Captures the response headers in the mockup" ) 49 | argget.add_argument( "--Time", "-T", action = "store_true", help = "Capture the time of each GET in the mockup" ) 50 | argget.add_argument( "--Copyright", "-C", type = str, help = "Copyright string to add to each resource", default = None ) 51 | argget.add_argument( "--description", "-d", type = str, help = "Mockup description to add to the output readme file", default = "" ) 52 | argget.add_argument( "--quiet", "-q", action = "store_true", help = "Quiet mode; progress messages suppressed" ) 53 | argget.add_argument( "--trace", "-trace", action = "store_true", help = "Enable tracing; creates the file rf-mockup-create.log in the output directory to capture Redfish traces with the service" ) 54 | argget.add_argument( "--maxlogentries", "-maxlogentries", type = int, help = "The maximum number of log entries to collect in each log service" ) 55 | argget.add_argument( "--forcefolderrename", "-forcefolderrename", action = "store_true", help = "Indicates if URIs containing characters that are disallowed in Windows folder names are renamed to replace the characters with underscores" ) 56 | args, unknown = argget.parse_known_args() 57 | 58 | # Convert the authentication method to something usable with the Redfish library 59 | # This is needed for backwards compatibility with older versions of the tool 60 | if args.Auth == "Session": 61 | args.Auth = "session" 62 | else: 63 | args.Auth = "basic" 64 | 65 | # Build the base URL for the service 66 | # More backwards compatibility 67 | if "://" not in args.rhost: 68 | if args.Secure: 69 | args.rhost = "https://{}".format( args.rhost ) 70 | else: 71 | args.rhost = "http://{}".format( args.rhost ) 72 | 73 | # Set up the output 74 | if not os.path.isdir( args.Dir ): 75 | # Does not exist; make the directory 76 | try: 77 | os.makedirs( args.Dir ) 78 | except Exception as err: 79 | print( "ERROR: Aborting; could not create output directory '{}': {}".format( args.Dir, err ) ) 80 | sys.exit( 1 ) 81 | else: 82 | if len( os.listdir( args.Dir ) ) != 0: 83 | print( "ERROR: Aborting; output directory not empty..." ) 84 | sys.exit( 1 ) 85 | 86 | print( "Redfish Mockup Creator, Version {}".format( tool_version ) ) 87 | print( "Address: {}".format( args.rhost ) ) 88 | print( "Full Output Path: {}".format( os.path.abspath( args.Dir ) ) ) 89 | print( "Description: {}".format( args.description ) ) 90 | print( "Starting mockup creation..." ) 91 | if args.quiet: 92 | print( "Quiet mode enabled; please wait..." ) 93 | 94 | # Create the readme file 95 | try: 96 | with open( os.path.join( args.Dir, "README" ), "w" ) as readf: 97 | readf.write( "Redfish service captured by the Redfish Mockup Creator, Version {}\n".format( tool_version ) ) 98 | readf.write( "Created: {}\n".format( datetime.datetime.now().strftime( "%Y-%m-%d %H:%M:%S" ) ) ) 99 | readf.write( "Service: {}\n".format( args.rhost ) ) 100 | readf.write( "User: {}\n".format( args.user ) ) 101 | readf.write( "Description: {}\n".format( args.description ) ) 102 | except Exception as err: 103 | print( "ERROR: Aborting; could not create README file in output directory: {}".format( err ) ) 104 | sys.exit( 1 ) 105 | 106 | # Set up the trace file if requested 107 | if args.trace: 108 | redfish_logger( os.path.join( args.Dir, "rf-mockup-create.log" ), "%(asctime)s - %(name)s - %(levelname)s - %(message)s", logging.DEBUG ) 109 | 110 | # Set up the Redfish object 111 | try: 112 | redfish_obj = redfish.redfish_client( base_url = args.rhost, username = args.user, password = args.password ) 113 | redfish_obj.login( auth = args.Auth ) 114 | except Exception as err: 115 | print( "ERROR: Aborting; could not authenticate with the Redfish service: {}".format( err ) ) 116 | sys.exit( 1 ) 117 | 118 | # Scan the service 119 | response_times = {} 120 | scan_resource( redfish_obj, args, response_times, "/redfish" ) 121 | scan_resource( redfish_obj, args, response_times, "/redfish/v1/odata" ) 122 | scan_resource( redfish_obj, args, response_times, "/redfish/v1/$metadata", is_csdl = True ) 123 | scan_resource( redfish_obj, args, response_times, "/redfish/v1" ) 124 | redfish_obj.logout() 125 | 126 | # Add time statistics to the readme 127 | total_response_time = sum( response_times.values() ) 128 | average_response_time = total_response_time / len( response_times ) 129 | min_response_uri = min( response_times, key = response_times.get ) 130 | max_response_uri = max( response_times, key = response_times.get ) 131 | with open( os.path.join( args.Dir, "README" ), "a" ) as readf: 132 | readf.write( "Total response time: {}\n".format( total_response_time ) ) 133 | readf.write( "Average response time: {}\n".format( average_response_time ) ) 134 | readf.write( "Minimum response time: {}, {}\n".format( response_times[min_response_uri], min_response_uri ) ) 135 | readf.write( "Maximum response time: {}, {}\n".format( response_times[max_response_uri], max_response_uri ) ) 136 | 137 | print( "Completed mockup creation!" ) 138 | 139 | def scan_resource( redfish_obj, args, response_times, uri, is_csdl = False ): 140 | """ 141 | Scans a resource and saves its response 142 | 143 | Args: 144 | redfish_obj: The Redfish client object with an open session 145 | args: The command line arguments 146 | response_times: The response times database 147 | uri: The URI to get 148 | is_csdl: Indicates if the resource is a CSDL file 149 | """ 150 | 151 | # Check if the URI is a relative URI 152 | if not uri.startswith( "/" ): 153 | return 154 | 155 | # Set up the output folder 156 | try: 157 | path = uri[1:] 158 | if folder_name_fix or args.forcefolderrename: 159 | for character in disallowed_folder_characters_win: 160 | path = path.replace( character, "_" ) 161 | path = os.path.join( args.Dir, path ) 162 | if not os.path.isdir( path ): 163 | # Does not exist; make the directory 164 | os.makedirs( path ) 165 | except Exception as err: 166 | print( "ERROR: Could not create directory for '{}': {}".format( uri, err ) ) 167 | return 168 | 169 | # Check if the index file already exists 170 | index_name = "index.json" 171 | if is_csdl: 172 | index_name = "index.xml" 173 | index_path = os.path.join( path, index_name ) 174 | if os.path.isfile( index_path ): 175 | # File exists; already scanned this resource 176 | return 177 | 178 | # Get the resource 179 | if not args.quiet: 180 | print( "Getting {}...".format( uri ) ) 181 | try: 182 | start_time = time.time() 183 | resource = redfish_obj.get( uri, headers = { "Accept-Encoding": "*" } ) 184 | end_time = time.time() 185 | except Exception as err: 186 | print( "ERROR: Could not get '{}': {}".format( uri, err ) ) 187 | return 188 | 189 | # Save the resource and other information 190 | try: 191 | # Save the resource itself 192 | if is_csdl: 193 | with open( index_path, "w", encoding = "utf-8" ) as file: 194 | file.write( resource.text ) 195 | else: 196 | save_dict = resource.dict 197 | 198 | # Prune the log entry collection if needed 199 | if save_dict.get( "@odata.type", None ) == "#LogEntryCollection.LogEntryCollection" and args.maxlogentries is not None: 200 | if args.maxlogentries < 0: 201 | args.maxlogentries = 0 202 | if "Members@odata.nextLink" in save_dict: 203 | save_dict.pop( "Members@odata.nextLink" ) 204 | if "Members" in save_dict: 205 | if isinstance( save_dict["Members"], list ): 206 | for i in range( 0, len( save_dict["Members"] ) - args.maxlogentries ): 207 | save_dict["Members"].pop() 208 | save_dict["Members@odata.count"] = len( save_dict["Members"] ) 209 | 210 | # The saved copy might contain URI fixes and other changes that aren't reflective of the service, but are 211 | # needed to ensure compatibility with the system creating the mockup 212 | scan_dict = copy.deepcopy( save_dict ) 213 | 214 | # Add the copyright statement if needed 215 | if args.Copyright: 216 | save_dict["@Redfish.Copyright"] = args.Copyright 217 | 218 | # Update the payload's URIs if they need to be corrected based on allowable folder names for the system 219 | if folder_name_fix or args.forcefolderrename: 220 | fix_uris( save_dict ) 221 | 222 | with open( index_path, "w", encoding = "utf-8" ) as file: 223 | json.dump( save_dict, file, indent = 4, separators = ( ",", ": " ) ) 224 | 225 | # Deep copies of all payloads gets expensive; force garbage collection to avoid stack overflows 226 | del save_dict 227 | gc.collect() 228 | except Exception as err: 229 | print( "ERROR: Could not save '{}': {}".format( uri, err ) ) 230 | print( "Attempting to save response data in error.txt..." ) 231 | try: 232 | with open( os.path.join( path, "error.txt" ), "w", encoding = "utf-8" ) as file: 233 | file.write( "HTTP {}\n".format( resource.status ) ) 234 | for header in resource.getheaders(): 235 | file.write( "{}: {}\n".format( header[0], header[1] ) ) 236 | file.write( "\n" ) 237 | file.write( resource.text ) 238 | except: 239 | print( "Could not save response data; moving on... " ) 240 | return 241 | 242 | # Save additional info 243 | try: 244 | # Save headers 245 | if args.Headers: 246 | with open( os.path.join( path, "headers.json" ), "w", encoding = "utf-8" ) as file: 247 | headers_dict = {} 248 | for header in resource.getheaders(): 249 | headers_dict[header[0]] = header[1] 250 | json.dump( { "GET": headers_dict }, file, indent = 4, separators = ( ",", ": " ) ) 251 | 252 | # Save timing info 253 | response_times[uri] = end_time - start_time 254 | if args.Time: 255 | with open( os.path.join( path, "time.json" ), "w", encoding = "utf-8" ) as file: 256 | json.dump( { "GET_Time": "{0:.2f}".format( response_times[uri] ) }, file, indent = 4, separators = ( ",", ": " ) ) 257 | except Exception as err: 258 | print( "ERROR: Could not save header or timing data for '{}': {}".format( uri, err ) ) 259 | return 260 | 261 | # Scan the response to see where to go next 262 | try: 263 | if is_csdl: 264 | scan_csdl( redfish_obj, args, response_times, resource.text ) 265 | else: 266 | scan_object( redfish_obj, args, response_times, scan_dict ) 267 | except Exception as err: 268 | print( "ERROR: Could not scan '{}': {}".format( uri, err ) ) 269 | return 270 | 271 | def scan_object( redfish_obj, args, response_times, object ): 272 | """ 273 | Scans an object or array to find links to other resources 274 | 275 | Args: 276 | redfish_obj: The Redfish client object with an open session 277 | args: The command line arguments 278 | response_times: The response times database 279 | object: The object to scan 280 | """ 281 | 282 | for item in object: 283 | # If the object is a dictionary, inspect the properties found 284 | if isinstance( object, dict ): 285 | # If the item is a reference, go to the resource 286 | if item == "@odata.id" or item == "Uri" or item == "Members@odata.nextLink" or item == "@Redfish.ActionInfo": 287 | if isinstance( object[item], str ): 288 | if object[item].startswith( "/" ) and "#" not in object[item]: 289 | scan_resource( redfish_obj, args, response_times, object[item] ) 290 | 291 | # If the item is an object or array, scan one level deeper 292 | elif isinstance( object[item], dict ) or isinstance( object[item], list ): 293 | scan_object( redfish_obj, args, response_times, object[item] ) 294 | 295 | # If the object is a list, see if the member needs to be scanned 296 | elif isinstance( object, list ): 297 | if isinstance( item, dict ) or isinstance( item, list ): 298 | scan_object( redfish_obj, args, response_times, item ) 299 | 300 | def scan_csdl( redfish_obj, args, response_times, csdl ): 301 | """ 302 | Scans a CSDL string to find links to other CSDL files 303 | 304 | Args: 305 | redfish_obj: The Redfish client object with an open session 306 | args: The command line arguments 307 | response_times: The response times database 308 | csdl: The CSDL string to scan 309 | """ 310 | 311 | # Convert to an element tree object 312 | tree = ET.ElementTree( ET.fromstring( csdl ) ) 313 | root = tree.getroot() 314 | 315 | # Find references 316 | for reference in root: 317 | if "reference" in str( reference.tag ).lower(): 318 | if "Reference" not in str( reference.tag ): 319 | print( "Warning: Found invalid reference tag '()'; tags are case sensitive!".format( str( reference.tag ) ) ) 320 | for tag in [ "Uri", "uri", "URI" ]: 321 | uri = reference.attrib.get( tag ) 322 | if uri is not None: 323 | if tag != "Uri": 324 | print( "Warning: Found invalid Uri attribute '{}'; attributes are case sensitive!".format( tag ) ) 325 | # Scan the reference 326 | scan_resource( redfish_obj, args, response_times, uri, is_csdl = True ) 327 | 328 | def fix_uris( payload ): 329 | """ 330 | Updates URIs in a payload to ensure they do not conflict with local system folder name rules 331 | 332 | Args: 333 | payload: The payload to update 334 | """ 335 | 336 | for item in payload: 337 | # If the payload is a dictionary, inspect the properties found 338 | if isinstance( payload, dict ): 339 | # If the item is a reference, go to the resource 340 | if item == "@odata.id" or item == "Uri" or item == "Members@odata.nextLink": 341 | if isinstance( payload[item], str ): 342 | for character in disallowed_folder_characters_win: 343 | payload[item] = payload[item].replace( character, "_" ) 344 | 345 | # If the item is an object or array, scan one level deeper 346 | elif isinstance( payload[item], dict ) or isinstance( payload[item], list ): 347 | fix_uris( payload[item] ) 348 | 349 | # If the object is a list, see if the member needs to be scanned 350 | elif isinstance( payload, list ): 351 | if isinstance( item, dict ) or isinstance( item, list ): 352 | fix_uris( item ) 353 | 354 | if __name__ == "__main__": 355 | sys.exit( main() ) 356 | --------------------------------------------------------------------------------