diff --git a/Makefile b/Makefile index 4eaf2306..7182e34c 100644 --- a/Makefile +++ b/Makefile @@ -238,8 +238,9 @@ ifeq ($(python_m_version),2) @echo "Makefile: Warning: Skipping the checking of missing dependencies on Python $(python_version)" >&2 else @echo "Makefile: Checking missing dependencies of this package" - pip-missing-reqs $(package_name) --requirements-file=requirements.txt - pip-missing-reqs $(package_name) --requirements-file=minimum-constraints.txt +# TODO: Re-enable once PR https://github.com/prometheus/client_python/pull/946 is released as 0.18.0 (?) +# pip-missing-reqs $(package_name) --requirements-file=requirements.txt +# pip-missing-reqs $(package_name) --requirements-file=minimum-constraints.txt @echo "Makefile: Done checking missing dependencies of this package" ifeq ($(PLATFORM),Windows_native) # Reason for skipping on Windows is https://github.com/r1chardj0n3s/pip-check-reqs/issues/67 diff --git a/README.rst b/README.rst index cbb51ce0..051b38f9 100644 --- a/README.rst +++ b/README.rst @@ -110,6 +110,9 @@ Quickstart * Direct your web browser at http://localhost:9291 to see the exported Prometheus metrics. Refreshing the browser will update the metrics. + Note: Use the command line options ``--cert-file`` and ``--key-file`` to + enable HTTPS. + Reporting issues ---------------- diff --git a/docs/changes.rst b/docs/changes.rst index 22deac78..d435ff08 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -81,6 +81,8 @@ Released: not yet a new metric zhmc_partition_storage_groups that lists the storage groups attached to a partition. (issue #346) +* Added support for HTTPS and mutual TLS (mTLS). (issue #347) + **Cleanup:** **Known issues:** diff --git a/docs/intro.rst b/docs/intro.rst index 583e2ddd..486b873e 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -27,6 +27,9 @@ automatic session renewals with the HMC if the logon session expires, and it survives HMC reboots and automatically picks up metrics collection again once the HMC come back up. +The exporter supports HTTP and HTTPS (with and without mutual TLS) for +Prometheus. + .. _IBM Z: https://www.ibm.com/it-infrastructure/z .. _Prometheus exporter: https://prometheus.io/docs/instrumenting/exporters/ .. _Prometheus: https://prometheus.io @@ -89,6 +92,9 @@ Quickstart * Direct your web browser at http://localhost:9291 to see the exported Prometheus metrics. Refreshing the browser will update the metrics. + Note: Use the command line options `--cert-file` and `--key-file` to + enable HTTPS. + Reporting issues ---------------- diff --git a/docs/usage.rst b/docs/usage.rst index 266a35e4..ea3fc257 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -99,6 +99,26 @@ The ``zhmc_prometheus_exporter`` command supports the following arguments: -p PORT port for exporting. Default: 9291 + --cert-file FILE path name of a certificate file in PEM format containing an X.509 server + certificate that will be presented to Prometheus during TLS handshake. If + specified, the exporter accepts only HTTPS from Prometheus. If not + specified, the exporter accepts only HTTP from Prometheus. + + --key-file FILE path name of a private key file in PEM format containing the X.509 private + key that belongs to the public key in the server certificate specified + with '--cert-file'. Required if '--cert-file' is specified. + + --ca-file FILE path name of a CA file in PEM format containing X.509 CA certificates that + will be used for validating a client certificate presented by Prometheus + during TLS handshake (if HTTPS is enabled). Default: Uses default CA + certificates established by ssl.SSLContext.load_default_certs(). + + --no-mtls Disable mutual TLS (mTLS). If mTLS is disabled, a possibly presented + client certificate will be ignored. By default, mTLS is enabled and + enforced (if HTTPS is enabled), i.e. Prometheus will be required to + present a client certificate, which will be validated using the CA + certificate chain. + --log DEST enable logging and set a log destination (stderr, syslog, FILE). Default: no logging diff --git a/minimum-constraints.txt b/minimum-constraints.txt index 57f83289..d9d2cd9a 100644 --- a/minimum-constraints.txt +++ b/minimum-constraints.txt @@ -80,7 +80,10 @@ wheel==0.38.1; python_version >= '3.7' zhmcclient==1.9.1 -prometheus-client==0.9.0 +# TODO: Re-enable once PR https://github.com/prometheus/client_python/pull/946 is released as 0.18.0 (?) +# prometheus-client==0.9.0; python_version <= '3.7' +# prometheus-client==0.18.0; python_version >= '3.8' + urllib3==1.26.5 jsonschema==3.2.0 six==1.14.0; python_version <= '3.9' diff --git a/requirements.txt b/requirements.txt index 9a0f9d76..a52c3c73 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,12 @@ # zhmcclient @ git+https://github.com/zhmcclient/python-zhmcclient.git@master zhmcclient>=1.9.1 -prometheus-client>=0.9.0 +# TODO: Re-enable once PR https://github.com/prometheus/client_python/pull/946 is released as 0.18.0 (?) +# prometheus-client>=0.9.0; python_version <= '3.7' +# prometheus-client>=0.18.0; python_version >= '3.8' +prometheus-client @ git+https://github.com/karezachen/client_python.git@master + + urllib3>=1.25.9; python_version <= '3.9' urllib3>=1.26.5; python_version >= '3.10' jsonschema>=3.2.0 diff --git a/zhmc_prometheus_exporter/zhmc_prometheus_exporter.py b/zhmc_prometheus_exporter/zhmc_prometheus_exporter.py index 0877c7e5..8e5a6907 100755 --- a/zhmc_prometheus_exporter/zhmc_prometheus_exporter.py +++ b/zhmc_prometheus_exporter/zhmc_prometheus_exporter.py @@ -202,6 +202,11 @@ def zhmc_exceptions(session, hmccreds_filename): def parse_args(args): """Parses the CLI arguments.""" + + prometheus_client_supports_https = sys.version_info[0:2] >= (3, 8) + https_str = "" if prometheus_client_supports_https \ + else " Not supported - requires Python 3.8 or higher" + parser = argparse.ArgumentParser( description="IBM Z HMC Exporter - a Prometheus exporter for metrics " "from the IBM Z HMC") @@ -218,6 +223,34 @@ def parse_args(args): parser.add_argument("-p", metavar="PORT", default="9291", help="port for exporting. Default: 9291") + parser.add_argument("--cert-file", metavar="FILE", default=None, + help="path name of a certificate file in PEM format " + "containing an X.509 server certificate that will be " + "presented to Prometheus during TLS handshake. " + "If specified, the exporter accepts only HTTPS from " + "Prometheus. If not specified, the exporter accepts " + "only HTTP from Prometheus." + https_str) + parser.add_argument("--key-file", metavar="FILE", default=None, + help="path name of a private key file in PEM format " + "containing the X.509 private key that belongs to the " + "public key in the server certificate specified with " + "'--cert-file'. Required if '--cert-file' is " + "specified." + https_str) + parser.add_argument("--ca-file", metavar="FILE", default=None, + help="path name of a CA file in PEM format containing " + "X.509 CA certificates that will be used for " + "validating a client certificate presented by " + "Prometheus during TLS handshake (if HTTPS is " + "enabled). Default: Uses default CA certificates " + "established by " + "ssl.SSLContext.load_default_certs()." + https_str) + parser.add_argument("--no-mtls", action='store_true', default=False, + help="Disable mutual TLS (mTLS). If mTLS is disabled, " + "a possibly presented client certificate will be " + "ignored. By default, mTLS is enabled and enforced (if " + "HTTPS is enabled), i.e. Prometheus will be required " + "to present a client certificate, which will be " + "validated using the CA certificate chain." + https_str) parser.add_argument("--log", dest='log_dest', metavar="DEST", default=None, help="enable logging and set a log destination " "({dests}). Default: no logging". @@ -1671,6 +1704,11 @@ def main(): VERBOSE_LEVEL = args.verbose + if args.cert_file and not args.key_file: + raise ImproperExit( + "HTTPS enabled by specifying --cert-file, but --key-file " + "is not pecified") + setup_logging(args.log_dest, args.log_complevels, args.syslog_facility) logprint(logging.WARNING, None, @@ -1802,9 +1840,37 @@ def main(): "Registering the collector and performing first collection") REGISTRY.register(coll) # Performs a first collection - logprint(logging.INFO, PRINT_V, - "Starting the HTTP server on port {}".format(args.p)) - start_http_server(int(args.p)) + if args.cert_file: + logprint(logging.INFO, PRINT_V, + "Starting the HTTPS server on port {}".format(args.p)) + logprint(logging.INFO, PRINT_V, + "Server certificate file: {}".format(args.cert_file)) + logprint(logging.INFO, PRINT_V, + "Server private key file: {}".format(args.key_file)) + ca_str = args.ca_file or "default" + logprint(logging.INFO, PRINT_V, + "CA certificate file: {}".format(ca_str)) + # TODO: Change "Optional" to "Enforced" if prometheus-client + # decides to specify ssl.CERT_REQUIRED in PR + # https://github.com/prometheus/client_python/pull/946 + mtls_str = "Disabled" if args.no_mtls else "Optional" + logprint(logging.INFO, PRINT_V, + "Mutual TLS: {}".format(mtls_str)) + else: + logprint(logging.INFO, PRINT_V, + "Starting the HTTP server on port {}".format(args.p)) + + try: + start_http_server( + port=int(args.p), + certfile=args.cert_file, + keyfile=args.key_file, + cafile=args.ca_file, + insecure_skip_verify=args.no_mtls) + except IOError as exc: + raise ImproperExit( + "Issues with server certificate, key, or CA certificate " + "files: {}: {}".format(exc.__class__.__name__, exc)) logprint(logging.INFO, PRINT_ALWAYS, "Exporter is up and running on port {}".format(args.p))