├── .github └── workflows │ └── release.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── RedfishEventListener_v1.py ├── cert.pem ├── config.ini ├── requirements.txt └── server.key /.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}}\''/' RedfishEventListener_v1.py 57 | - name: Update the changelog 58 | run: | 59 | ex CHANGELOG.md <" 73 | git add * 74 | git commit -s -m "${{github.event.inputs.version}} versioning" 75 | git push origin main 76 | - name: Make the release 77 | env: 78 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 79 | run: | 80 | gh release create ${{github.event.inputs.version}} -t ${{github.event.inputs.version}} -n "Changes since last release:"$'\n\n'"$CHANGES" 81 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.1.6] - 2024-10-25 4 | - Migrated to Python's HTTP server to avoid manual socket handling 5 | 6 | ## [1.1.5] - 2024-08-02 7 | - Fixed length check when reading data from incoming events 8 | 9 | ## [1.1.4] - 2024-06-28 10 | - Updated embedded TLS certificate to no longer be expired 11 | - Updated tool to no longer require 'http_parser' module that is no longer maintained 12 | 13 | ## [1.1.3] - 2023-02-24 14 | - Modified the listener to respond with HTTP 204 in favor of HTTP 200 since no response body is sent 15 | 16 | ## [1.1.2] - 2022-06-02 17 | - Corrected usage of 'lower' method for the authentication type 18 | 19 | ## [1.1.1] - 2022-04-08 20 | - Added 'Connection: close' header to responses 21 | 22 | ## [1.1.0] - 2022-02-28 23 | - Added IPv6 support 24 | - Replaced internal HTTP calls with the 'redfish-utilities' module 25 | - Added support for newer event subscription parameters, such as subscribing based on registries and resource types 26 | - Added support for configuring the event format to receive 27 | 28 | ## [1.0.2] - 2019-03-26 29 | - Added support for receiving Metric Reports 30 | 31 | ## [1.0.1] - 2018-03-30 32 | - Added missing CRLF in the HTTP response 33 | - Added option to control the use of HTTP or HTTPS 34 | - Added option to specify the listening port 35 | 36 | ## [1.0.0] - 2017-10-25 37 | - Initial Public Release 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017-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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redfish Event Listener 2 | 3 | Copyright 2017-2024 DMTF. All rights reserved. 4 | 5 | ## About 6 | 7 | The Redfish Event Listener is a lightweight HTTP(S) server that can be deployed to read and record events from Redfish services. It can also subscribe to multiple services. 8 | 9 | ## Prerequisites 10 | 11 | The Redfish Event Listener requires Python3 on the user's system. Additionally, the following Python packages are required: 12 | 13 | * redfish 14 | * redfish_utilities 15 | 16 | To install the required packages, use either command: 17 | 18 | `pip install ` 19 | `pip install -r requirements.txt` 20 | 21 | To upgrade already installed packages, use the command: 22 | 23 | `pip install --upgrade ` 24 | 25 | The target Redfish services must also be configured to send Redfish events. 26 | 27 | ## Configuration 28 | 29 | The file `config.ini` contains a default configuration for the tool. This can be used as a template for real configurations or edited as needed. [Sample Configuration with All Options](#sample-configuration-with-all-options) shows an example with all possible configuration options. 30 | 31 | The configuration file is broken into different sections with different options. 32 | 33 | The `Information` section contains details about the configuration file itself. The tool does not parse any information in this section, and is used to help users track configuration files. The following options are found in this section: 34 | 35 | * `Updated`: The date the file was updated. 36 | * `Description`: A description of the configuration. 37 | 38 | The `SystemInformation` section contains settings for the system acting as the event listener. All options in this section are required. The following options are found in this section: 39 | 40 | * `ListenerIP`: The interface to use to listen for events. This is typically `0.0.0.0` to listen on all interfaces for IPv4 or `::` to listen on all interfaces for IPv6. 41 | * `ListenerPort`: The port number to use to listen for events. This is typically `80` for HTTP or `443` for HTTPS. 42 | * `UseSSL`: `on` to use HTTPS, `off` to use HTTP for incoming event messages. If HTTPS is to be used, the `CertificateDetails` section will need to be configured. 43 | 44 | The `CertificateDetails` section contains the certificate information for the system acting as the event listener. This section and its options are only required if `UseSSL` in `SystemInformation` is set to `on`. The following options are found in this section: 45 | 46 | * `certfile`: The file containing the certificate for the event listener. The tool comes with a default self-signed certificate named `cert.pem`. 47 | * `keyfile`: The file containing the certificate private key for the event listener. The tool comes with a default private key named `server.key`. 48 | 49 | The `SubscriptionDetails` section contains subscription information so that the Redfish service can send the desired events to the event listener. The `Destination` option is required; other options can be omitted or have an empty value. The following options are found in this section: 50 | 51 | * `Destination`: The URI of the event listener. The Redfish service will perform an HTTP POST on this URI to send events to the event listener. If provided, the tool will apply the value to the `Destination` property in the `EventDestination` resource. 52 | * `Context`: The context string to be transmitted back by the Redfish service when sending events. If provided, the tool will apply the value to the `Context` property in the `EventDestination` resource. 53 | * `Format`: The format of the event notifications the event listener will receive. Possible values are `Event` and `MetricReport`. If provided, the tool will apply the value to the `EventFormatType` property in the `EventDestination` resource. 54 | * `Expand`: `true` indicates if the `OriginOfCondition` property will contain the expanded resource in the event payload. `false` indicates no payload expansion. If provided, the tool will apply the value to the `IncludeOriginOfCondition` property in the `EventDestination` resource. 55 | * `ResourceTypes`: An array of resource types the Redfish service will use to filter events for the event listener. Some examples include `Chassis`, `Manager`, and `ComputerSystem`. If provided, the tool will apply the value to the `ResourceTypes` property in the `EventDestination` resource. 56 | * `Registries`: An array of registry names the Redfish service will use to filter events for the event listener. Some examples include `ResourceEvent` and `TaskEvent`. If provided, the tool will apply the value to the `RegistryPrefixes` property in the `EventDestination` resource. 57 | * `EventTypes`: An array of classes of events the event listener will receive. This setting has been deprecated by Redfish in favor of the above settings. Possible values are `StatusChange`, `ResourceUpdated`, `ResourceAdded`, `ResourceRemoved`, `Alert`, `MetricReport`, and `Other`. If provided, the tool will apply the value to the `EventTypes` property in the `EventDestination` resource. 58 | 59 | The `ServerInformation` section contains information about the Redfish services that will transmit events to the event listener. All options in this section, except `LoginType`, are required. The following options are found in this section: 60 | 61 | * `ServerIPs`: An array of URIs of the Redfish services for subscribing. 62 | * `UserNames`: An array of Redfish usernames for each of the listed Redfish services. This must be the same array length as `ServerIPs`. 63 | * `Passwords`: An array of Redfish passwords for each of the listed Redfish services. This must be the same array length as `ServerIPs`. 64 | * `LoginType`: An array of login types for each of the listed Redfish services. Possible values are `Basic` and `Session`. If not specified, `Session` is used. 65 | 66 | ## Running the Tool 67 | 68 | Execute using `python RedfishEventListener_v1.py` 69 | 70 | Received event details will be captured on the console and recorded into a file named Events_.txt in the working directory. Individual files will be generated for each subscribed service. 71 | 72 | The tool can be stopped by issuing a keyboard interrupt (CTRL-C). 73 | 74 | ## Limitations 75 | 76 | * The subscription information remains the same for all the subscriptions initiated from the tool. 77 | * The event counter will restart in the event file each time the tool is restarted. 78 | 79 | ## Sample Configuration with All Options 80 | 81 | The following example shows a configuration file with all possible options supported by the tool. Many Redfish services do not support every possible option, so if a service is returning the HTTP `400 Bad Request` status code, inspect the error message to determine which options are not supported, and modify the configuration file accordingly. 82 | 83 | ``` 84 | [Information] 85 | Updated = February 24, 2023 86 | Description = Redfish Event Listener Tool Full Config 87 | 88 | [SystemInformation] 89 | ListenerIP = 0.0.0.0 90 | ListenerPort = 443 91 | UseSSL = on 92 | 93 | [CertificateDetails] 94 | certfile = cert.pem 95 | keyfile = server.key 96 | 97 | [SubscriptionDetails] 98 | Destination = https:/// 99 | EventTypes = [ 100 | "Alert", 101 | "ResourceRemoved", 102 | "ResourceAdded" , 103 | "ResourceUpdated", 104 | "StatusChange"] 105 | Context = Public 106 | Format = Event 107 | Expand = false 108 | ResourceTypes = ["Chassis"] 109 | Registries = ["ResourceEvent"] 110 | 111 | [ServerInformation] 112 | ServerIPs = ["https://","https://"] 113 | UserNames = ["Username1","Username2"] 114 | Passwords = ["Password1","Password2"] 115 | LoginType = ["Session","Basic"] 116 | 117 | ``` 118 | 119 | ## Release Process 120 | 121 | 1. Go to the "Actions" page 122 | 2. Select the "Release and Publish" workflow 123 | 3. Click "Run workflow" 124 | 4. Fill out the form 125 | 5. Click "Run workflow" 126 | -------------------------------------------------------------------------------- /RedfishEventListener_v1.py: -------------------------------------------------------------------------------- 1 | # Copyright Notice: 2 | # Copyright 2017-2024 DMTF. All rights reserved. 3 | # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Event-Listener/blob/main/LICENSE.md 4 | 5 | import traceback 6 | import logging 7 | import json 8 | import ssl 9 | import sys 10 | import signal 11 | import re 12 | from datetime import datetime 13 | import argparse 14 | from redfish import redfish_client 15 | import redfish_utilities 16 | from http.server import BaseHTTPRequestHandler, HTTPServer 17 | 18 | my_logger = logging.getLogger() 19 | my_logger.setLevel(logging.DEBUG) 20 | standard_out = logging.StreamHandler(sys.stdout) 21 | standard_out.setLevel(logging.INFO) 22 | my_logger.addHandler(standard_out) 23 | 24 | tool_version = '1.1.6' 25 | 26 | config = { 27 | 'listenerip': '0.0.0.0', 28 | 'listenerport': 443, 29 | 'usessl': True, 30 | 'certfile': 'cert.pem', 31 | 'keyfile': 'server.key', 32 | 'destination': 'https://contoso.com', 33 | 'eventtypes': None, 34 | 'contextdetail': None, 35 | 'serverIPs': [], 36 | 'usernames': [], 37 | 'passwords': [], 38 | "logintype": [], 39 | 'verbose': False, 40 | 'format': None, 41 | 'expand': None, 42 | 'resourcetypes': None, 43 | 'registries': None 44 | } 45 | 46 | event_count = {} 47 | 48 | class RedfishEventListenerServer(BaseHTTPRequestHandler): 49 | """ 50 | Redfish Event Listener Server 51 | Handles HTTP requests 52 | """ 53 | 54 | def do_POST(self): 55 | # Check for the content length 56 | try: 57 | length = int(self.headers["content-length"]) 58 | except: 59 | my_logger.error("{} - No Content-Length header".format(self.client_address[0])) 60 | self.send_response(411) 61 | self.send_header("Content-Length", "0") 62 | self.end_headers() 63 | return 64 | 65 | # Read the data 66 | try: 67 | payload = json.loads(self.rfile.read(length).decode("utf-8")) 68 | except: 69 | my_logger.error("{} - No data received or data is not JSON".format(self.client_address[0])) 70 | self.send_response(400) 71 | self.send_header("Content-Length", "0") 72 | self.end_headers() 73 | return 74 | 75 | my_logger.info("") 76 | my_logger.info("Event received from {}".format(self.client_address[0])) 77 | 78 | # Print out the events 79 | if 'Events' in payload and config['verbose']: 80 | event_array = payload['Events'] 81 | for event in event_array: 82 | my_logger.info(" EventType: %s", event.get('EventType')) 83 | my_logger.info(" MessageId: %s", event.get('MessageId')) 84 | if 'EventId' in event: 85 | my_logger.info(" EventId: %s", event['EventId']) 86 | if 'EventGroupId' in event: 87 | my_logger.info(" EventGroupId: %s", event['EventGroupId']) 88 | if 'EventTimestamp' in event: 89 | my_logger.info(" EventTimestamp: %s", event['EventTimestamp']) 90 | if 'Severity' in event: 91 | my_logger.info(" Severity: %s", event['Severity']) 92 | if 'MessageSeverity' in event: 93 | my_logger.info(" MessageSeverity: %s", event['MessageSeverity']) 94 | if 'Message' in event: 95 | my_logger.info(" Message: %s", event['Message']) 96 | if 'MessageArgs' in event: 97 | my_logger.info(" MessageArgs: %s", event['MessageArgs']) 98 | if 'Context' in payload: 99 | my_logger.info(" Context: %s", payload['Context']) 100 | my_logger.info("") 101 | if 'MetricValues' in payload and config['verbose']: 102 | metric_array = payload['MetricValues'] 103 | my_logger.info(" Metric Report Name: %s", payload.get('Name')) 104 | for metric in metric_array: 105 | my_logger.info(" MetricId: %s", metric.get('MetricId')) 106 | my_logger.info(" MetricValue: %s", metric.get('MetricValue')) 107 | my_logger.info(" Timestamp: %s", metric.get('Timestamp')) 108 | if 'MetricProperty' in metric: 109 | my_logger.info(" MetricProperty is: %s", metric['MetricProperty']) 110 | my_logger.info("\n") 111 | 112 | # Update the timestamp log 113 | with open("TimeStamp.log", 'a') as f: 114 | if 'EventTimestamp' in payload: 115 | try: 116 | receTime = datetime.now() 117 | sentTime = datetime.strptime(payload['EventTimestamp'], "%Y-%m-%d %H:%M:%S.%f") 118 | f.write("%s %s %sms\n" % ( 119 | sentTime.strftime("%Y-%m-%d %H:%M:%S.%f"), receTime, (receTime - sentTime).microseconds / 1000)) 120 | except: 121 | f.write('Timestamp not in the correct format.\n') 122 | else: 123 | f.write('No available timestamp.\n') 124 | 125 | # Log the event 126 | outputfile = "Events_" + self.client_address[0] + ".txt" 127 | try: 128 | if event_count.get(self.client_address[0]): 129 | event_count[self.client_address[0]] = event_count[self.client_address[0]] + 1 130 | else: 131 | event_count[self.client_address[0]] = 1 132 | 133 | my_logger.info("Event Counter for Host %s = %s" % (self.client_address[0], event_count[self.client_address[0]])) 134 | my_logger.info("") 135 | fd = open(outputfile, "a") 136 | fd.write("Time:%s Count:%s\nHost IP:%s\nEvent Details:%s\n" % ( 137 | datetime.now(), event_count[self.client_address[0]], self.client_address[0], json.dumps(payload))) 138 | fd.close() 139 | except Exception: 140 | my_logger.info(traceback.print_exc()) 141 | 142 | self.send_response(204) 143 | self.send_header("Content-Length", "0") 144 | self.end_headers() 145 | 146 | 147 | if __name__ == '__main__': 148 | """ 149 | Main program 150 | """ 151 | 152 | # Print the tool banner 153 | logging.info('Redfish Event Listener v{}'.format(tool_version)) 154 | 155 | argget = argparse.ArgumentParser(description='Redfish Event Listener (v{}) is a tool that deploys an HTTP(S) server to read and record events from Redfish services.'.format(tool_version)) 156 | 157 | # config 158 | argget.add_argument('-c', '--config', type=str, default='./config.ini', help='Location of the configuration file; default: "./config.ini"') 159 | argget.add_argument('-v', '--verbose', action='count', default=0, help='Verbose output') 160 | args = argget.parse_args() 161 | 162 | # Initiate Configuration File 163 | from configparser import ConfigParser 164 | parsed_config = ConfigParser() 165 | parsed_config.read(args.config) 166 | 167 | # Inline helper to help parse lists into arrays 168 | def parse_list(string: str): 169 | string = string.strip() 170 | if re.fullmatch(r'\[\s*\]', string.strip()) or len(string.strip()) == 0: 171 | return [] 172 | if string[0] == '[' and string[-1] == ']': 173 | string = string.strip('[]') 174 | return [x.strip().strip("'\"") for x in string.split(',')] 175 | 176 | # Host Info 177 | config['listenerip'] = parsed_config.get('SystemInformation', 'ListenerIP') 178 | config['listenerport'] = parsed_config.getint('SystemInformation', 'ListenerPort') 179 | config['usessl'] = parsed_config.getboolean('SystemInformation', 'UseSSL') 180 | 181 | # Cert Info 182 | if config['usessl']: 183 | config['certfile'] = parsed_config.get('CertificateDetails', 'certfile') 184 | config['keyfile'] = parsed_config.get('CertificateDetails', 'keyfile') 185 | 186 | # Subscription Details 187 | # Note: Older versions of the tool contained a spelling error for 'Subscription'; need to support both variants to maintain compatibility with older config files 188 | if parsed_config.has_section("SubsciptionDetails") and parsed_config.has_section("SubscriptionDetails"): 189 | my_logger.error('Use either SubsciptionDetails or SubscriptionDetails in config, not both.') 190 | sys.exit(1) 191 | my_config_key = "SubsciptionDetails" if parsed_config.has_section("SubsciptionDetails") else "SubscriptionDetails" 192 | config['destination'] = parsed_config.get(my_config_key, 'Destination') 193 | if parsed_config.has_option(my_config_key, 'Context'): 194 | config['contextdetail'] = parsed_config.get(my_config_key, 'Context') 195 | if parsed_config.has_option(my_config_key, 'EventTypes'): 196 | config['eventtypes'] = parse_list(parsed_config.get(my_config_key, 'EventTypes')) 197 | if parsed_config.has_option(my_config_key, 'Format'): 198 | config['format'] = parsed_config.get(my_config_key, 'Format') 199 | if parsed_config.has_option(my_config_key, 'Expand'): 200 | config['expand'] = parsed_config.get(my_config_key, 'Expand') 201 | if parsed_config.has_option(my_config_key, 'ResourceTypes'): 202 | config['resourcetypes'] = parse_list(parsed_config.get(my_config_key, 'ResourceTypes')) 203 | if parsed_config.has_option(my_config_key, 'Registries'): 204 | config['registries'] = parse_list(parsed_config.get(my_config_key, 'Registries')) 205 | for k in ['format', 'expand', 'resourcetypes', 'registries', 'contextdetail', 'eventtypes']: 206 | if config[k] in ['', [], None]: 207 | config[k] = None 208 | 209 | # Subscription Targets 210 | config['serverIPs'] = parse_list(parsed_config.get('ServerInformation', 'ServerIPs')) 211 | config['usernames'] = parse_list(parsed_config.get('ServerInformation', 'UserNames')) 212 | config['passwords'] = parse_list(parsed_config.get('ServerInformation', 'Passwords')) 213 | config['logintype'] = ['Session' for x in config['serverIPs']] 214 | if parsed_config.has_option('ServerInformation', 'LoginType'): 215 | config['logintype'] = parse_list(parsed_config.get('ServerInformation', 'LoginType')) 216 | config['logintype'] += ['Session'] * (len(config['serverIPs']) - len(config['logintype'])) 217 | 218 | # Other Info 219 | config['verbose'] = args.verbose 220 | if config['verbose']: 221 | print(json.dumps(config, indent=4)) 222 | 223 | # Perform the Subscription if provided 224 | target_contexts = [] 225 | if not (len(config['serverIPs']) == len(config['usernames']) == len(config['passwords'])): 226 | my_logger.error("Number of ServerIPs does not match UserNames, Passwords, or LoginTypes") 227 | sys.exit(1) 228 | elif len(config['serverIPs']) == 0: 229 | my_logger.info("No subscriptions are specified. Continuing with Listener.") 230 | else: 231 | # Create the subscriptions on the Redfish services provided 232 | for dest, user, passwd, logintype in zip(config['serverIPs'], config['usernames'], config['passwords'], config['logintype']): 233 | try: 234 | # Log in to the service 235 | my_logger.info("ServerIP:: {}".format(dest)) 236 | my_logger.info("UserName:: {}".format(user)) 237 | my_ctx = redfish_client(dest, user, passwd, timeout=30) 238 | my_ctx.login(auth=logintype.lower()) 239 | 240 | # Create the subscription 241 | response = redfish_utilities.create_event_subscription(my_ctx, config['destination'], 242 | client_context=config['contextdetail'], 243 | event_types=config['eventtypes'], 244 | format=config['format'], 245 | expand=config['expand'], 246 | resource_types=config['resourcetypes'], 247 | registries=config['registries']) 248 | 249 | # Save the subscription info for deleting later 250 | my_location = response.getheader('Location') 251 | my_logger.info("Subscription is successful for {}, {}".format(dest, my_location)) 252 | unsub_id = None 253 | try: 254 | # Response bodies are expected to have the event destination 255 | unsub_id = response.dict['Id'] 256 | except Exception: 257 | # Fallback to determining the Id from the Location header 258 | if my_location is not None: 259 | unsub_id = my_location.strip('/').split('/')[-1] 260 | if unsub_id is None: 261 | my_logger.error('{} did not provide a location for the subscription; cannot unsubscribe'.format(dest)) 262 | else: 263 | target_contexts.append((dest, my_ctx, unsub_id)) 264 | except Exception: 265 | my_logger.info('Unable to subscribe for events with {}'.format(dest)) 266 | my_logger.info(traceback.print_exc()) 267 | 268 | my_logger.info("Continuing with Listener.") 269 | 270 | event_server = HTTPServer((config['listenerip'], config['listenerport']), RedfishEventListenerServer) 271 | def clean_subscriptions(): 272 | for name, ctx, unsub_id in target_contexts: 273 | my_logger.info('\nClosing {}'.format(name)) 274 | try: 275 | redfish_utilities.delete_event_subscription(ctx, unsub_id) 276 | ctx.logout() 277 | except Exception: 278 | my_logger.info('Unable to unsubscribe for events with {}'.format(ctx.get_base_url())) 279 | my_logger.info(traceback.print_exc()) 280 | def sigterm_handler(signal_number, frame): 281 | my_logger.info("SIGTERM: Shutting down the Redfish Event Listener") 282 | event_server.server_close() 283 | clean_subscriptions() 284 | sys.exit(0) 285 | signal.signal(signal.SIGTERM, sigterm_handler) 286 | if config['usessl']: 287 | context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 288 | context.load_cert_chain(certfile=config['certfile'], keyfile=config['keyfile']) 289 | event_server.socket = context.wrap_socket(event_server.socket, server_side=True) 290 | 291 | my_logger.info("Listening for events...") 292 | my_logger.info('Press Ctrl-C to close program') 293 | try: 294 | event_server.serve_forever() 295 | except KeyboardInterrupt: 296 | pass 297 | my_logger.info("Shutting down the Redfish Event Listener") 298 | event_server.server_close() 299 | clean_subscriptions() 300 | sys.exit(0) 301 | -------------------------------------------------------------------------------- /cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEtTCCAp0CFGm+Smy1Lz+5P5BkJzSWS3qEe9eZMA0GCSqGSIb3DQEBCwUAMBcx 3 | FTATBgNVBAMMDFJlZGZpc2hFdmVudDAeFw0yNDA2MjYxOTMzNTdaFw0zNDA2MjQx 4 | OTMzNTdaMBcxFTATBgNVBAMMDFJlZGZpc2hFdmVudDCCAiIwDQYJKoZIhvcNAQEB 5 | BQADggIPADCCAgoCggIBAMPeBm2GpNLrRqkuGFAaENQSbgXbjQYIi94muWPYIj0Z 6 | MMGRKOw7E+xsZE3w5b36c3p+WvKXuF4jKIf5jst9hjya/RXCgvY2cn86my/09kmD 7 | u1zgdJEC+Yk2z/AWStzBNMEWSbxrk1+p+cRd+mq9or1FmcSCiPt35chicNmWl16y 8 | hoKgHFD6Xg2EBhtDQ3j7SerrtVe/Sh1bMWogcBQONMU3CbZuTuxIHwzzObnRIZb4 9 | KeV1lBEIvQ61BzcXGbz+W3a+b4+KMmGwXKfc5Mossz19Z6E5D/Trt4u8bV6A71k2 10 | zIzUfbVeWntfB1/e8mbVD05JkgCevy3HaGhC9gPQHEyzPv5lCgIDZ97Mwhji95lK 11 | deFII+CvjF35zR8c/wNjwmQ/D4pL/b5Kvg9JR9KlZC1i/8STxXVMdm6WoldQzEH4 12 | CH0bov4dIse0AkLqZMN8jhwtlagcG9oV30pHwU51zuU0VAIor0J34eqvuSZC+Kqe 13 | C3gm+WFvX1Ci7xHxHfRRIZfzHlw5CIyuSmw8DvnqJ4vxAnSTo//m8xtq/NQz5FF1 14 | vdc2cxD5Pna1pS841TnPFH1YH0KpYPrizAmLEc3OFhbczt1++mrLRZZJwGzNelbo 15 | HMTTZ9JFplJA7gb+HGlRHTbDiHnTL0UdewJovEhrM1ooi6AYbD0fLiCffGqakBfF 16 | AgMBAAEwDQYJKoZIhvcNAQELBQADggIBAFlUCmUR2pS/lOJui/KuJyTtAETb4ELA 17 | dQKqapBRXBDZb/b4yYTce1bAZPi44+khrp203wGaa3oyfc4bL4VTlR/dvH/xJODI 18 | uuXhn0souU9NFJjcJYdMoAQyQPyhiydD+ln3vplhPYVxpWDyXqr/XpvDi1nKc2iW 19 | LYKcO8tHsxBItyhhrMlDhuVWGR+zX+ZKRlQY1GcOnLns18jQE87ySTJiFZjiPnCT 20 | OxEf6O23Hz2j7DquFgxP6DxtJ99G3KlDBAlXuc6XNBWeXoI1hfHLefiDPVlmZioZ 21 | xgZrB0AYxrFvuZn9Hc5deBxbQO1izmQaLwqkVIZG21GBZbv39gAOR+FjTSKXSJPg 22 | eV+poSWOokJ6mE4LJWWHAb8JapC7kPRwllZJm/YHZ8PY59Q7oDTYRa0BYGofubl7 23 | iNlziDaS9yl0NWue2XDv3Eky3dErgZLgBH2e23G1AsY+Jt4tzAsoDx1ILFsH/EEk 24 | txGg91uq2YmNeThOCH9c4yyZl/m7SwULURWgBYeko/MptmzbeCfK1OY0Xibn4/n0 25 | kQCHQgSOXJtNpSAXgJCJsWiHUWbVUvSpLiqAG4QRR7X3WM9sG0fLvEZYWyeGJCFH 26 | sjgrgzVkZWT4sge+QcA4nPtikqrtbUvXPfrJHYbTpWKRfVc+w0jmrDLjAgCi6tm9 27 | 6XMN0AU0C6ND 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [Information] 2 | Updated = February 24, 2023 3 | Description = Redfish Event Listener Tool Simple Config 4 | 5 | [SystemInformation] 6 | ListenerIP = 0.0.0.0 7 | ListenerPort = 443 8 | UseSSL = on 9 | 10 | [CertificateDetails] 11 | certfile = cert.pem 12 | keyfile = server.key 13 | 14 | [SubscriptionDetails] 15 | Destination = https:/// 16 | Context = Public 17 | 18 | [ServerInformation] 19 | ServerIPs = ["https://"] 20 | UserNames = ["Username1"] 21 | Passwords = ["Password1"] 22 | LoginType = ["Session"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | redfish 2 | redfish_utilities>=1.2.0 -------------------------------------------------------------------------------- /server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | xLbEeTVHc9SbdguebpCQfBU6dAbU 52 | -----END PRIVATE KEY----- 53 | --------------------------------------------------------------------------------