From 996fbb809c3f2f9f508e10ff1c76ea8d9fd5fa99 Mon Sep 17 00:00:00 2001 From: "th.l" <thl-cmk@outlook.com> Date: Sat, 21 Oct 2023 19:33:31 +0200 Subject: [PATCH] update project --- agent_based/curl.py | 577 +++++++++ agents/plugins/curl.ps1 | 218 ++++ agents/plugins/curl.sh | 219 ++++ gui/metrics/curl.py | 210 ++++ gui/wato/check_parameters/curl.py | 280 +++++ gui/wato/check_parameters/curl_bakery.py | 1061 +++++++++++++++++ .../cmk/base/cee/plugins/bakery/curl.py | 453 +++++++ mkp/curl-0.2.1-20231021.mkp | Bin 26407 -> 26406 bytes packages/curl | 34 + 9 files changed, 3052 insertions(+) create mode 100644 agent_based/curl.py create mode 100755 agents/plugins/curl.ps1 create mode 100755 agents/plugins/curl.sh create mode 100644 gui/metrics/curl.py create mode 100644 gui/wato/check_parameters/curl.py create mode 100644 gui/wato/check_parameters/curl_bakery.py create mode 100644 lib/python3/cmk/base/cee/plugins/bakery/curl.py create mode 100644 packages/curl diff --git a/agent_based/curl.py b/agent_based/curl.py new file mode 100644 index 0000000..8788157 --- /dev/null +++ b/agent_based/curl.py @@ -0,0 +1,577 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2022-02-15 + +# +# based on the work by Christian Wirtz doc[at]snowheaven[dot]de +# +# 2022-02-15: rewritten for cmk 2.0 +# 2022-02-20: removed url_effective from format file (bad output on linux: "url_effective":http://"https//checkmk.com") +# added num_connects, num_redirects, size_download, size_header, speed_download +# added redirect_url, remote_ip, scheme, http_version, http_connect +# 2022-02-22: added params for all values +# 2022-02-23: changed items on service info line +# 2022-02-28: added expected_strings and state_item_not_found option +# 2022-03-05: added support for search in header strings +# 2022-03-06: added support for tls/certificate information +# 2022-03-10: optimized code for perfdata +# 2022-03-12: added cafile and capath to Cert info section +# added max_age +# 2022-03-18: added regex pattern match +# 2022-03-22: added curl_error_code_to_ignore and http_error_code_to_ignore options +# 2022-04-06: removed .replace('://', ': //')) from json.loads() so service name is without space +# 2022-04-26: made http(s) URLs clickable +# 2022-05-15: added workaround for raise ValueError("Cannot render negative timespan") +# 2022-05-17: fixed wrong import path for _TIME_UNITS and _gen_timespan_chunks +# 2023-06-07: moved gui files to ~/local/lib/chek_mk/gui/plugins/... +# + +# Example output from agent: +# +# <<<curl>>> +# { +# "thl-cmk.hopto.org": { +# "url": "https://thl-cmk.hopto.org", +# "data": { +# "ssl_verify_result": "0", +# "http_return_code": "000", +# "time_namelookup": "0,000000", +# "time_connect": "0,000000", +# "time_appconnect": "0,000000", +# "time_pretransfer": "0,000000", +# "time_redirect": "0,000000", +# "time_starttransfer": "0,000000", +# "time_total": "0,000000", +# "http_connect": "000", +# "http_version": "0", +# "num_connects": "0", +# "num_redirects": "0", +# "redirect_url": "", +# "remote_ip": "", +# "scheme": "(nil)", +# "size_download": "0", +# "size_header": "0", +# "speed_download": "0,000", +# }, +# "error_level": "6" +# } +# } +# + +import json +import time +from typing import Dict, Any, List, Iterable +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( + DiscoveryResult, + CheckResult, +) + +from cmk.base.plugins.agent_based.agent_based_api.v1 import ( + register, + Service, + State, + check_levels, + Result, + render, +) + +# +# start workaround for : raise ValueError("Cannot render negative timespan") +# +from cmk.base.api.agent_based.render import ( + _TIME_UNITS, + _gen_timespan_chunks, +) + + +def timespan(seconds: float) -> str: + """Render a time span in seconds + + Example: + >>> timespan(1606721) + '18 days 14 hours' + >>> timespan(0.0001) + '100 microseconds' + + """ + if seconds >= 0: + ts = " ".join(_gen_timespan_chunks(float(seconds), nchunks=2)) + else: + seconds = -1 * seconds + ts = " ".join(_gen_timespan_chunks(float(seconds), nchunks=2)) + ts = f"-{ts}" + if ts == "0 %s" % _TIME_UNITS[-1][0]: + ts = "0 seconds" + return ts + +# +# end workaround for : raise ValueError("Cannot render negative timespan") +# + + +_curl_error_codes = { + 1: 'Unsupported protocol. This build of curl has no support for this protocol.', + 2: 'Failed to initialize.', + 3: 'URL malformed. The syntax was not correct.', + 4: 'A feature or option that was needed to perform the desired request was not enabled or was explicitly ' + 'disabled at build-time. To make curl able to do this, you probably need another build of libcurl.', + 5: 'Could not resolve proxy. The given proxy host could not be resolved.', + 6: 'Could not resolve host. The given remote host could not be resolved.', + 7: 'Failed to connect to host.', + 8: 'Weird server reply. The server sent data curl could not parse.', + 9: 'FTP access denied. The server denied login or denied access to the particular resource or directory you ' + 'wanted to reach. Most often you tried to change to a directory that does not exist on the server.', + 10: 'FTP accept failed. While waiting for the server to connect back when an active FTP session is used, ' + 'an error code was sent over the control connection or similar.', + 11: 'FTP weird PASS reply. Curl could not parse the reply sent to the PASS request.', + 12: 'During an active FTP session while waiting for the server to connect back to curl, the timeout expired.', + 13: 'FTP weird PASV reply, Curl could not parse the reply sent to the PASV request.', + 14: 'FTP weird 227 format. Curl could not parse the 227-line the server sent.', + 15: 'FTP cannot use host. Could not resolve the host IP we got in the 227-line.', + 16: 'HTTP/2 error. A problem was detected in the HTTP2 framing layer. This is somewhat generic and can be one ' + 'out of several problems, see the error message for details.', + 17: 'FTP could not set binary. Could not change transfer method to binary.', + 18: 'Partial file. Only a part of the file was transferred.', + 19: 'FTP could not download/access the given file, the RETR (or similar) command failed.', + 21: 'FTP quote error. A quote command returned error from the server.', + 22: 'HTTP page not retrieved. The requested url was not found or returned another error with the HTTP error ' + 'code being 400 or above. This return code only appears if --fail is used.', + 23: 'Write error. Curl could not write data to a local filesystem or similar.', + 25: 'FTP could not STOR file. The server denied the STOR operation, used for FTP uploading.', + 26: 'Read error. Various reading problems.', + 27: 'Out of memory. A memory allocation request failed.', + 28: 'Operation timeout. The specified time-out period was reached according to the conditions.', + 30: 'FTP PORT failed. The PORT command failed. Not all FTP servers support the PORT command, ' + 'try doing a transfer using PASV instead!', + 31: 'FTP could not use REST. The REST command failed. This command is used for resumed FTP transfers.', + 33: 'HTTP range error. The range "command" did not work.', + 34: 'HTTP post error. Internal post-request generation error.', + 35: 'SSL connect error. The SSL handshaking failed.', + 36: 'Bad download resume. Could not continue an earlier aborted download.', + 37: 'FILE could not read file. Failed to open the file. Permissions?', + 38: 'LDAP cannot bind. LDAP bind operation failed.', + 39: 'LDAP search failed.', + 41: 'Function not found. A required LDAP function was not found.', + 42: 'Aborted by callback. An application told curl to abort the operation.', + 43: 'Internal error. A function was called with a bad parameter.', + 45: 'Interface error. A specified outgoing interface could not be used.', + 47: 'Too many redirects. When following redirects, curl hit the maximum amount.', + 48: 'Unknown option specified to libcurl. This indicates that you passed a weird option to curl that was' + ' passed on to libcurl and rejected. Read up in the manual!', + 49: 'Malformed telnet option.', + 51: 'The peer\'s SSL certificate or SSH MD5 fingerprint was not OK.', + 52: 'The server did not reply anything, which here is considered an error.', + 53: 'SSL crypto engine not found.', + 54: 'Cannot set SSL crypto engine as default.', + 55: 'Failed sending network data.', + 56: 'Failure in receiving network data.', + 58: 'Problem with the local certificate.', + 59: 'Could not use specified SSL cipher.', + 60: 'Peer certificate cannot be authenticated with known CA certificates.', + 61: 'Unrecognized transfer encoding.', + 62: 'Invalid LDAP URL.', + 63: 'Maximum file size exceeded.', + 64: 'Requested FTP SSL level failed.', + 65: 'Sending the data requires a rewind that failed.', + 66: 'Failed to initialise SSL Engine.', + 67: 'The user name, password, or similar was not accepted and curl failed to log in.', + 68: 'File not found on TFTP server.', + 69: 'Permission problem on TFTP server.', + 70: 'Out of disk space on TFTP server.', + 71: 'Illegal TFTP operation.', + 72: 'Unknown TFTP transfer ID.', + 73: 'File already exists (TFTP).', + 74: 'No such user (TFTP).', + 75: 'Character conversion failed.', + 76: 'Character conversion functions required.', + 77: 'Problem reading the SSL CA cert (path? access rights?).', + 78: 'The resource referenced in the URL does not exist.', + 79: 'An unspecified error occurred during the SSH session.', + 80: 'Failed to shut down the SSL connection.', + 82: 'Could not load CRL file, missing or wrong format.', + 83: 'Issuer check failed.', + 84: 'The FTP PRET command failed.', + 85: 'Mismatch of RTSP CSeq numbers.', + 86: 'Mismatch of RTSP Session Identifiers.', + 87: 'Unable to parse FTP file list.', + 88: 'FTP chunk callback reported error.', + 89: 'No connection available, the session will be queued.', + 90: 'SSL public key does not matched pinned public key.', + 91: 'Invalid SSL certificate status.', + 92: 'Stream error in HTTP/2 framing layer.', + 93: 'An API function was called from inside a callback.', + 94: 'An authentication function returned an error.', + 95: 'A problem was detected in the HTTP/3 layer. This is somewhat generic and can be one out of ' + 'several problems, see the error message for details.', + 96: 'QUIC connection error. This error may be caused by an SSL library error. QUIC is the ' + 'protocol used for HTTP/3 transfers.', +} + + +def _get_tls_info(tls_lines: List[str]): + tls_infos = {} + alpn_offerings = [] + for line in tls_lines: + line = line.strip() + if line.startswith('subject: '): + tls_infos.update({'subject': line[9:]}) + elif line.startswith('start date: '): + tls_infos.update({'start_date': line[12:]}) + elif line.startswith('expire date: '): + tls_infos.update({'expire_date': line[13:]}) + time_left = time.mktime( # 'Feb 16 10:18:08 2022 GMT' + time.strptime(tls_infos['expire_date'], '%b %d %H:%M:%S %Y %Z') + ) - time.mktime(time.gmtime()) + tls_infos.update({'time_left': time_left}) + elif line.startswith('subjectAltName: '): + tls_infos.update({'subject_alternate_name': line[21:]}) + elif line.startswith('issuer: '): + tls_infos.update({'issuer': line[8:]}) + elif line.startswith('ALPN, offering '): + alpn_offerings.append(line[15:]) + elif line.startswith('ALPN, server accepted to use '): + tls_infos.update({'alpn_accepted': line[29:]}) + elif line.startswith('SSL certificate verify result: '): + tls_infos.update({'ssl_verify_result': line[31:]}) + elif line.startswith('SSL connection using '): + tls_version, cipher = line[21:].split(' / ') + tls_infos.update({'tls_version_used': tls_version}) + tls_infos.update({'cipher_used': cipher}) + elif line.startswith('CAfile'): + tls_infos.update({'ca_file': line[8:]}) + elif line.startswith('CApath'): + tls_infos.update({'ca_path': line[8:]}) + + tls_infos.update({'alpn_offerings': alpn_offerings}) + + return tls_infos + + +def parse_curl(string_table): + try: + # section = json.loads(string_table[0][0].replace('://', ': //')) # removed so service name is without space + section = json.loads(string_table[0][0]) + except (IndexError, TypeError, json.JSONDecodeError): + return {} + if 'ERROR' not in section.keys(): + if len(string_table) > 1: + # insert splitter, split (and implicit remove splitter), avoid removal of '}{' + for line in string_table[1][0].replace('}{', '}|||{').split('|||'): + # '{'thl-cmk.hopto.org': {'expected_strings': [['string1', 1], ['string2', 1], ['html', 0]]}}' + try: + # line = line.replace('://', ': //') # removed so service name is without space + line = json.loads(line) + except json.JSONDecodeError: + continue + # get first key from line + key = list(line.keys())[0] + # update section data for key + section[key].update(line[key]) + for key in section.keys(): + if section[key].get('TLS_INFO'): + tls_info = _get_tls_info(section[key]['TLS_INFO']) + section[key].update({'tls_info': tls_info}) + # section[key].pop('TLS_INFO') + return section + + +def discovery_curl(section: Dict[str, Any]) -> DiscoveryResult: + for key in section.keys(): + yield Service(item=key) + + +def check_curl(item, params, section: Dict[str, Any]) -> CheckResult: + try: + _data = section[item] + except KeyError: + yield Result(state=State(params['state_item_not_found']), notice='Item not found in agent data') + return + + # url = _data['url'].replace('://', ': //') # ugly workaround to stop cmk from replacing the url + http_return_code = _data['data']['http_code'] + ssl_verify_result = _data['data']['ssl_verify_result'] + curl_error_code = int(_data['data']['exitcode']) + + # yield Result(state=State.OK, notice=f'URL from cfg: {url}') + for key, label in [ + ('url', 'URL'), + ('redirect_url', 'URL redirect'), + ('url_effective', 'URL effective'), + ('referer', 'Referer'), + ]: + if _data['data'].get(key): + value = _data['data'][key] + if value.startswith('http'): + if params.get('clickable_url'): + value = f'<a href={value} target="blank">{value}</a>' + else: + value = f'{value.replace("://", ": //")}' # ugly workaround to stop cmk from replacing the url + yield Result(state=State.OK, notice=f'{label}: {value}') + + if curl_error_code != 0: + if curl_error_code in params['curl_error_code_to_ignore']: + yield Result( + state=State.OK, + notice=f'curl error code: {curl_error_code}, {_curl_error_codes.get(curl_error_code, "N/A")}') + else: + yield Result( + state=State(params['state_curl_result_not_0']), + summary=f'curl error code: {curl_error_code} see details', + details=f'curl error code: {curl_error_code}, {_curl_error_codes.get(curl_error_code, "N/A")}' + ) + + if http_return_code < 400: # no connect, Ok, Redirect + yield Result(state=State.OK, notice=f'HTTP Return code: {http_return_code}') + else: + if http_return_code in params['http_error_code_to_ignore']: + yield Result( + state=State.OK, notice=f'HTTP Return code: {http_return_code}' + ) + else: + yield Result( + state=State(params['state_http_result_not_200']), + notice=f'HTTP Return code: {http_return_code}' + ) + + if ssl_verify_result == 0: + yield Result(state=State.OK, notice=f'SSL verify result: {ssl_verify_result}') + else: + yield Result(state=State(params['state_verify_sll_not_0']), summary=f'SSL verify result: {ssl_verify_result}') + + for key, label in [ + ('scheme', 'Scheme (protocol)'), + ('http_version', 'HTTP version'), + ('method', 'Method'), + ('content_type', 'Content type'), + ('remote_ip', 'Remote IP'), + ('http_connect', 'Proxy connect code'), + ('errormsg', 'Error message'), + ('curl_version', 'cURL version'), + ]: + if _data['data'].get(key): + yield Result(state=State.OK, notice=f'{label}: {_data["data"][key]}') + + if _data.get('curl_options'): + yield Result(state=State.OK, notice=f'cURL options: {_data["curl_options"]}') + + if _data.get('expected_response'): + for expected_string, result in _data['expected_response']: + if result == 0: + yield Result(state=State.OK, notice=f'Response string: "{expected_string}" found') + else: + yield Result( + state=State(params['state_expected_str_not_found']), + notice=f'Response string: "{expected_string}" not found' + ) + + if _data.get('expected_header'): + for header_strings, result in _data['expected_header']: + if result == 0: + yield Result(state=State.OK, notice=f'Header string: "{header_strings}" found') + else: + yield Result( + state=State(params['state_header_str_not_found']), + notice=f'Header string: "{header_strings}" not found' + ) + + regex_match, regex_no_match, regex_missing = params['state_for_regex'] + if _data.get('regex') == 0: # match + yield Result(state=State(regex_match), notice='Regex state: pattern matches') + elif _data.get('regex') == 1: # no match + yield Result(state=State(regex_no_match), notice='Regex state: pattern don\'t matches') + elif not _data.get('regex'): # missing info + yield Result(state=State(regex_missing), notice='Regex state: missing pattern match info') + + max_age_warn, max_age_crit, max_age_state = params['max_age'] + if max_age_warn: + max_age = None + if _data.get('RESPONSE_HEADER'): + for line in _data['RESPONSE_HEADER']: + if line.startswith('Last-Modified: '): + max_age = time.mktime(time.gmtime()) - time.mktime( + time.strptime(line[15:], '%a, %d %b %Y %H:%M:%S %Z')) # 'Fri, 18 Jun 2021 18:17:33 GMT' + if max_age: + yield from check_levels( + label='Last modified', + value=max_age, + render_func=render.timespan, + notice_only=True, + levels_upper=(max_age_warn, max_age_crit), + ) + else: + yield Result(state=State(max_age_state), notice='Last-Modified: Document modification date unknown') + + if _data.get('tls_info', {}).get('subject'): + yield Result(state=State.OK, notice=f' ') + yield Result(state=State.OK, notice=f'Certificate info:') + + for key, label in [ + ('subject', 'Subject'), + ('issuer', 'Issuer'), + ('subject_alternate_name', 'Subject alternate name'), + ('start_date', 'Start date'), + ('expire_date', 'Expire date'), + ('tls_version_used', 'TLS Version used'), + ('cipher_used', 'Cipher used'), + ('ssl_verify_result', 'SSL verify result'), + ('ca_file', 'CA file'), + ('ca_path', 'CA path'), + ]: + if _data['tls_info'].get(key): + yield Result(state=State.OK, notice=f'{label}: {_data["tls_info"][key]}') + + if _data['tls_info'].get('time_left'): + if params['cert_time_left'].get('upper'): + warn, crit = params['cert_time_left'].get('upper') + upper = (warn * 86400, crit * 86400) + else: + upper = None + + if params['cert_time_left'].get('lower'): + warn, crit = params['cert_time_left'].get('lower') + lower = (warn * 86400, crit * 86400) + else: + lower = None + + yield from check_levels( + label='Certificate time left', + value=_data['tls_info']['time_left'], + metric_name='cert_time_left', + render_func=timespan, + notice_only=True, + levels_upper=upper, + levels_lower=lower, + ) + + yield Result(state=State.OK, notice=f' ') + yield Result(state=State.OK, notice=f'Performance data:') + + def _render_string(v): + return f'{v}' + + for key, label, notice_only, render_func in [ + # times + ('time_namelookup', 'Time name lookup', True, render.timespan,), + ('time_connect', 'Time connect', True, render.timespan,), + ('time_appconnect', 'Time app connect', True, render.timespan,), + ('time_pretransfer', 'Time pre transfer', True, render.timespan,), + ('time_redirect', 'Time redirect', True, render.timespan,), + ('time_starttransfer', 'Time start transfer', True, render.timespan,), + ('time_total', 'Time total', False, render.timespan,), + ]: + if _data['data'].get(key): + # ms needs to be changed to s + value = _data['data'][key] / 1000 + if value != 0: + yield from check_levels( + label=label, + value=value, + metric_name=key, + notice_only=notice_only, + render_func=render_func, + levels_lower=params[key].get('lower'), + levels_upper=params[key].get('upper'), + ) + + for key, label, notice_only, render_func in [ + # counts + ('num_connects', '# of connects', True, _render_string, ), + ('num_redirects', '# of redirects', True, _render_string, ), + ('num_headers', '# of headers', True, _render_string, ), + # sizes + ('size_download', 'Size download', False, render.bytes,), + ('size_upload', 'Size upload', True, render.bytes,), + ('size_header', 'Size header', True, render.bytes,), + ('size_request', 'Size request', True, render.bytes,), + # speed + ('speed_download', 'Speed download', False, render.networkbandwidth,), + ('speed_upload', 'Speed upload', True, render.networkbandwidth,), + ]: + if _data['data'].get(key): + value = _data['data'][key] + if value != 0: + yield from check_levels( + label=label, + value=value, + metric_name=key, + notice_only=notice_only, + render_func=render_func, + levels_lower=params[key].get('lower'), + levels_upper=params[key].get('upper'), + ) + + show_request_header, show_response_headers, show_session_info, show_raw_data = params['show_additional_info'] + if show_request_header and _data.get('REQUEST_HEADER'): + yield Result(state=State.OK, notice=f' ') + yield Result(state=State.OK, notice=f'Request headers:') + for line in _data['REQUEST_HEADER']: + if line: + yield Result(state=State.OK, notice=f'{line}') + + if show_response_headers and _data.get('RESPONSE_HEADER'): + yield Result(state=State.OK, notice=f' ') + yield Result(state=State.OK, notice=f'Response headers:') + for line in _data['RESPONSE_HEADER']: + if line: + yield Result(state=State.OK, notice=f'{line}') + + if show_session_info and _data.get('TLS_INFO'): + yield Result(state=State.OK, notice=f' ') + yield Result(state=State.OK, notice=f'TLS/SSL/SSH info:') + for line in _data['TLS_INFO']: + if line: + yield Result(state=State.OK, notice=f'{line}') + + if show_raw_data: + yield Result(state=State.OK, notice=f' ') + yield Result(state=State.OK, notice=f'RAW data:') + yield Result(state=State.OK, notice=f'{_data}') + + +register.agent_section( + name="curl", + parse_function=parse_curl, +) + +register.check_plugin( + name='curl', + service_name='cURL %s', + discovery_function=discovery_curl, + check_function=check_curl, + check_default_parameters={ + 'show_additional_info': (None, None, None, None), + 'max_age': (None, None, None), + 'state_item_not_found': 3, + 'state_http_result_not_200': 1, + 'http_error_code_to_ignore': [], + 'state_curl_result_not_0': 1, + 'curl_error_code_to_ignore': [], + 'state_verify_sll_not_0': 1, + 'state_expected_str_not_found': 1, + 'state_header_str_not_found': 1, + 'state_for_regex': (0, 1, 0), + 'time_namelookup': {}, + 'time_connect': {}, + 'time_appconnect': {}, + 'time_pretransfer': {}, + 'time_redirect': {}, + 'time_starttransfer': {}, + 'time_total': {}, + 'num_connects': {}, + 'num_redirects': {}, + 'num_headers': {}, + 'size_download': {}, + 'size_upload': {}, + 'size_header': {}, + 'size_request': {}, + 'speed_download': {}, + 'speed_upload': {}, + 'cert_time_left': {}, + }, + check_ruleset_name='curl' +) diff --git a/agents/plugins/curl.ps1 b/agents/plugins/curl.ps1 new file mode 100755 index 0000000..04727a2 --- /dev/null +++ b/agents/plugins/curl.ps1 @@ -0,0 +1,218 @@ +<# +License: GNU General Public License v2 + +Author: thl-cmk[at]outlook[dot]com +URL : https://thl-cmk.hopto.org +Date : 2022-02-10 + +based on the work by Christian Wirtz doc[at]snowheaven[dot]de and Ingo Hambrock + +Wrapper around: https://curl.se/ + +2022-02-10: rewritten by thl-cmk[at]outlook.com to output json string +2022-02-16: added file checks for CURL_EXECUTABLE and CURL_FORMAT +2022-02-17: added ERRORLEVEL +2022-02-21: added curl_option to the output +2022-02-22: fixed search for curl.exe +2022-02-23: fixed handling of options from curl.cfg +2022-02-24: removed curl.format, replaced by -w %{json} +2022-02-25: removed separate url and error_level from output +2022-02-27: rewritten for powershell (from cmd) +2022-02-28: added support to search for strings in cURL output +2022-03-05: added support for search in header strings +2022-03-06: added support for ssl/tls session info +2022-03-11: added --verbose --stderr $TEMP_DIR\curl_session to CURL_OPTIONS +2022-03-12: fixed escapes for json format in TLS_INFO +2022-03-13: moved curl_item files to curl sub directory under $MK_CONFDIR +2022-03-15: moved curl options from curl.cfg to curl_item_#.options +2022-03-15: added regex pattern match +#> + +$TEMP_DIR="c:\windows\temp" + +# config file directory +$MK_CONFDIR = $env:MK_CONFDIR + +# Fallback if no MK_CONFDIR is set +if (!$MK_CONFDIR) { + $MK_CONFDIR= "$env:ProgramData\checkmk\agent\config" +} + +Write-Output "<<<curl:sep(0)>>>" + +if (Test-Path -Path $MK_CONFDIR\curl.cfg -PathType Leaf) { + $CURL_CONFIG="$MK_CONFDIR\curl.cfg" +} else { + Write-Output '{"ERROR":"config file $MK_CONFDIR\curl.cfg does not exist"}' + exit 0 +} + +# first check for curl.exe in cmk agent directory +if (Test-Path -Path "C:\ProgramData\checkmk\agent\bin\curl.exe" -PathType Leaf) { + $CURL_EXECUTABLE="C:\ProgramData\checkmk\agent\bin\curl.exe" + # if not found look for system provided file +} elseif (Test-Path -Path "C:\Windows\System32\curl.exe" -PathType Leaf) { + $CURL_EXECUTABLE="C:\Windows\System32\curl.exe" +} else { + Write-Output '{"ERROR":"executable file curl.exe not found"}' + exit 0 +} + +$CURL_OPTIONS="--disable --write-out %{json} --silent --verbose --include --stderr $TEMP_DIR\curl_session" +$CURL_OPTIONS=$CURL_OPTIONS.Substring(0,$CURL_OPTIONS.Length -0) +$CURL_RUN="FIRST" +$CURL_RESULT="" +$CURL_OUTPUT="$TEMP_DIR\curl_output" +$CURL_HEADER="$TEMP_DIR\curl_header" +$CURL_SESSION_FILE="$TEMP_DIR\curl_session" + +function Find-CurlStrings { + param ( + [string]$SEARCH_FILE, + [string]$SEARCH_EXTENSION, + [string]$JSON_KEY + ) + if (Test-Path -Path "$SEARCH_FILE" -PathType Leaf) { + if (Test-Path -Path "$MK_CONFDIR/curl/$CURL_ITEM.$SEARCH_EXTENSION" -PathType Leaf) { + $ROUND="FIRST" + $CURL_FILE=Get-Content $SEARCH_FILE -Raw + $CURL_RESULT="$CURL_RESULT{""$SERVICE_NAME"":{""$JSON_KEY"":[" + foreach($LINE in Get-Content $MK_CONFDIR/curl/$CURL_ITEM.$SEARCH_EXTENSION) { + if ( $ROUND -eq "SECOND" ) { + $CURL_RESULT="$CURL_RESULT," + } else { + $ROUND="SECOND" + } + $CURL_SEARCH=$CURL_FILE.IndexOf($LINE) + # change to match grep return codes + if ($CURL_SEARCH -ne -1){ + $CURL_SEARCH = 0 + } else { + $CURL_SEARCH = 1 + } + $CURL_RESULT="$CURL_RESULT[""$LINE"", $CURL_SEARCH]" + } + $CURL_RESULT="$CURL_RESULT]}}" + } + } + return $CURL_RESULT +} + +function Find-CurlSessionInfo { + param ( + [string]$SEARCH_MARKER, + [string]$JSON_KEY + ) + if (Test-Path -Path "$CURL_SESSION_FILE" -PathType Leaf) { + $CURL_RESULT = "$CURL_RESULT{""$SERVICE_NAME"":{""$JSON_KEY"":[" + $ROUND="FIRST" + foreach($LINE in Get-Content "$CURL_SESSION_FILE") { + if ($LINE[0] -eq "$SEARCH_MARKER") { + $LINE = $LINE.Substring(1, $LINE.Length -1).trim() -replace """", "\""" + if ( $ROUND -eq "SECOND" ) { + $CURL_RESULT="$CURL_RESULT," + } else { + $ROUND="SECOND" + } + $CURL_RESULT="$CURL_RESULT""$LINE""" + } + } + $CURL_RESULT="$CURL_RESULT]}}" + } + # return $CURL_RESULT even if it not altered + return $CURL_RESULT +} + +function Find-CurlRegex { + if (Test-Path -Path "$CURL_OUTPUT" -PathType Leaf) { + if (Test-Path -Path "$MK_CONFDIR/curl/$CURL_ITEM.regex" -PathType Leaf) { + if ($CURL_REG_MULTI -eq "multiline") { + $CURL_REG_MULTI = "(?sm)" + } else { + $CURL_REG_MULTI = "" + } + $CURL_FILE=Get-Content "$CURL_OUTPUT" -Raw + foreach($LINE in Get-Content "$MK_CONFDIR/curl/$CURL_ITEM.regex") { + if ($CURL_REG_CASE -eq "case") { + $CURL_REG_RESULT= $CURL_FILE -cmatch "$CURL_REG_MULTI$LINE" + } else { + $CURL_REG_RESULT= $CURL_FILE -match "$CURL_REG_MULTI$LINE" + } + } + if ("$CURL_REG_RESULT" -eq "True") { + $CURL_REG_RESULT = 0 + } else { + $CURL_REG_RESULT = 1 + } + $CURL_RESULT = "$CURL_RESULT{""$SERVICE_NAME"":{""regex"":$CURL_REG_RESULT}}" + } + } + return $CURL_RESULT +} + +function Cleanup { + if (Test-Path -Path $CURL_OUTPUT -PathType Leaf) { + Remove-Item -Path $CURL_OUTPUT + } + if (Test-Path -Path $CURL_HEADER -PathType Leaf) { + Remove-Item -Path $CURL_HEADER + } + if (Test-Path -Path $CURL_SESSION_FILE -PathType Leaf) { + Remove-Item -Path $CURL_SESSION_FILE + } +} + + +Write-Host -NoNewline "{" + +foreach($LINE in Get-Content $CURL_CONFIG) { + $SERVICE_NAME=$LINE.split("|")[0] + $CURL_ITEM=$LINE.split("|")[1] + $CURL_REGEX=$LINE.split("|")[2] + $CURL_REG_CASE=$CURL_REGEX.split("_")[0] + $CURL_REG_MULTI=$CURL_REGEX.split("_")[1] + + if ( $CURL_RUN -eq "SECOND") { + Write-Host -NoNewline "," + } else { + $CURL_RUN="SECOND" + } + + Cleanup + + Write-Host -NoNewline """$SERVICE_NAME"":{""data"":" + # $RUN="$CURL_EXECUTABLE $CURL_OPTIONS --url $URL $OPTIONS" + $RUN="$CURL_EXECUTABLE $CURL_OPTIONS --config $MK_CONFDIR/curl/$CURL_ITEM.options" + $RESULT = . cmd /c "$RUN" + Write-Host -NoNewline $RESULT + # Write-Host -NoNewline ",""curl_options"":""$CURL_OPTIONS $OPTIONS""" + Write-Host -NoNewline "}" + + # check for expected strings in response + $CURL_RESULT=Find-CurlStrings -SEARCH_FILE "$CURL_OUTPUT" -SEARCH_EXTENSION "search_response" -JSON_KEY "expected_response" + + # check for expected strings in headers + $CURL_RESULT=Find-CurlStrings -SEARCH_FILE "$CURL_HEADER" -SEARCH_EXTENSION "search_header" -JSON_KEY "expected_header" + + # collect for tls/ss session info + $CURL_RESULT=Find-CurlSessionInfo -SEARCH_MARKER "*" -JSON_KEY "TLS_INFO" + + # collect request header + $CURL_RESULT=Find-CurlSessionInfo -SEARCH_MARKER ">" -JSON_KEY "REQUEST_HEADER" + + # collect response header + $CURL_RESULT=Find-CurlSessionInfo -SEARCH_MARKER "<" -JSON_KEY "RESPONSE_HEADER" + + # check for regex match + $CURL_RESULT=Find-CurlRegex + +} + +Write-Output "}" + +Write-Output $CURL_RESULT + +# clean up +Cleanup + +exit 0 diff --git a/agents/plugins/curl.sh b/agents/plugins/curl.sh new file mode 100755 index 0000000..5189a27 --- /dev/null +++ b/agents/plugins/curl.sh @@ -0,0 +1,219 @@ +#!/bin/bash +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2022-02-20 +# +# Wrapper around: https://curl.se/ +# +# curl plugin for the check_mk linux agent +# +# 2022-02-21: added curl_option to the output +# 2022-02-24: changed to use -w %{json} instead of -w @$curl.format +# extended search for curl executable to agent dir +# 2022-02-25: removed separate url and error_level from output +# 2022-02-27: added support to search for strings in cURL output +# 2022-03-03: fixed handling of spaces in options string +# 2022-03-05: added support for search in header strings +# 2022-03-06: added support for ssl/tls session info +# 2022-03-11: added --verbose --stderr $TEMP_DIR/curl_session to CURL_OPTIONS +# 2022-03-13: moved curl_item files to curl sub directory under $MK_CONFDIR +# 2022-03-15: moved curl options from curl.cfg to curl_item_#.options +# 2022-03-17: added regex pattern match +# + +# ToDo: add plugin timeout enforcement + +CONF_DIR="/etc/check_mk" +LIB_DIR="/usr/lib/check_mk_agent" +TEMP_DIR="/var/tmp" + +CURL_OPTIONS="--disable --write-out %{json} --silent --verbose --include --stderr $TEMP_DIR/curl_session" +CURL_CONFIG=curl.cfg +CURL_RUN="FIRST" +CURL_OUTPUT="$TEMP_DIR/curl_output" +CURL_HEADER="$TEMP_DIR/curl_header" +CURL_SESSION_FILE="$TEMP_DIR/curl_session" +CURL_RESULT="" + +function find_curl_strings { + # usage: search_strings file_to_search_in extension_for_file_with_search_strings json_key + # check for expected strings in response + if [ -f "$1" ]; then + if [ -f "$CONF_DIR/curl/$CURL_ITEM.$2" ]; then + local ROUND="FIRST" + CURL_RESULT=$CURL_RESULT$(printf '{"%s":{"%s":[' "$SERVICE_NAME" "$3") + while read -r LINE; do + if [ "$ROUND" = "SECOND" ]; then + CURL_RESULT=$CURL_RESULT$(printf ',') + else + ROUND="SECOND" + fi + # ToDo: remove grep, read $CURL_OUTPUT to $CURL_FILE and search in there + grep -q -s "$LINE" "$1" + CURL_RESULT=$CURL_RESULT$(printf '["%s", %s]' "$LINE" $?) + done <"$CONF_DIR/curl/$CURL_ITEM.$2" + CURL_RESULT=$CURL_RESULT$(printf ']}}') + fi + # rm "$1" + fi +} + +function find_curl_session_info { + # usage: search_marker json_key replace_new_line + if [ -f "$CURL_SESSION_FILE" ]; then + CURL_RESULT="$CURL_RESULT"$(printf '{"%s":{"%s":[' "$SERVICE_NAME" "$2") + local ROUND="FIRST" + while read -r LINE; do + if [ "${LINE:0:1}" = "$1" ]; then + if [ "$ROUND" = "SECOND" ]; then + CURL_RESULT=$CURL_RESULT$(printf ',') + else + ROUND="SECOND" + fi + LINE=${LINE//"\""/"\\\""} + if [ "$3" = "YES" ]; then + local NEW_LINE + NEW_LINE=$(printf '"%s"' "${LINE:1:${#LINE}-2}") + CURL_RESULT="$CURL_RESULT$NEW_LINE" + else + CURL_RESULT=$CURL_RESULT$(printf '"%s"' "${LINE:1}") + fi + fi + done <"$CURL_SESSION_FILE" + CURL_RESULT="$CURL_RESULT]}}" + # rm "$CURL_SESSION_FILE" + fi +} + + +function find_curl_regex { + local GREP_OPTIONS="-q -s -o -z -P" + if [ -f "$CURL_OUTPUT" ]; then + if [ -f "$CONF_DIR/curl/$CURL_ITEM.regex" ]; then + if [ "$CURL_REG_MULTI" = "multiline" ]; then + CURL_REG_MULTI="(?sm)" + else + CURL_REG_MULTI="" + fi + while read -r LINE; do + if [ "$CURL_REG_CASE" = "case" ]; then + LINE="'$CURL_REG_MULTI$LINE'" + eval grep $GREP_OPTIONS $LINE "$CURL_OUTPUT" + CURL_REG_RESULT=$? + else + GREP_OPTIONS="$GREP_OPTIONS"" -i" + LINE="'$CURL_REG_MULTI$LINE'" + eval grep $GREP_OPTIONS $LINE $CURL_OUTPUT + CURL_REG_RESULT=$? + fi + CURL_RESULT=$CURL_RESULT$(printf '{"%s":{"regex":%s}}' "$SERVICE_NAME" "$CURL_REG_RESULT") + done < "$CONF_DIR/curl/$CURL_ITEM.regex" + fi + fi +} + +function cleanup { + if [ -f "$CURL_OUTPUT" ]; then + rm "$CURL_OUTPUT" + fi + + if [ -f "$CURL_HEADER" ]; then + rm "$CURL_HEADER" + fi + + if [ -f "$CURL_SESSION_FILE" ]; then + rm "$CURL_SESSION_FILE" + fi +} + +printf "<<<curl:sep(0)>>>\n" + +if [ -f "$MK_LIBDIR/bin/curl" ]; then + CURL_EXECUTABLE=$MK_LIBDIR/bin/curl +elif [ -f "$LIB_DIR/bin/curl" ]; then + CURL_EXECUTABLE=$LIB_DIR/bin/curl +elif [ -f "$(which curl)" ]; then + CURL_EXECUTABLE=$(which curl) +else + printf '{"ERROR":"executable file curl not found"}\n' + exit +fi + +if [ -f "$MK_CONFDIR/$CURL_CONFIG" ]; then + CURL_CONFIG=$MK_CONFDIR/$CURL_CONFIG + CONF_DIR="$MK_CONFDIR" +elif [ -f "$CONF_DIR/$CURL_CONFIG" ]; then + CURL_CONFIG=$CONF_DIR/$CURL_CONFIG +else + printf '{"ERROR":"config file %s not found"}\n' "$CURL_CONFIG" + exit +fi + +# start JSON +printf "{" + +while read -r LINE; do + SERVICE_NAME=$(echo "$LINE" | awk -F'|' '{print $1}') + CURL_ITEM=$(echo "$LINE" | awk -F'|' '{print $2}') + CURL_REGEX=$(echo "$LINE" | awk -F'|' '{print $3}') + CURL_REG_CASE=$(echo "$CURL_REGEX" | awk -F'_' '{print $1}') + CURL_REG_MULTI=$(echo "$CURL_REGEX" | awk -F'_' '{print $2}') + + # FIELDS=($(awk -F"|" '{$1=$1} 1' <<<"${LINE}")) + + if [ "$CURL_RUN" = "SECOND" ]; then + printf ',' + else + CURL_RUN="SECOND" + fi + + cleanup + + printf '"%s":{"data":' "$SERVICE_NAME" + eval "$CURL_EXECUTABLE" "$CURL_OPTIONS" --config "$CONF_DIR/curl/$CURL_ITEM.options" + printf '}' + + # check for expected strings in response + find_curl_strings "$CURL_OUTPUT" "search_response" "expected_response" + # check for expected strings in headers + find_curl_strings "$CURL_HEADER" "search_header" "expected_header" + + # retrieve SSL/TLS session info + find_curl_session_info "*" "TLS_INFO" "NO" + + # retrieve request header + find_curl_session_info ">" "REQUEST_HEADER" "YES" + + # retrieve response header + find_curl_session_info "<" "RESPONSE_HEADER" "YES" + + # check for regex match + find_curl_regex + +done <"$CURL_CONFIG" + +printf "}\n" +# end JSON + +echo "$CURL_RESULT" + +# clean up +cleanup + +unset CONF_DIR +unset LIB_DIR +unset CURL_EXECUTABLE +unset CURL_FORMAT +unset CURL_CONFIG +unset CURL_OPTIONS +unset CURL_RUN +unset CURL_OUTPUT +unset CURL_HEADER +unset CURL_SESSION_FILE +unset SERVICE_NAME +unset URL +unset CURL_REG_CASE +unset CURL_REG_MULTI + +exit 0 diff --git a/gui/metrics/curl.py b/gui/metrics/curl.py new file mode 100644 index 0000000..bf186c5 --- /dev/null +++ b/gui/metrics/curl.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2022-02-15 +# +# Metrics file for the curl plugin +# +# 2022-02-15: rewritten form cmk 2.0, based on the work by doc[at]snowheaven[dot]de +# 2022-02-20: added num_connects, num_redirects, size_download, size_header and speed_download +# 2022-03-13: moved cert time left graph to the end of graphs +# 2022-05-17: added scalars to cert_time_left + +from cmk.gui.i18n import _ + +from cmk.gui.plugins.metrics.utils import ( + metric_info, + graph_info, + perfometer_info, +) + +metric_info['time_namelookup'] = { + 'title': _('Time name lookup'), + 'unit': 's', + 'color': '11/a', +} +metric_info['time_connect'] = { + 'title': _('Time connect'), + 'unit': 's', + 'color': '21/a', +} +metric_info['time_appconnect'] = { + 'title': _('Time app connect'), + 'unit': 's', + 'color': '31/b', +} +metric_info['time_pretransfer'] = { + 'title': _('Time pre transfer'), + 'unit': 's', + 'color': '41/c', +} +metric_info['time_starttransfer'] = { + 'title': _('Time start transfer'), + 'unit': 's', + 'color': '13/b', +} +metric_info['time_total'] = { + 'title': _('Time Total'), + 'unit': 's', + 'color': '25/a', +} +metric_info['time_redirect'] = { + 'title': _('Time redirect'), + 'unit': 's', + 'color': '33/b', +} + +metric_info['num_connects'] = { + 'title': _('# of connects'), + 'unit': 'count', + 'color': '14/a', +} +metric_info['num_redirects'] = { + 'title': _('# of redirects'), + 'unit': 'count', + 'color': '24/b', +} +metric_info['num_headers'] = { + 'title': _('# of headers'), + 'unit': 'count', + 'color': '34/b', +} + +metric_info['size_download'] = { + 'title': _('Size download'), + 'unit': 'bytes', + 'color': '15/b', +} +metric_info['size_upload'] = { + 'title': _('Size upload'), + 'unit': 'bytes', + 'color': '25/b', +} + +metric_info['size_header'] = { + 'title': _('Size header'), + 'unit': 'bytes', + 'color': '35/b', +} +metric_info['size_request'] = { + 'title': _('Size request'), + 'unit': 'bytes', + 'color': '14/b', +} + +metric_info['speed_download'] = { + 'title': _('Speed download'), + 'unit': 'bytes/s', + 'color': '23/a', +} +metric_info['speed_upload'] = { + 'title': _('Speed upload'), + 'unit': 'bytes/s', + 'color': '13/a', +} + +metric_info['cert_time_left'] = { + 'title': _('Certificate Time left'), + 'unit': 's', + 'color': '33/b', +} + +graph_info['curl_times_total'] = { + 'title': _('Times total'), + 'metrics': [ + ('time_total', 'area'), + ], + 'scalars': [ + ('time_total:crit', _('crit')), + ('time_total:warn', _('warn')), + ], +} +graph_info['curl_times'] = { + 'title': _('Times'), + 'metrics': [ + ('time_redirect', 'line'), + ('time_starttransfer', 'line'), + ('time_pretransfer', 'line'), + ('time_appconnect', 'line'), + ('time_connect', 'line'), + ('time_namelookup', 'line'), + ], + 'optional_metrics': [ + 'time_redirect', + 'time_starttransfer', + 'time_pretransfer', + 'time_appconnect', + 'time_connect', + 'time_namelookup', + ], +} +graph_info['curl_speed'] = { + 'title': _('Speed'), + 'metrics': [ + ('speed_upload', '-area'), + ('speed_download', 'area'), + ], + 'optional_metrics': [ + 'speed_download', + 'speed_upload', + ], +} +graph_info['curl_size_download'] = { + 'title': _('Size download/upload'), + 'metrics': [ + ('size_upload', '-area'), + ('size_download', 'area'), + ], + 'optional_metrics': [ + 'size_upload', + 'size_download', + ], +} +graph_info['curl_size_header'] = { + 'title': _('Size header/request'), + 'metrics': [ + ('size_request', '-area'), + ('size_header', 'area'), + ], + 'optional_metrics': [ + 'size_request', + 'size_header', + ], +} +graph_info['curl_counts'] = { + 'title': _('Counts'), + 'metrics': [ + ('num_redirects', '-line'), + ('num_connects', 'line'), + ('num_headers', 'line'), + + ], + 'optional_metrics': [ + 'num_connects', + 'num_redirects', + 'num_headers', + ], +} +graph_info['curl_cert_time'] = { + 'title': _('Certificate time left'), + 'metrics': [ + ('cert_time_left', 'area'), + ], + 'scalars': [ + ('cert_time_left:crit', _('crit')), + ('cert_time_left:warn', _('warn')), + ], +} + +perfometer_info.append( + { + 'type': 'logarithmic', + 'metric': 'time_total', + 'half_value': 5.0, # 5 seconds -> bar half full + 'exponent': 2.0, # every double of 5 == 10% of bar more full + }, +) diff --git a/gui/wato/check_parameters/curl.py b/gui/wato/check_parameters/curl.py new file mode 100644 index 0000000..e6c64b8 --- /dev/null +++ b/gui/wato/check_parameters/curl.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2022-02-15 +# +# WATO file for curl plugin (bakery and check) +# +# 2022-02-19: moved global options under "Default settings" +# added per url settings +# added proxy settings (--proxy, --proxy-user, --proxy-digest, --proxy-basic, --proxy-ntlm, --proxy-anyauth) +# added follow redirects (--location) +# added don't verify certificates (--insecure) +# 2022-02-21: added tls/ssl version (--tlsv1.3, --tlsv1.2, --tlsv1.1, --tlsv1.0, --tlsv1, --sslv3, --sslv2) +# added http version (--http2, --http1.1, --http1.0) +# added header only (--head) +# added user authentication (--user, --basic, --digest, --ntlm, --ntlm-wb, --negotiate, --anyauth) +# added to proxy authentication (--proxy-negotiate) +# 2022-02-22: added option for cURL check plugin +# 2022-02-24: changed forbidden_chars from '<>| ' to '"<>#%{}|\^~[]` ' + "'" +# 2022-02-25: added plugin interval and timeout options +# added noproxy option to default settings +# 2022-02-26: added proxy protocol (--socks4, --socks4a, --socks4a, --socks5-hostname) +# 2022-02-27: added expected_strings option to default settings +# 2022-02-28: added expected_strings option to url settings +# added state_item_not_found option +# 2022-03-01: added options --limit-rate, --max-filesize, --max-time, --speed-limit, --speed-time, --connect-timeout +# added options --user-agent, --compressed, --no-alpn, --no-npn, --tcp-fastopen, --tcp-nodelay, +# added options --cert-status +# 2022-03-02: added options --referer -header and api_key_header (header from password store) +# 2022-03-05: added option --dump-header, state_header_str_not_found +# 2022-03-06: added option --verbose, --stderr cert_time_left +# fixed upper/lower levels +# 2022-03-10: optimized generation of upper/lower limit +# added transform for ms to s -> no need in th agent to change the value any more +# 2022-03-11: added check to avoid duplicate service names in url list +# added --location-trust, --max-redirs +# reworked redirect (--location, --location-trust, --max-redirs) +# reworked cert verify (-insecure, --cert-status, --ssl-no-revok) +# reworked advanced_settings (--no-alpn, --no-npn, --tcp-fastopen, -tcp-nodelay) +# removed get_session_data. moved to curl default options +# 2022-03-12: added --cacert +# added max_age +# 2022-03-13: added post data +# made some entries fordable (Default options/Per URL settings, CA Cert) +# changed url and service_name to curl_service tuple +# changed headers to list of tuple +# 2022-03-15: added regex pattern match for bakery +# 2022-03-18: added regex pattern match for check +# 2022-03-19: added options --dns-interface, -dns-ipv4-addr, --dns-ipv6-addr and --dns-servers +# reworked ip_address_resolution section +# added options --ftp-account, --ftp-alternative-to-user, --ftp-create-dirs, --ftp-method +# added options --ftp-pasv, --disable-epsv, --ftp-pret, --ftp-skip-pasv-ip +# added options --ftp-port, --disable-eprt +# added options --ftp-ssl-control, --ftp-ssl-ccc, --ftp-ssl-ccc-mode +# 2022-03-21: moved --connect-timeout, --limit-rate, --max-filesize, --max-time, --speed-limit, --speed-time +# to "limits" sub Directory +# 2022-03-22: added curl_error_code_to_ignore and http_error_code_to_ignore options +# 2022-03-24: added options --hostpubmd5, --hostpubsha256, --pubkey +# 2022-03-24: added options --key --passs +# reworked user_auth section +# 2022-03-25: added options --compressed-ssh, --list-only, --use-ascii +# added options --path-as-is, --ssl-allow-beast, --no-buffer, --no-keepalive, --no-sessionid +# 2022-03-28: added options --mail-auth, --mail-from, --mail-rcpt, --mail-rcpt-allowfails, --crlf, --upload-file (SMTP) +# 2022-04-10: added options to deploy cURL executable, no separate rules for curl and curl executable anymore +# windows/linux summarized under on option, no separate WATO rules per OS necessary anymore +# 2022-04-26: added check option clickable_url +# clarified option 'Don\'t verify certificate/pub key', 'Don\'t stop at verify errors (certificate/pub key)' +# 2023-10-21: split in service and bakery part +# modified for cmk 2.2.0 + +from cmk.gui.i18n import _ +from cmk.gui.valuespec import ( + Dictionary, + TextUnicode, + FixedValue, + Integer, + Tuple, + MonitoringState, + ListOfStrings, + Checkbox, + Age, +) +from cmk.gui.plugins.wato.utils import ( + rulespec_registry, + RulespecGroupCheckParametersOperatingSystem, + CheckParameterRulespecWithItem, +) + +bakery_plugin_version = '20220426.v0.0.8' + +# unsafe characters https://www.tutorialspoint.com/html/html_url_encoding.htm +forbidden_chars = '"<>#%{}|\^~[]` \'' + +_limits = [ + ('time_namelookup', 'Time name lookup', 'ms', ), + ('time_connect', 'Time connect', 'ms', ), + ('time_appconnect', 'Time app connect', 'ms', ), + ('time_pretransfer', 'Time pre transfer', 'ms', ), + ('time_redirect', 'Time redirect', 'ms', ), + ('time_starttransfer', 'Time start transfer', 'ms', ), + ('time_total', 'Time Total', 'ms', ), + ('cert_time_left', 'Certificate life time', 'Day(s)',), + ('num_connects', '# of connects', 'count',), + ('num_redirects', '# of redirects', 'count',), + ('num_headers', '# of headers', 'count',), + ('size_download', 'Size download', 'bytes',), + ('size_upload', 'Size upload', 'bytes',), + ('size_header', 'Size header', 'bytes',), + ('size_request', 'Size request', 'bytes',), + ('speed_download', 'Speed download', 'bytes/s',), + ('speed_upload', 'Speed upload', 'bytes/s',), +] + +_curl_check_elements = [ + ('clickable_url', + FixedValue( + True, + title=_('Render clickable URLs for HTTP/HTTPS (see Inline help)'), + totext=_(''), + help=_('Needs "Escape HTML in service output" rule or in Global settings enabled to work.') + )), + ('state_item_not_found', + MonitoringState( + default_value=1, + title=_('State on item not found'), + help=_('Monitoring state if the item is not found in the agent data. Default is UNKNOWN.') + )), + ('state_http_result_not_200', + MonitoringState( + default_value=1, + title=_('State on HTTP result not OK'), + help=_('Monitoring state if the HTTP return code is not in 2xx or 3xx. Default is WARN.') + )), + ('http_error_code_to_ignore', + ListOfStrings( + title=_('HTTP error codes to ignore'), + allow_empty=False, + orientation='horizontal', + # valuespec=Integer(size=3, minvalue=0, maxvalue=999), + )), + ('state_curl_result_not_0', + MonitoringState( + default_value=1, + title=_('State on cURL exit code not OK'), + help=_('Monitoring state if the exit code is not 0. Default is WARN.') + )), + ('curl_error_code_to_ignore', + ListOfStrings( + title=_('cURL error codes to ignore'), + allow_empty=False, + orientation='horizontal', + # valuespec=Integer(size=3, minvalue=0, maxvalue=999), + )), + ('state_verify_sll_not_0', + MonitoringState( + default_value=1, + title=_('State on SSL verify not OK'), + help=_('Monitoring state if the SSL verify code is not 0. Default is WARN.') + )), + ('state_expected_str_not_found', + MonitoringState( + default_value=1, + title=_('State on expected string not found'), + help=_('Monitoring state if one expected string is not found in the cURL output. Default is WARN.') + )), + ('state_header_str_not_found', + MonitoringState( + default_value=1, + title=_('State on expected header not found'), + help=_('Monitoring state if one expected header string is not found in the cURL output. Default is WARN.') + )), + ('state_for_regex', + Tuple( + title=_('State for regex pattern match'), + elements=[ + MonitoringState( + default_value=0, + title=_('State on regex pattern match'), + ), + MonitoringState( + default_value=1, + title=_('State on regex pattern don\'t match'), + ), + MonitoringState( + default_value=0, + title=_('State on regex pattern match info missing'), + ), + ], + )), + ('show_additional_info', + Tuple( + title=_('Show additional info'), + help=_('Shows RAW data from cURL in the service details.'), + elements=[ + Checkbox('Request headers'), + Checkbox('Response headers'), + Checkbox('(TLS/SSL) session info'), + Checkbox('RAW data'), + ], + )), + ('max_age', + Tuple( + title=_('Maximum age'), + elements=[ + Age( + title=_('Warning at'), + help=_('Warn, if the age of the page is older than this'), + default_value=3600 * 24, + ), + Age( + title=_('Critical at'), + help=_('Critical, if the age of the page is older than this'), + default_value=3600 * 24 * 4, + ), + MonitoringState( + default_value=0, + title=_('State if not available'), + help=_( + 'Monitoring state if the "Last-Modified" header is not available in the agent data. Default is OK.' + ) + ), + ] + )), +] + + +def _get_tuple_upper(_unit: str) -> Tuple: + return Tuple( + title=_('Upper limits'), + orientation='horizontal', + elements=[ + Integer(title=_('Warning at'), minvalue=0, unit=_(_unit), size=10), + Integer(title=_('Critical at'), minvalue=0, unit=_(_unit), size=10), + ], + ) + + +def _get_tuple_lower(_unit): + return Tuple( + title=_('Lower limits'), + orientation='horizontal', + elements=[ + Integer(title=_('Warning below'), minvalue=0, unit=_(_unit), size=10), + Integer(title=_('Critical below'), minvalue=0, unit=_(_unit), size=10), + ], + ) + + +for key, label, unit in _limits: + _curl_check_elements.append( + (key, + Dictionary( + title=_(label), + elements=[ + ('upper', _get_tuple_upper(unit),), + ('lower', _get_tuple_lower(unit),), + ], + )) + ) + + +def _valuespec_curl(): + return Dictionary( + elements=_curl_check_elements, + ) + + +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name='curl', + group=RulespecGroupCheckParametersOperatingSystem, + parameter_valuespec=_valuespec_curl, + title=lambda: _('cURL'), + match_type='dict', + item_spec=lambda: TextUnicode(title=_('cURL service name'), ), + )) diff --git a/gui/wato/check_parameters/curl_bakery.py b/gui/wato/check_parameters/curl_bakery.py new file mode 100644 index 0000000..3c7c37d --- /dev/null +++ b/gui/wato/check_parameters/curl_bakery.py @@ -0,0 +1,1061 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2022-02-15 +# +# WATO file for curl plugin (bakery and check) +# +# 2022-02-19: moved global options under "Default settings" +# added per url settings +# added proxy settings (--proxy, --proxy-user, --proxy-digest, --proxy-basic, --proxy-ntlm, --proxy-anyauth) +# added follow redirects (--location) +# added don't verify certificates (--insecure) +# 2022-02-21: added tls/ssl version (--tlsv1.3, --tlsv1.2, --tlsv1.1, --tlsv1.0, --tlsv1, --sslv3, --sslv2) +# added http version (--http2, --http1.1, --http1.0) +# added header only (--head) +# added user authentication (--user, --basic, --digest, --ntlm, --ntlm-wb, --negotiate, --anyauth) +# added to proxy authentication (--proxy-negotiate) +# 2022-02-22: added option for cURL check plugin +# 2022-02-24: changed forbidden_chars from '<>| ' to '"<>#%{}|\^~[]` ' + "'" +# 2022-02-25: added plugin interval and timeout options +# added noproxy option to default settings +# 2022-02-26: added proxy protocol (--socks4, --socks4a, --socks4a, --socks5-hostname) +# 2022-02-27: added expected_strings option to default settings +# 2022-02-28: added expected_strings option to url settings +# added state_item_not_found option +# 2022-03-01: added options --limit-rate, --max-filesize, --max-time, --speed-limit, --speed-time, --connect-timeout +# added options --user-agent, --compressed, --no-alpn, --no-npn, --tcp-fastopen, --tcp-nodelay, +# added options --cert-status +# 2022-03-02: added options --referer -header and api_key_header (header from password store) +# 2022-03-05: added option --dump-header, state_header_str_not_found +# 2022-03-06: added option --verbose, --stderr cert_time_left +# fixed upper/lower levels +# 2022-03-10: optimized generation of upper/lower limit +# added transform for ms to s -> no need in th agent to change the value any more +# 2022-03-11: added check to avoid duplicate service names in url list +# added --location-trust, --max-redirs +# reworked redirect (--location, --location-trust, --max-redirs) +# reworked cert verify (-insecure, --cert-status, --ssl-no-revok) +# reworked advanced_settings (--no-alpn, --no-npn, --tcp-fastopen, -tcp-nodelay) +# removed get_session_data. moved to curl default options +# 2022-03-12: added --cacert +# added max_age +# 2022-03-13: added post data +# made some entries fordable (Default options/Per URL settings, CA Cert) +# changed url and service_name to curl_service tuple +# changed headers to list of tuple +# 2022-03-15: added regex pattern match for bakery +# 2022-03-18: added regex pattern match for check +# 2022-03-19: added options --dns-interface, -dns-ipv4-addr, --dns-ipv6-addr and --dns-servers +# reworked ip_address_resolution section +# added options --ftp-account, --ftp-alternative-to-user, --ftp-create-dirs, --ftp-method +# added options --ftp-pasv, --disable-epsv, --ftp-pret, --ftp-skip-pasv-ip +# added options --ftp-port, --disable-eprt +# added options --ftp-ssl-control, --ftp-ssl-ccc, --ftp-ssl-ccc-mode +# 2022-03-21: moved --connect-timeout, --limit-rate, --max-filesize, --max-time, --speed-limit, --speed-time +# to "limits" sub Directory +# 2022-03-22: added curl_error_code_to_ignore and http_error_code_to_ignore options +# 2022-03-24: added options --hostpubmd5, --hostpubsha256, --pubkey +# 2022-03-24: added options --key --passs +# reworked user_auth section +# 2022-03-25: added options --compressed-ssh, --list-only, --use-ascii +# added options --path-as-is, --ssl-allow-beast, --no-buffer, --no-keepalive, --no-sessionid +# 2022-03-28: added options --mail-auth, --mail-from, --mail-rcpt, --mail-rcpt-allowfails, --crlf, --upload-file (SMTP) +# 2022-04-10: added options to deploy cURL executable, no separate rules for curl and curl executable anymore +# windows/linux summarized under on option, no separate WATO rules per OS necessary anymore +# 2022-04-26: added check option clickable_url +# clarified option 'Don\'t verify certificate/pub key', 'Don\'t stop at verify errors (certificate/pub key)' +# 2023-10-21: changed for CMK 2.2.0 + +import ipaddress +from cmk.gui.i18n import _ +from cmk.gui.exceptions import MKUserError +from cmk.gui.valuespec import ( + Dictionary, + ListOf, + CascadingDropdown, + TextUnicode, + FixedValue, + Integer, + Tuple, + DropdownChoice, + ListOfStrings, + TextInput, + Checkbox, + CAorCAChain, + Optional, + Foldable, + UploadOrPasteTextFile, + Alternative, +) +from cmk.gui.plugins.wato.utils import ( + rulespec_registry, + HostRulespec, +) +from cmk.gui.plugins.wato.utils import ( + PasswordFromStore, +) + +from cmk.gui.cee.plugins.wato.agent_bakery.rulespecs.utils import ( + RulespecGroupMonitoringAgentsAgentPlugins, +) + +# unsafe characters https://www.tutorialspoint.com/html/html_url_encoding.htm +forbidden_chars = '"<>#%{}|\^~[]` \'' + +# ######################################################################################################### +# +# cURL bakery options +# +# ######################################################################################################### + +_option_curl_service = ('curl_service', + Tuple( + elements=[ + TextUnicode( + title=_('Service name'), + help=_('Name for the discovered service, for example www.example.com'), + allow_empty=False, + placeholder='your.service.name', + forbidden_chars=forbidden_chars, + ), + TextUnicode( # ToDo: change to URL? + title=_('URL to check'), + help=_('URL to check with cURL, for example https://www.example.com'), + allow_empty=False, + size=90, + placeholder='https://www.example.com', + forbidden_chars=forbidden_chars, + ), + ], + orientation='horizontal', + )) + +_option_verify_remote_host = ('cert_verify', + Tuple( + title='Configure verification of remote host (certificate/pub key)', + elements=[ + # Checkbox('Don\'t verify certificate/pub key'), + Checkbox('Don\'t stop at verify errors (certificate/pub key)'), + Checkbox('Use OCSP to check certificate status'), + Checkbox('Disable cert revocation checks (WinSSL)'), + Optional(Foldable(CAorCAChain()), label='Certificate to verify against', ), + Optional(TextUnicode(size=40, minlen=32, maxlen=32, regex='[0-9a-fA-F]', ), + label='Expected MD5 hash of pub key'), + Optional(TextUnicode(size=60, allow_empty=False), + label='Expected SHA256 hash of pub key'), + Optional(Foldable(UploadOrPasteTextFile(title='Public key'), ), + label='Expected public key'), + ] + )) + +_option_redirects = ('redirects', + Tuple( + title='Configure redirects', + elements=[ + Checkbox('Follow redirects'), + Checkbox('Use authentication on redirects'), + Optional(Integer(minvalue=-1, default_value=10), label='Max. redirects', sameline=True) + ] + )) + +_option_regex_response = ('regex_response', + Tuple( + title=_('Regular expression to expect in content'), + elements=[ + TextUnicode( + label=_('Regular expression'), + placeholder=_('If empty regex search will be disabled') + ), + Checkbox('Case insensitive'), + Checkbox('Multiline string matching'), + ] + )) + +_option_advanced_settings = ('advanced_settings', + Tuple( + title='Advanced settings', + elements=[ + Checkbox('Allow SSL beast security flaw to improve interoperability'), + Checkbox('Convert LF to CRLF in upload'), + Checkbox('Disable Application Layer Protocol Negotiation (ALPN)'), + Checkbox('Disable buffering of the output stream'), + Checkbox('Disable Next Protocol Negotiation (NPN)'), + Checkbox('Disable SSL session-ID reusing'), + Checkbox('Disable TCP keep alive on the connection'), + Checkbox('Do not squash .. sequences in URL path'), + Checkbox('Use TCP fast open option'), + Checkbox('Use TCP no delay option'), + ] + )) + +_options_get_header_only = ('get_header_only', + FixedValue( + '--head', + title=_('Get header only'), + totext=_('Only headers will be downloaded'), + help=_('cURL will will only download headers. Implements the "--head" option'), + )) +_url_get_header_only = ('get_header_only', + DropdownChoice( + title=_('Get Header only'), + choices=[ + ('--head', _('Get header only')), + ('', _('Get the hole document')), + ], + )) + +_option_auth_user = ('user_auth', + _('Username/Password'), + Tuple( + title=_('Configure user authentication'), + help=_( + 'The password entered here is stored in plain text within the monitored host. ' + 'This is needed because the agent plugin needs to have access to the unencrypted ' + 'password to authenticate with the server.' + ), + elements=[ + TextUnicode( + title=_('Username'), + allow_empty=False, + forbidden_chars=forbidden_chars, + placeholder='username', + ), + PasswordFromStore( + title=_('Password of the user'), + allow_empty=False, + ), + DropdownChoice( + title=_('Authentication method'), + choices=[ + ('', _('Use cURL default')), + ('--basic', _('Basic authentication')), + ('--digest', _('Digest authentication')), + ('--ntlm', _('NTLM authentication')), + ('--ntlm-wb', _('NTLM authentication with winbind')), + ('--negotiate', _('HTTP Negotiate (SPNEGO) authentication')), + ('--anyauth', _('Any authentication')), + ]), + ], + )) +_option_auth_priv_key = ('priv_key_auth', + _('Private/public key'), + Tuple( + elements=[ + TextUnicode( + title=_('Username'), + allow_empty=False, + forbidden_chars=forbidden_chars, + placeholder='username', + ), + PasswordFromStore( + title=_('Pass phrase'), + allow_empty=False, + ), + Foldable(UploadOrPasteTextFile(title='Private key', file_title='Private key (PEM)'), + title='Private key'), + ] + )) +_option_auth = ('user_auth', + CascadingDropdown( + title=_('Configure authentication'), + sorted=False, + choices=[ + _option_auth_user, + _option_auth_priv_key, + (None, _('No authentication')), + ], + )) + +_option_proxy_server = ('proxy_server', + Tuple( + title=_('Proxy server'), + show_titles=False, + elements=[ + DropdownChoice( + title=_('Protocol'), + choices=[ + ('--proxy', _('HTTP')), + ('--socks4', _('SOCKS4')), + ('--socks4a', _('SOCKS4a')), + ('--socks5', _('SOCKS5')), + ('--socks5-hostname', _('SOCKS5 hostname')), + ]), + TextUnicode( + label=_('Server'), + help=_('Name or IP-address of the proxy server.'), + allow_empty=False, + size=40, + placeholder='your.proxy.server', + forbidden_chars=forbidden_chars, + ), + Integer( + label=_('Port'), + default_value=3128, + minvalue=1, + maxvalue=65565, + ), + ], + orientation='horizontal', + )) + +_option_proxy_auth = ('proxy_auth', + Tuple( + title=_('Proxy authentication'), + help=_( + 'The password entered here is stored in plain text within the monitored host. ' + 'This is needed because the agent plugin needs to have access to the unencrypted ' + 'password to authenticate with the proxy.' + ), + elements=[ + TextUnicode( + title=_('Proxy username'), + allow_empty=False, + forbidden_chars=forbidden_chars, + placeholder='proxyusername', + ), + PasswordFromStore( + title=_('Password of the user'), + allow_empty=False, + ), + DropdownChoice( + title=_('Authentication method'), + choices=[ + ('--proxy-basic', _('Basic authentication')), + ('--proxy-digest', _('Digest authentication')), + ('--proxy-ntlm', _('NTLM authentication')), + ('--proxy-negotiate', _('HTTP Negotiate (SPNEGO) authentication')), + ('--proxy-anyauth', _('Any authentication')), + ('--socks5-basic', _('SOCKS5 basic authentication')), + ]), + ], + )) + +_options_proxy = ('http_proxy', + Alternative( + title=_('Configure proxy server'), + elements=[ + Dictionary( + title='Use proxy', + elements=[ + _option_proxy_server, + _option_proxy_auth, + ], + required_keys=['proxy_server'], + ), + FixedValue('--noproxy', title=_('Don\'t use any proxy'), totext=_('')), + FixedValue(None, title=_('Don\'t configure an proxy (use system settings)'), totext=_('')), + ], + )) + +_option_tls_ssl_version = ('tls_ssl_version', + DropdownChoice( + title=_('Use TLS/SSL version'), + choices=[ + ('', _('cURL default')), + ('--ssl', _('Try TLS/SSL')), + ('--ssl-reqd', _('Require TLS/SSL')), + ('--tlsv1.3', _('Use TLS 1.3')), + ('--tlsv1.2', _('Use TLS 1.2')), + ('--tlsv1.1', _('Use TLS 1.1')), + ('--tlsv1.0', _('Use TLS 1.0')), + ('--tlsv1', _('Use TLS1.0 or greater')), + ('--sslv3', _('Use SSLv3')), + ('--sslv2', _('Use SSLv2')), + ]),) + +_option_http_version = ('http_version', + DropdownChoice( + title=_('Use HTTP version'), + choices=[ + ('', _('cURL preferred version')), + ('--http2', _('Use HTTP/2')), + ('--http1.1', _('Use HTTP 1.1')), + ('--http1.0', _('Use HTTP 1.0')), + ]),) + + +def _validate_ipaddress(pattern, varprefix): + if pattern: + try: + ipaddress.ip_address(pattern) + except ValueError: + raise MKUserError(varprefix, _(f'{pattern} is not a valid IP address')) + + +_option_address_resolution = ('ip_address_resolution', + Foldable( + Dictionary( + title=_('DNS options'), + elements=[ + ('dns_resolve_names', DropdownChoice( + title=_('Resolve names'), + choices=[ + (None, _('IPv4/IPv6')), + ('--ipv4', _('IPv4 only')), + ('--ipv6', _('IPv6 only')), + ])), + ('dns_source_interface', TextUnicode( + title=_('Source interface'), + regex='[0-9a-zA-Z]', + size=15, + )), + ('dns_source_ipv4', TextUnicode( + title=_('IPv4 source address'), + validate=_validate_ipaddress, + size=15, + )), + ('dns_source_ipv6', TextUnicode( + title=_('IPv6 source address'), + validate=_validate_ipaddress, + size=42 + )), + ('dns_servers', ListOfStrings( + title=_('DNS servers'), + valuespec=TextInput( + size=42, + validate=_validate_ipaddress, + ), + orientation='horizontal', + )), + ], + ), + title=_('Set DNS options'), + )) + +_option_ftp_settings = ('ftp_settings', + Foldable( + Dictionary( + title=_('FTP/SCP/SFTP options'), + elements=[ + ('ftp_account', TextUnicode(title=_('Account data string')),), + ('ftp_alternate_to_user', TextUnicode(title=_('String to replace USER command'))), + ('ftp_create_dirs', FixedValue( + True, + title=_('Create remote dir(s)'), + totext=_('') + )), + ('ftp_change_cwd_method', DropdownChoice( + title=_('Change working directory method'), + choices=[ + ('multicwd', 'one CD for each directory'), + ('nocdw', 'No CD. Use full path in SIZE, RETR, STOR etc.'), + ('singlecwd', 'use one CD with full path') + ] + )), + ('ftp_mode', + CascadingDropdown( + title=_('Passive/Actrive mode'), + sorted=False, + choices=[ + ('ftp_pass', + _('FTP passive mode'), + Tuple( + elements=[ + Checkbox(label=_('Don\'t send EPSV command')), + Checkbox(label=_('Send PRET before PASV')), + Checkbox( + label=_('Use remote IP form control channel for data chanel')) + ] + )), + ('ftp_active', + _('FTP active mode'), + Tuple( + elements=[ + Checkbox(label=_('Don\'t send EPRT command')), + TextUnicode( + label=_('Address to use'), + help=_( + 'Can be the interface name ie "eth0", a exact ip address, a ' + 'hostname/FQDN or "-" to use the same address used for the ' + 'control connection' + ), + default_value='-', + regex='[0-9a-zA-Z\\.:\\-_]', + ), + ] + )) + ] + )), + ('ftp_ssl_control', + Tuple( + title=_('Require SSL/TLS for FTP login'), + elements=[ + Checkbox(label=_('Send CCC after authenticating')), + DropdownChoice( + label=_('Set CCC mode'), + choices=[ + ('active', 'Active'), + ('passive', 'Passive'), + ] + ) + ] + )), + ('compressed_ssh', FixedValue( + True, + title=_('Enable ssh compression'), + totext=_(''), + )), + ('list_only', FixedValue( + True, + title=_('Enable list only'), + totext=_(''), + )), + ('use_ascii', FixedValue( + True, + title=_('Enable ASCII transfer'), + totext=_(''), + )), + ], + ), + title=_('Set FTP/SCP/SFTP options'), + )) + +_option_expected_strings = ('expected_strings', + ListOfStrings( + title=_('Strings to expect in response'), + # orientation='horizontal', + allow_empty=False, + valuespec=TextInput(allow_empty=False, regex='[a-zA-Z0-9\.]'), + )) +_url_expected_strings = ('expected_strings', + Alternative( + title=_('Override strings to expect in response'), + elements=[ + _option_expected_strings[1], + FixedValue(None, title=_('Don\'t expect any strings in the response'), totext=_('')), + ], + )) + +_option_header_strings = ('header_strings', + ListOfStrings( + title=_('Strings to expect in header'), + # orientation='horizontal', + allow_empty=False, + valuespec=TextInput(allow_empty=False, regex='[a-zA-Z0-9\\.]'), + )) +_url_header_strings = ('header_strings', + Alternative( + title=_('Override strings to expect in header'), + elements=[ + _option_expected_strings[1], + FixedValue(None, title=_('Don\'t expect any strings in the header'), totext=_('')), + ], + )) + +_option_request_headers = ('request_headers', + ListOf( + Tuple( + title=_('Set headers'), + orientation='horizontal', + elements=[ + TextUnicode( + label=_('Header'), + allow_empty=False, + placeholder='X-your-header', + regex='[a-zA-Z0-9_\\-]', + # size=30, + ), + TextUnicode( + label=_('Value'), + # allow_empty=False, + placeholder='value of header', + # regex='[a-zA-Z0-9_ :;.,=<>#\\-@\\+\\*\'/\\?!\\(\\)\\{\\}\\[\\]\\$\\&~\\^%|"`\\]', + size=50, + empty_text=';' + ), + ] + ), + allow_empty=False, + title=_('Set headers'), + add_label=_('Add header'), + movable=True, + )) +_url_request_headers = ('request_headers', + Alternative( + title=_('Override default headers'), + elements=[ + _option_request_headers[1], + FixedValue(None, title=_('Don\'t configure request headers'), totext=_('')) + ] + )) + +_option_mail_settings = ('mail_settings', + Foldable( + Dictionary( + title=_('SMTP settings'), + elements=[ + ('mail_from', TextUnicode( + title=_('Mail from address'), + allow_empty=False, + )), + ('mail_rcpt', ListOfStrings( + title=_('Mail to address'), + allow_empty=False, + max_entries=5, + )), + ('mail_auth', TextUnicode( + title=_('Mail originator address'), + )), + # ('oauth2_header', TextUnicode( + # title=_('Oauth2 token'), + # )), + ('request', TextUnicode( + title=_('REQUEST command'), + help=_('Send this command instead of LIST (POP3/IMAP) or HELP/VRFY (SMTP).') + )), + _option_request_headers, + ('message', UploadOrPasteTextFile( + title=_('Message to send'), + )), + ('mail_rpct_allowfail', FixedValue( + True, + title=_('Allow some mail to addresses to fail'), + totext=_('') + )) + ] + ), + title=_('Set SMTP options'), + )) + +_option_api_key_header = ('api_key_header', + Tuple( + title=_('Set API key header'), + help=_( + 'The password entered here is stored in plain text within the monitored host. ' + 'This is needed because the agent plugin needs to have access to the unencrypted ' + 'password to authenticate with the server.' + ), + elements=[ + TextUnicode( + title=_('API Key header'), + allow_empty=False, + forbidden_chars='|"', + placeholder='X-API-Key: ', + ), + PasswordFromStore( + title=_('API Key'), + allow_empty=False, + ), + ], + )) +_url_api_key_header = ('api_key_header', + Alternative( + title=_('Set API key header'), + elements=[ + _option_api_key_header[1], + FixedValue(None, title=_('Don\'t configure an API key header'), totext=_('')), + ], + )) + +_option_limit_rate = ('limit_rate', + Tuple( + title=_('Maximum UP-/Download rate'), + # show_titles=False, + elements=[ + Integer( + label=_('Speed'), + # default_value=3128, + minvalue=1, + # maxvalue=65565, + ), + DropdownChoice( + default_value='M', + choices=[ + ('B', _('Byte/s')), + ('K', _('KByte/s')), + ('M', _('MByte/s')), + ('G', _('GByte/s')), + ]), + ], + orientation='horizontal', + )) +_url_limit_rate = ('limit_rate', + Alternative( + title=_('Maximum UP-/Download rate'), + elements=[ + _option_limit_rate[1], + FixedValue(None, title=_('Don\'t configure a rate limit'), totext=_('')), + ], + )) + +_option_max_file_size = ('max_file_size', + Tuple( + title=_('Maximum file size'), + # show_titles=False, + elements=[ + Integer( + label=_('Size'), + # default_value=3128, + minvalue=1, + # maxvalue=65565, + ), + DropdownChoice( + # title=_('Unit'), + choices=[ + ('B', _('Byte')), + ('K', _('KByte')), + ('M', _('MByte')), + ('G', _('GByte')), + ]), + ], + orientation='horizontal', + )) +_url_max_file_size = ('max_file_size', + Alternative( + title=_('Maximum file size'), + elements=[ + _option_max_file_size[1], + FixedValue(None, title=_('Don\'t configure a file size limit'), totext=_('')), + ], + )) + +_option_max_time = ('max_time', + Integer( + title=_('Maximum transfer time'), + default_value=10, + minvalue=1, + unit='s', + )) +_url_max_time = ('max_time', + Alternative( + title=_('Maximum transfer time'), + elements=[ + _option_max_time[1], + FixedValue(None, title=_('Don\'t configure a transfer time limit'), totext=_('')), + ], + )) + +_option_speed_limit = ('speed_limit', + Tuple( + title=_('Minimum speed'), + # show_titles=False, + elements=[ + Integer( + label=_('Speed'), + minvalue=1, + ), + DropdownChoice( + default_value=1024, + choices=[ + (1, _('Byte/s')), + (1024, _('KByte/s')), + (1048576, _('MByte/s')), + (1073741824, _('GByte/s')), + ]), + ], + orientation='horizontal', )) +_url_speed_limit = ('speed_limit', + Alternative( + title=_('Minimum speed'), + elements=[ + _option_speed_limit[1], + FixedValue(None, title=_('Don\'t configure a lower speed limit'), totext=_('')), + ], + )) + +_option_speed_time = ('speed_time', + Integer( + title=_('Minimum speed time'), + default_value=30, + minvalue=1, + unit='s', )) +_url_speed_time = ('speed_time', + Alternative( + title=_('Minimum speed time'), + elements=[ + _option_speed_time[1], + FixedValue(None, title=_('Don\'t configure a minimum speed time limit'), totext=_('')), + ], + )) + +_option_connect_timeout = ('connect_timeout', + Integer( + title=_('Maximum time to connect'), + default_value=1, + minvalue=1, + unit='s', )) +_url_connect_timeout = ('connect_timeout', + Alternative( + title=_('Maximum time to connect'), + elements=[ + _option_connect_timeout[1], + FixedValue(None, title=_('Don\'t configure a maximum time to connect'), totext=_('')), + ], + )) + +_option_limits = ('limits', + Foldable( + Dictionary( + title=_('Limits'), + elements=[ + _option_limit_rate, + _option_max_file_size, + _option_connect_timeout, + _option_max_time, + _option_speed_limit, + _option_speed_time, + ] + ), + title=_('Set connection limits') + )) +_url_limits = ('limits', + Foldable( + Dictionary( + title=_('Limits'), + elements=[ + _url_limit_rate, + _url_max_file_size, + _url_connect_timeout, + _url_max_time, + _url_speed_limit, + _url_speed_time, + ] + ), + title=_('Override connection limits') + )) + +_option_user_agent = ('user_agent', + TextUnicode( + title=_('Set user agent'), + allow_empty=False, + placeholder='your user agent', + # forbidden_chars=forbidden_chars, + forbidden_chars='"|' + )) +_url_user_agent = ('user_agent', + CascadingDropdown( + title=_('Set user agent'), + sorted=False, + choices=[ + ('user_agent', _('Override default user agent'), + _option_user_agent[1],), + ('', _('Don\'t configure a user agent')), + ], + )) + +_option_referer = ('referer', + TextUnicode( + title=_('Set referer'), + allow_empty=False, + placeholder='http://your.referer.url/', + # forbidden_chars=forbidden_chars, + forbidden_chars='|" ', + )) +_url_referer = ('referer', + Alternative( + title=_('Override default referer'), + elements=[ + _option_referer[1], + FixedValue(None, title=_('Don\'t configure a referer'), totext=_('')) + ] + )) + +_options_compressed = ('compressed', + FixedValue( + '--compressed', + title=_('Request compressed response'), + totext=_('Request compressed response enabled'), + )) +_url_compressed = ('compressed', + DropdownChoice( + title=_('Request compressed response'), + choices=[ + ('--compressed', _('Request compressed response')), + ('', _('Don\'t Request compressed response')), + ], + )) + +_option_post = ('post_binary', + Tuple( + title=_('Send HTTP POST data'), + elements=[ + TextUnicode( + label=_('Content-Type'), + allow_empty=False, + forbidden_chars=forbidden_chars, + default_value='text/html' + ), + UploadOrPasteTextFile( + # title=_('HTTP POST data'), + allow_empty=False, + default_value=_( + 'This posts data exactly as specified with no extra processing whatsoever.\n\n' + 'To disable HTTP POST data in per URL settings leave this empty.' + ), + ), + ] + )) + +_option_url_settings = ('url_settings', + Foldable( + Dictionary( + title=_('Per URL settings'), + elements=[ + _option_verify_remote_host, + _options_proxy, + _option_redirects, + _option_auth, + _url_get_header_only, + _option_regex_response, + _url_compressed, + _option_post, + _url_api_key_header, + _url_referer, + _url_request_headers, + _url_user_agent, + _url_header_strings, + _url_expected_strings, + _url_limits, + _option_address_resolution, + _option_ftp_settings, + _option_mail_settings, + _option_tls_ssl_version, + _option_http_version, + _option_advanced_settings, + ], + ), + title=_('Override default settings'), + )) + + +def _validate_service_names(pattern, varprefix): + service_names = [] + + for url in pattern: + service_names.append(url['curl_service'][0]) + duplicates = [service_name for service_name in service_names if service_names.count(service_name) > 1] + duplicates = list(set(duplicates)) + if duplicates: + raise MKUserError( + varprefix, + _(f'There are duplicate service names. Please check the following service names: {", ".join(duplicates)}') + ) + + +_option_url = ('url_list', + Foldable( + ListOf( + Dictionary( + elements=[ + _option_curl_service, + _option_url_settings, + ], + required_keys=['curl_service', ], + ), + title='Services/URLs', + add_label=_('Add URL'), + movable=True, + # title=_('URLs to check'), + allow_empty=False, + validate=_validate_service_names, + ), + title=_('URLs to check'), + )) + +_option_default_settings = ('default_settings', + Foldable( + Dictionary( + title=_('Plugin settings'), + elements=[ + _option_verify_remote_host, + _options_proxy, + _option_redirects, + _option_auth, + _options_get_header_only, + _option_regex_response, + _options_compressed, + _option_post, + _option_api_key_header, + _option_referer, + _option_request_headers, + _option_user_agent, + _option_header_strings, + _option_expected_strings, + _option_limits, + _option_address_resolution, + _option_ftp_settings, + _option_mail_settings, + _option_http_version, + _option_tls_ssl_version, + _option_advanced_settings, + ], + ), + title='Default setting', + )) + +_option_plugin_interval = ('interval', + Integer( + title=_('Plugin run interval'), + minvalue=1, + unit=_('min'), + # default_value=1, + help=_( + 'This is the interval at witch the plugin runs. If not set the plugin will ' + 'run with every cycle of the agent (By default every 1 minute).' + ), + ),) + +_option_plugin_timeout = ('timeout', + Integer( + title=_('Plugin timeout'), + minvalue=1, + unit=_('min'), + # default_value=300, + help=_( + 'This is the maximum run time for the plugin. If not set the timeout for ' + 'the plugin is 1 minute by default.' + ), + ),) + +_option_curl_executable = ('curl_executable', + DropdownChoice( + title=_('cURL executable to use'), + help=_( + 'By default this plugin will use the system provided cURL executable. You can ' + 'decide to deploy a separate cURL executable. If you do so you need to install the ' + 'optional CMK package curl_executable<version>.mkp.' + ), + choices=[ + (None, _('Use system provided cURL executable')), + ('64bit', _('Deploy 64bit cURL version')), + ('32bit', _('Deploy 32bit cURL version')), + ] + )) + + +def _valuespec_agent_config_curl(): + return Alternative( + title=_('cURL'), + elements=[ + Dictionary( + title=_('Deploy cURL agent plugin'), + elements=[ + _option_url, + _option_plugin_interval, + _option_plugin_timeout, + _option_curl_executable, + _option_default_settings, + ], + required_keys=['url_list'], + ), + FixedValue( + None, + title=_('Do not deploy the cURL agent plugin'), + totext=_('The cURL agent plugin will not be deployed') + ), + ], + ) + + +rulespec_registry.register( + HostRulespec( + group=RulespecGroupMonitoringAgentsAgentPlugins, + name='agent_config:curl', + valuespec=_valuespec_agent_config_curl, + ) +) diff --git a/lib/python3/cmk/base/cee/plugins/bakery/curl.py b/lib/python3/cmk/base/cee/plugins/bakery/curl.py new file mode 100644 index 0000000..e9cd95d --- /dev/null +++ b/lib/python3/cmk/base/cee/plugins/bakery/curl.py @@ -0,0 +1,453 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2022-02-15 +# +# bakery curl plugin +# +# 2022-02-19: integrated per url settings +# added proxy settings (--proxy, --proxy-user, --proxy-digest/--proxy-basic/--proxy-ntlm/--proxy-anyauth) +# moved the curl.exe deployment to curl_windows.mkp package +# 2022-02-23: fixed handling aof user_auth settings +# fixed options in curl.cfg for windows +# 2022-02-24: removed deployment of curl.format +# added noproxy option in per url settings +# 2022-02-25: optimized plugin file write code +# added double quotas to curl options for linux to be equal with windows +# added plugin interval and timeout +# added noproxy option to default settings +# 2022-02-26: removed 'http://' prefix from proxy server address +# added proxy protocol +# 2022-02-27: added expected_strings options +# 2022-03-01: added options --limit-rate, --max-filesize, --max-time, --speed-limit, --speed-time, +# --connect-timeout and --user-agent +# 2022-03-02: added options --referer -header and api_key_header (header from password store) +# 2022-03-04: fixed Requests headers per url settings +# 2022-03-05: added option --dump-header +# 2022-03-06: added option --stderr, --verbose +# 2022-03-11: added redirection (--location, --location-trust, --max-redirs) +# added cert_verify (--insecure, --cert-status, --ssl-no-revoke) +# added advanced_settings (--no-alpn, --no-npn, --tcp-fastopen, -tcp-nodelay) +# removed get_session_data. moved to curl default options +# 2022-03-12: added --cacert option +# fixed api-key-header handling +# 2022-03-13: moved curl_item files to curl sub directory under MK_CONFDIR +# changed url/service_name from separate dict entries to tuple +# changed headers to read from curl_item_x.header file +# 2022-03-15: moved curl options from curl.cfg to curl_item_#.options +# added regex pattern match +# 2022-03-20: added dns_options, ftp_options +# 2022-03-21: fixed handling of limits and sub directories from wato +# 2022-03-24: added options --hostpubmd5, --hostpubsha256, --pubkey +# 2022-03-24: added options --key --passs +# 2022-03-25: added options --compressed-ssh, --list-only, --use-ascii +# added options --path-as-is, --ssl-allow-beast, --no-buffer, --no-keepalive, --no-sessionid +# 2022-03-28: added option --crlf +# added SMTP settings: --mail-auth, --mail-from, --mail-rcpt, --mail-rcpt-allowfails, --upload-file (SMTP) +# 2022-04-10: added deployment of cURL executables +# no separate WATO rules per OS necessary anymore +# reworked to make scalable for multiple OSs (THX to andreas.doehler[at]gmail[dot]com) +# 2022-10-21: moved from ~/local/share/check_mk/agents_bakery to ~/local/lib/check_mk/base/cee/plugins/bakery + +from pathlib import Path +from typing import List, Tuple, Dict, Any +from dataclasses import dataclass + +from cmk.utils import ( + password_store, +) +from cmk.base.cee.plugins.bakery.bakery_api.v1 import ( + FileGenerator, + OS, + Plugin, + PluginConfig, + register +) + + +@dataclass +class CurlConfig: + base_os: OS + curl_output: str + temp_path: str + conf_path: str + traget_path_bin: str + plugin_name: str + curl_executable_src: Dict[str, str] + curl_executable_dest: str + + +CURL_CONFIGS: List[CurlConfig] = [ + CurlConfig( + base_os=OS.LINUX, + curl_output="--output /dev/null", + temp_path="/var/tmp/", + conf_path="/etc/check_mk/", + traget_path_bin='../bin/curl', + plugin_name='curl.sh', + curl_executable_src={'64bit': 'curl-amd64', '32bit': 'curl-i386'}, + curl_executable_dest='../bin/curl', + ), + CurlConfig( + base_os=OS.WINDOWS, + curl_output="--output NUL", + temp_path="c:/windows/temp/", + conf_path="C:/ProgramData/checkmk/agent/config/", + traget_path_bin='..\\bin\\curl.exe', + plugin_name='curl.ps1', + curl_executable_src={'64bit': 'curl.exe.64', '32bit': 'curl.exe.32'}, + curl_executable_dest='..\\bin\\curl.exe', + ), +] + +bakery_version = '20220410.v0.0.7' + + +def get_curl_files(conf) -> FileGenerator: + field_separator: str = '|' # needs matching separator in the shell scripts + + # catch pre 20220410 WATO format + options: Dict[str, Any] = conf[1].copy() if type(conf) == tuple else conf + + url_cfg_lines = [] + url_list = options['url_list'] + default_settings = options.get('default_settings', {}) + + interval = options['interval'] * 60 if options.get('interval') else None + timeout = options['timeout'] * 60 if options.get('timeout') else None + + if options.get('curl_executable'): + for curl_config in CURL_CONFIGS: + yield Plugin( + base_os=curl_config.base_os, + source=Path(curl_config.curl_executable_src[str(options['curl_executable'])]), + target=Path(curl_config.curl_executable_dest), + ) + yield Plugin(base_os=curl_config.base_os, source=Path('curl-ca-bundle.crt')) + + curl_item = 0 + for entry in url_list: + curl_item += 1 + regex_option = 'no_regex' + save_output = False + # get service name and url, first try new format, then old format + try: + service_name, url = entry['curl_service'] + except KeyError: + service_name = entry['service_name'] + url = entry['url'] + + url_settings = default_settings.copy() + + entry = entry.get('url_settings', {}).copy() + # merge subdirectories + for key in [ + 'ftp_settings', + 'ip_address_resolution', + 'limits', + 'mail_settings', + ]: + if (key in url_settings.keys()) and (key in entry.keys()): + url_settings[key].update(entry[key]) + entry.pop(key) + elif key in entry.keys(): + url_settings.update({key: entry[key]}) + entry.pop(key) + + # merge sub directory with url_settings for back ward compatibility + for key in [ + 'limits' + ]: + if key in url_settings.keys(): + url_settings.update(url_settings[key]) + url_settings.pop(key) + + # merge per url settings with default settings + url_settings.update(entry) + + for curl_config in CURL_CONFIGS: + _os = curl_config.base_os + _curl_output = curl_config.curl_output + _temp_path = curl_config.temp_path + _conf_path = curl_config.conf_path + + _options = [f'--url "{url}"'] + _headers = [] + + # filter options + _options.append(url_settings['get_header_only']) if url_settings.get('get_header_only') else None + _options.append(url_settings['compressed']) if url_settings.get('compressed') else None + _options.append(f'--max-time {url_settings["max_time"]}') if url_settings.get('max_time') else None + _options.append(f'--speed-time {url_settings["speed_time"]}') if url_settings.get('speed_time') else None + _options.append(f'--connect-timeout {url_settings["connect_timeout"]}') if url_settings.get('connect_timeout') else None + _options.append(f'--user-agent "{url_settings["user_agent"]}"') if url_settings.get('user_agent') else None + _options.append(f'--referer {url_settings["referer"]}') if url_settings.get('referer') else None + _options.append(f'{url_settings["tls_ssl_version"]}') if url_settings.get('tls_ssl_version') else None + _options.append(f'{url_settings["http_version"]}') if url_settings.get('http_version') else None + + if url_settings.get('max_file_size'): + max_size, unit = url_settings['max_file_size'] + _options.append(f'--max-filesize {max_size}{unit}') + + if url_settings.get('speed_limit'): + speed, unit = url_settings['speed_limit'] + _options.append(f'--speed-limit {speed * unit}') + + if url_settings.get('limit_rate'): + speed, unit = url_settings['limit_rate'] + _options.append(f'--limit-rate {speed}{unit}') + + if url_settings.get('http_proxy'): + if url_settings['http_proxy'] == '--noproxy': + _options.append("--noproxy '*'") + else: + proxy_protocol, proxy_server, proxy_port = url_settings['http_proxy']['proxy_server'] + _options.append(f'{proxy_protocol} {proxy_server}:{proxy_port}') + if url_settings['http_proxy'].get('proxy_auth'): + proxy_user, proxy_password, proxy_auth = url_settings['http_proxy']['proxy_auth'] + if proxy_password[0] == 'store': + pw = password_store.extract(proxy_password[1]) + else: + pw = proxy_password[1] + _options.append(f'--proxy-user {proxy_user}:{pw}') + _options.append(proxy_auth) + + if url_settings.get('request_headers'): + for header in url_settings['request_headers']: + key, value = header + _headers.append(f'{key}:{value}') + + if url_settings.get('api_key_header'): + api_header, api_key = url_settings['api_key_header'] + api_header = api_header.rstrip(':') + if api_key[0] == 'store': + api_key = password_store.extract(api_key[1]) + else: + api_key = api_key[1] + _headers.append(f'{api_header}:{api_key}') + + if url_settings.get('redirects'): + location, location_trusted, max_redirects = url_settings['redirects'] + _options.append(f'--location') if location else None + _options.append(f'--location-trusted') if location_trusted else None + _options.append(f'--max-redirs {max_redirects}') if max_redirects else None + + if url_settings.get('advanced_settings'): + allow_beast, cr2lf, no_apln, no_buffering, no_npn, no_sessionid, no_keepalive, \ + path_as_is, tcp_fastopen, tcp_nodelay = url_settings['advanced_settings'] + + _options.append(f'--ssl-allow-beast') if allow_beast else None + _options.append(f'--crlf') if cr2lf else None + _options.append(f'--no-alpn') if no_apln else None + _options.append(f'--no-buffer') if no_buffering else None + _options.append(f'--no-npn') if no_npn else None + _options.append(f'--no-sessionid') if no_sessionid else None + _options.append(f'--no-keepalive') if no_keepalive else None + _options.append(f'--path-as-is') if path_as_is else None + _options.append(f'--tcp-fastopen') if tcp_fastopen else None + _options.append(f'--tcp-nodelay') if tcp_nodelay else None + + if url_settings.get('ip_address_resolution'): + dns_options = url_settings['ip_address_resolution'] + _options.append(dns_options['dns_resolve_names']) if dns_options.get('dns_resolve_names') else None + _options.append(f'--dns-interface {dns_options["dns_source_interface"]}') if dns_options.get('dns_source_interface') else None + _options.append(f'--dns-ipv4-addr {dns_options["dns_source_ipv4"]}') if dns_options.get('dns_source_ipv4') else None + _options.append(f'--dns-ipv6-addr {dns_options["dns_source_ipv6"]}') if dns_options.get('dns_source_ipv6') else None + _options.append(f'--dns-servers {",".join(dns_options["dns_servers"])}') if dns_options.get('dns_servers') else None + + if url_settings.get('ftp_settings'): + ftp_options = url_settings['ftp_settings'] + _options.append(f'--compressed-ssh') if ftp_options.get('compressed_ssh') else None + _options.append(f'--list-only') if ftp_options.get('list_only') else None + _options.append(f'--use-ascii') if ftp_options.get('use_ascii') else None + _options.append(f'--ftp-create-dirs') if ftp_options.get('ftp_create_dirs') else None + _options.append(f'--ftp-account {ftp_options["ftp_account"]}') if ftp_options.get('ftp_account') else None + _options.append(f'--ftp-alternative-to-user {ftp_options["ftp_alternate_to_user"]}') if ftp_options.get('ftp_alternate_to_user') else None + _options.append(f'--ftp-method {ftp_options["ftp_change_cwd_method"]}') if ftp_options.get('ftp_change_cwd_method') else None + + if ftp_options.get('ftp_mode'): + ftp_mode = ftp_options['ftp_mode'] + if ftp_mode[0] == 'ftp_pass': + no_send_epsv, send_pret, skip_ip = ftp_mode[1] + _options.append(f'--ftp-pasv') + _options.append(f'--disable-epsv') if no_send_epsv else None + _options.append(f'--ftp-pret') if send_pret else None + _options.append(f'--ftp-skip-pasv-ip') if skip_ip else None + else: + no_send_eprt, active_address = ftp_options[1] + _options.append(f'--disable-eprt') if no_send_eprt else None + _options.append(f'--ftp-port {active_address}') if active_address else None + if ftp_options.get('ftp_ssl_control'): + _options.append(f'--ftp-ssl-control') + ftp_ssl_ccc, ftp_ssl_ccc_mode = ftp_options['ftp_ssl_control'] + if ftp_ssl_ccc: + _options.append(f'--ftp-ssl-ccc') + _options.append(f'--ftp-ssl-ccc-mode {ftp_ssl_ccc_mode}') + + if url_settings.get('user_auth'): + if url_settings['user_auth'][0] == 'user_auth': + user, user_password, user_auth = url_settings['user_auth'][1] + pw = password_store.extract(user_password[1]) if user_password[0] == 'store' else user_password[1] + _options.append(f'--user {user}:{pw}') + _options.append(user_auth) + elif url_settings['user_auth'][0] == 'priv_key_auth': + user, pass_phrase, priv_key = url_settings['user_auth'][1] + pw = password_store.extract(pass_phrase[1]) if pass_phrase[0] == 'store' else pass_phrase[1] + _options.append(f'--user {user}:') + _options.append(f'--key {_conf_path}curl/curl_item_{curl_item}.priv_key') + _options.append(f'--pass {pw}') + yield PluginConfig( + base_os=_os, + lines=[priv_key], + target=Path(f'curl/curl_item_{curl_item}.priv_key'), + include_header=False, + ) + + if url_settings.get('expected_strings'): + save_output = True + yield PluginConfig( + base_os=_os, + lines=url_settings['expected_strings'], + target=Path(f'curl/curl_item_{curl_item}.search_response'), + include_header=False, + ) + + if url_settings.get('header_strings'): + _options.append(f'--dump-header {_temp_path}curl_header') + yield PluginConfig( + base_os=_os, + lines=url_settings['header_strings'], + target=Path(f'curl/curl_item_{curl_item}.search_header'), + include_header=False, + ) + + if url_settings.get('cert_verify'): + pub_md5 = None + pub_sha256 = None + pub_key = None + try: # 2022-03-23: added ssh settings + insecure, ocsp, no_revoke, cert_chain = url_settings['cert_verify'] + except ValueError: # 2022-03-23: added ssh settings + insecure, ocsp, no_revoke, cert_chain, pub_md5, pub_sha256, pub_key = url_settings['cert_verify'] + + _options.append(f'--insecure') if insecure else None + _options.append(f'--cert-status') if ocsp else None + _options.append(f'--ssl-no-revoke') if no_revoke else None + _options.append(f'--hostpubmd5 {pub_md5}') if pub_md5 else None + _options.append(f'--hostpubsha256 {pub_sha256}') if pub_sha256 else None + + if cert_chain: + cert_chain = cert_chain[cert_chain.find('-----BEGIN CERTIFICATE-----'):] + yield PluginConfig( + base_os=_os, + lines=[cert_chain], + target=Path(f'curl/curl_item_{curl_item}.ca_cert'), + include_header=False, + ) + _options.append(f'--cacert {_conf_path}curl/curl_item_{curl_item}.ca_cert') + + if pub_key: + yield PluginConfig( + base_os=_os, + lines=[pub_key], + target=Path(f'curl/curl_item_{curl_item}.pub_key'), + include_header=False, + ) + _options.append(f'--pubkey {_conf_path}curl/curl_item_{curl_item}.pub_key') + + if url_settings.get('post_binary'): + _headers.append(f'content-type: {url_settings["post_binary"][0]}') + yield PluginConfig( + base_os=_os, + lines=[url_settings["post_binary"][1]], + target=Path(f'curl/curl_item_{curl_item}.post_binary'), + include_header=False, + ) + _options.append(f'--data-binary @{_conf_path}curl/curl_item_{curl_item}.post_binary') + + if url_settings.get('regex_response'): + regex_str, no_case, multi_line = url_settings['regex_response'] + if regex_str: + save_output = True + yield PluginConfig( + base_os=_os, + lines=[regex_str], + target=Path(f'curl/curl_item_{curl_item}.regex'), + include_header=False, + ) + regex_option = 'nocase' if no_case else 'case' + regex_option += '_multiline' if multi_line else '_nomultiline' + + if url_settings.get('mail_settings'): + mail_options = url_settings['mail_settings'] + + _options.append(f'--mail-from {mail_options["mail_from"]}') if mail_options.get('mail_from') else None + _options.append(f'--mail-auth {mail_options["mail_auth"]}') if mail_options.get('mail_auth') else None + _options.append(f'--request {mail_options["request"]}') if mail_options.get('request') else None + _options.append(f'--mail-rcpt-allowfails') if mail_options.get('mail_rpct_allowfail') else None + + if mail_options.get('mail_rcpt'): + for address in mail_options['mail_rcpt']: + _options.append(f'--mail-rcpt {address}') + + message = [] + message.append(mail_options['message']) if mail_options.get('message') else None + if mail_options.get('request_headers'): + for header, value in mail_options['request_headers']: + message.append(f'{header}: {value}') + if message: + _options.append(f'--upload-file {_conf_path}curl/curl_item_{curl_item}.message') + yield PluginConfig( + base_os=_os, + lines=message, + target=Path(f'curl/curl_item_{curl_item}.message'), + include_header=False, + ) + + _options.append(f'--output {_temp_path}curl_output') if save_output else _options.append(_curl_output) + + if _headers: + yield PluginConfig( + base_os=_os, + lines=_headers, + target=Path(f'curl/curl_item_{curl_item}.header'), + include_header=False, + ) + _options.append(f'--header @{_conf_path}curl/curl_item_{curl_item}.header') + + yield PluginConfig( + base_os=_os, + lines=_options, + target=Path(f'curl/curl_item_{curl_item}.options'), + include_header=True, + ) + + url_cfg_lines.append( + f'{service_name}{field_separator}' + f'curl_item_{curl_item}{field_separator}' + f'{regex_option}{field_separator}' + ) + + for curl_config in CURL_CONFIGS: + yield Plugin( + base_os=curl_config.base_os, + source=Path(curl_config.plugin_name), + interval=interval, + timeout=timeout + ) + yield PluginConfig( + base_os=curl_config.base_os, + lines=url_cfg_lines, + target=Path('curl.cfg'), + include_header=False + ) + + +register.bakery_plugin( + name='curl', + files_function=get_curl_files, +) diff --git a/mkp/curl-0.2.1-20231021.mkp b/mkp/curl-0.2.1-20231021.mkp index 6bc8509eab69b27e22d18886a67900e485b092b2..9cfd47c3df86d21918551f060dc0b7a256172062 100644 GIT binary patch delta 16267 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¥|&=c1|&;elNxR<sD=i7gKTW_~FV)oyy?d?_k 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 Bs#5>} delta 16334 zcmbu`LvSxn@F4Kmww=7#wr$(CoqS(xzu2~I+qP|6|6lFa9(J!=HK(b$%v^f9`_m;e zz@;<55QI@sK))~v5cUAiPmCQ0hu&+yQSM#8AXY@-$uR2P&)swan-$)!`!DU><r({p z+>I)N9a-(c^XqnWeeXxd1FzOoQid)(??v%yEu-cHd^kS~4DOt$;!p#5ex}O}cM<l? zsapKkfoe5P+GS+}{}<gqPreXHAnY&QRIm=V?2X@s=7=JvGv<JuG(Jz4Tsbd!pCS7$ zBDf4%@Qlk4RsAEkEgOurpX}>KfF0XL6{ogEI2c<97jS0-NZE=@`fyf)U412Fi^n04 z(3vLv=k)zxZz(a$L#Sk5-}8-w?d6PnL?~&d)=fBm+bY55dWQb{&RvTh1XG{HA2wXj zXMV1mFPi@EX(xchj-R0BZ1#A#Ru4zmt7I~o<6ygh(Ok6(Esv9KW=IalKE4xbXk%V) zPpsEQz&5=E-)ALXR@ln&khpH037!XMdfeamzpmlaX<CCimeQG4OY#Bkx3Uzr1Z$En z34ARls5|5RB1ju5?2p|%kp0R!1=ccqpZz&<U13xx=;r{&r>%MyzE`><hoj!PJ%~~G zYn!(*okS3X##_H1@U@eb7eeVhr?U|ET<C+l58X`yp|CJpV;!NfO{gzCB6EjL2cBz$ z+bT56PL|#Dx*f!-)pNHzgZ1vy-IjHOZZ2GqT@tZU#Ne{Ca8@F<DyKXpH*cOwk~-vh zIB_t?SvUY?2tFOsAV$)^yPNF@ix>zXkl|ocN@BeYjvd;kjT}I?|8+3xrL~`@38F)3 z>m{SQc+M_}dW*#!>PJXg{NLE@2z(&ccx_G?2v+v}_bktLKA$o@_Gj}uTj1k8R(LRp z-6g?LtXgv+4*d`B8^!r@{Er#M6Q43R)FKk~W??{Ft1)AvFDwkOSPDH065K!A#i8~C zcyFNoYx^otw$%irOOZ|eRJns<wd+&`Nq$Ufq6E5gY>@M7Rbu&Eu~g$w`OejwmcQxj z9ohOCzZ*H+H+`+Y>>j=sQQC+N?ln`qf#U_YjWy{;1VlmbC-!Huo+a=g5329>$@)xO zdB1?>UzWQqK-SOd=33+>z@e3cL;YiyWB#{-479>eU2g2Jb&{Hr>6`=-_FEMdpxlgp z4=}5EiE-hGX>WC1^^ZFAE>)vp!b-J^PUb>-l13c!#zi$J*xzNJU&Mu~wnVpxiX0QX z3FS!tJ~+!p-f}~NO~Z(uOXGa^`Cb0Fof*(@)`&gZK-_qDjM1apwN(IIdak5-(e8~l zK*k31mrtFI4JK!{QrMtOpQNOSXZ|wnCYbS{0~H-OB;sKdjbbXd693pDT$Q*2?1?Z| z)#<y``~9gY<8m=^1jsH6s`vZr*Bupr`v|~9`#WR(^NS$`7w=&9d)c$?JNavG7lHP( z8|v`ihJ(Ead){wnX0}7cFH_@RFnb(AjD?GzQ-=g&j?P}~->A<Vl^ei~y}f%!{bw!* zH*RJlBH;oANPTnTA^90Nz}}Tz{hqK_?0`di;?V%HcKg@rw726fKK!?BT`&9h1H<YX z5e*Fi6l?7UKa($*Bd1eI%cNSMWe$4LsrJ*BAXex04jx>W)>HpVtx(Fn73#-grh6+y zPb@OJKmrGtq*JH9P*jCGaL&dfgZ3FpdnJK|56VO$M|-}O-G=!Pz}!f|azlw4w-Hlv zl5KoG4`atW5sVpqcx-gF(Q3}|0EI>H^*r*Who<Ey!=s7;R~>Qu7{oV1XPO+zDeSg6 zlHPaT<C4x&j4B(n#^I#gL`Np>L%^LRXntZ}c;Kzu@zgvpm<lYt@i_G|N}#7ssn1T0 zUOne^Fw-g1Lh6eR*i+%`r}=_#^PlHFF(%1uW3OfR!0*zmrnuFqg71z9TkTx#*mi@} z=Py?M!!SNsE@Q5xtH97naLwP7RY9Z?alzhkse!$G{`z^m1i4-ybBn3*-@_4_t6J_r zr>T%5_9_)D+=!dgyt4&yC1)#zg5b9%uRC2(3}soX=79JCD0EEJ&9-9F^pm{OeYz+* zVELswr<>f<Q|V5YFwCgc^&D8LE1xcLU8s|^BGsDf)1@UI`Ny6%raBJ~m*>&1q=fB@ zn`L_qnM>&t3N_`rtFJ;MU};<`P`Lg(ov=;m^=l@t#w;TQV*Zrsm2WQYb^`udj$Tu$ z=u8^(9wP(be!Q5xY<^Z{GAm|fbicLm4)KW}$L0}Pa+x_|HIPmdb-Cv11;MO@!+?=g zg>i+ZD)H!|(P+n8ansr~Wt(iJ(-PDz(?O*cH*fzzLSvwWTVALQbziPdxZQcs%Zc-6 zEAY-?-bh>RAh}j<z|9EF@oFZsKG{miR>*a2bg=?t2)dUHbFuPKrAWEMcqlbd1U0RU z;^z7cJM3Kdb73M{Y-?n>Ogrg&ERId%Ho$glV!aDCrp%0cV~|@hxAEoHIbmVHf}XZC zUD37>psHho&6QL}zu=-Xton8*Rz+oCviF?Hwi4uqIBU~4p0MhvcKQyg;h$^Boi=Aa zWzhnhT1Ev<S{Ifs>(ZsxjVLq97sNDHo8ZVPjaCxe#yv*BciYJlldJ-~SY@XQ&qbr; z=87pe8*P8cFefuP=GBa8j@coaoFshHzssO!!?4-A*zarT%I?fB;M^)6UW_69K;n9K zHIwf5W-_Lb$1re&*KjtC1K|BQU^FlJIvW7Kq3vs){OO|L*R?FO&3dZR-UVrX7N3?Z z=_aB;iYKjWGYFUNJ)^r&W*7a^Dnee?HTG=37Lu>(XAze~XF!b3TQWL9kJr83)vcSj zOrN(aFDukPs}nz+b3dP(H|t8KU(2=d6$V<ZlyP%i(pTGzcKrg9gBQ<3oec$PV!VJc zrRSjkf?X04C{T!}dNEGtOIW5F(+JkE6iWL7%lpnMWOB%|4xRU+>4Ag|!J7XQt8<Nz zPk2skRUfu^oIPi{xI)UOA#en4IeJ|8_)upYEvF`%xWi~@Nh+3?J)J4hX!boQcI2?E zv>5)GMqb^vWVcHMVQsph5w`&-_#R+cueCd1?IE}O%!1=%T}i+Q$Eq1<3_rP_u-lb4 zLFFmqF8%KzB5>C4l_}6(i^ps>ucDWy>}<H-VEho{Dp!`Prl5QJMON9WDdhg{$~XFV zQ*HMgF!zCoeQHRKVXA3z!6!LoA*Oc&rl=(+4jgCI8x?*#W}^2y`cb!YULAnvFGmm5 zhT7--_ia_^*;0i-kHP29{rPLu@2A0O>Mx@H4{YU1MZHZGSm|T_X&Ymh;g~L-&qGPU z*-oKprCuBdH;gSuB)mX3H@bBUW*|Vs;Y==GJb<GMo;||rEH(MXrPA#XoS2>yx4hM= ze9-hkR)?u+2c5E4QN2$WZ3Dn7{NJ|;l)x@Bb+vZ>BI6rA5~u84<&W)q;CRW(Dn^KN zP)L~XNDdLn9?HMdID%t|{Ki(%)?yNDwaCFdF{Vb(_4BFLpgscRMISmFW|v`w)L)pe z#+5Y~VZ?u0?^oWWKE`~km`foCWg$=LxEztsiA8tU_1A-Sc2+#LD^q~}N#mmu9lbAd z^IR<<$J=*FKLtz&Z&tziFvhvl#dM%8wC&;Cy3Rs*neHc9U}WI>3?;m(wVEs`7=jIl zSYo=>KUm-KN?oFdDE0EWeG2)5M)HMH>C~Xdo0Q?(IrQFR89|u^lzb&k8j>_Xt0wd~ zBIoWT$aV0q$YOUnxM_eZCtzSxe<FN==L>fF@H>(&p4os1bN<XbHXO$}{4=<wOo>qm zr)b$MBK&bn5Sx&nELhzmyNtsFk%?9((o_(b2W-V$piEdpwuZm+;#FxRm)6R*8F<tu z3`f4ZJ&*P*`C5fMllTR7b5B7eA`-Y<QhUOU^&;p=IEc2bAtM0NLLO<W6lZchhDbTC zraHayZs4T>*dyc5Wns=kBa13Fb!Qlp5b(MNvp$KQK@qiB%1GVspoqB-CPvyB<3x73 zmextf078fQtVvTvrG8aYHC3lS<fS2fKJE}eMge`yV?mtWrZ2)y)K*uH(<%%P_DaY& z1l|#ZiN<zE5OjbH?a?{XKHw+b&P`h(ab@wLC_8T<*S*`iefO6$M{`hGpy<Fq-QD9^ zM-X+*X>Jd}51d)eRmADl84t*$=WUENH&^OsRC3S)@P4?VzOZ4bzTlVBxDKj$7)T2q zy7L9`L3BPNx#yV<l@~!eVHOa|Xuy~6`OqtfuxNwz@G1baGoPfAA8XENzLt_5$x{N= zw0BBb`IhO_^bwOYP#O&?4aH*58bZ}7Irz#PWW%)4qhTmpPqTcN>w;s5cWAeZr&=lO zKyLFa*D}xP6OF}rbrx?J`>LH4_G+sW4bMd*ez>lO>JUCWQ)kwPEg&|_m?-qIYO*m& zrvUrT{cQkJFr6+wYkSIKLty?PtsxxN3P*hXG3C9(K1h|DnU1m5UUSXKArm3eoukbr ziwy{<7%!~R_1T6<k%l<B937;g$Y;g_@H5u)9->A5ecwISAgK}i(;pLSOs%tse$DTv z$91Qo58FgxqZOGC1~jidnJCJ?mZ)lA=xgh_3Zj6o{d~01`%gz96{N_(98tktH~(G- z#$0*2+$QWEve%=KCUZAa;61kk>#sAkf62si^99YnLHY5v)=`i(@lY|gP!nTx1?+u1 zzQ!tpU~^bJ2_Nd8d+nIzlSgibh7ey77^Vr(r#9y0Q>^ZWu?kd6Wlp?GsiaIo$i|AK ziKqa~L}g93yn81m@W9YQsiHr<E&82Fm%C=ZB;*K1V{@+OAA569AkNN>_vznkUkBWr zU++<IWm!LwA@7gCvP!Fc2fQ5eIbWw1hPFsNjB&XID^mvkflltM825s2m3&ueG`BQ> zjqa9t#jK0)(2_}32G#=zODO#u;5+h@KL>zkg&vMvCA!gxJug;LKWxWB#||6{>Nwh; zOw=7b#T*Fnn$a69$7qNJlKMgGF~{ub9rw?g#ha$xWyZweE%+)X@i54Ekt#K<uAAH2 z&3(Lb6S=^2$VOg>Y=d6tQq&I%LFM)1lQ#CQ@*$0Do`{eSF?ecx$8p5m_Z+c{5huVv zCc=)K)1ic|B-v{rd~Vy*)(M8kKqFnZqU|g1d2Win>~1edcKyQRANI$^u7!mtYD}o< zR@wxX_S~XdX}K(>-3$^YBfg~|@#^x45sp*Y)Y_7V5M~%G9)G6R>S&UUmz;g?<V3!D zQd@5Yw&dP8Es_nh6~4R%XNk?44pe}Oy1gCYs-vj%WbWE*+&c;zTUd@JRW)>nfMV4U zc{EE8htwwJOYqI%3S>4@@7Q;%q@y^K26pIL)XOQBo$U?WMKfw9g%CK@s+e!e_xshA zC;l&&*>hiu(2ufXIj#@0%~@y1*$HR^ZUA;I6KM}`Fa$(ZhXN)P(9&$~Q#(LnXpmxk zB2PVfas>X=df&P;9E*-Y3DIWK#}pS?Nu^3B5vTrKh%`d8<iTzt+aAZQ49XhHH7p>~ zFlS??jKsdDzs|;TD6b&aJtnIyRx7lu6ue0{U$}=MtQU{q+dXYvb|G1)Qgo@lxm*6! zY%|ht#uGgFX`Z2YgC?@la1zjPJ$JFQTDvxiYtV~B_ha`;R^6T+1+uPdl2}{H8lgxG z#e2ol5D%2!8s4-Pz?U6@$U|sUumlOiisT(p#l_Q%>-oV`H51)!-!k$qvXS)PKW><$ z>==qnZu&o7r4j1zKc8WQ{_)GBJQ`cm#oVtUo$8cpc|S_CwDX-xM)QE`Y!bcn_79q^ zWW2{;dA)JAM<K@aE^&pN9RI|QUzF~$WcuyYF1HW!AsgszO{6~ZBZeJ&%&<g?$Yxxh z$Rwg^-d|}jq)wPwvcFEFbCdJH|G@pT^K5zPW-ij<ql`bXU!)7=lvmfeI7mOu7um9V zvdx(#5@(|7^3^$V!=wNn(6Zvb)xX?ObaFT_PS6L8P<b`FP4r0x_YgykY&cv4qliLu z=D3tY1;VO;3#h(mV4n5wm?ml`Ikbi*Lx*zL*efJE^=o0;Ki3dtG?xJ!L86P#S>k<D zT~*JG3Z<7sak%1O^So;XUd_JJQGEYGhQ4k6+)5s)tP4ZN>U{v&Bib-q4k@<<uJr?= zs82ZqwsKs!Ld#9WAOaR|xL~myLQ?2Enrlw-@i3F7r!m*^Vc!YQ?3i##;BU9@2;35~ z7;J(kKE>jF0-3x1T~VE7RXkYuZ6_0jg4VQNDdkS80$M_AAGEd+YFwBRQj*JUhIXz` zGMy<^Z3(0S1x5hi%5xaSV0g#RMD|^XrT{&{J>oQe`6&3Ul#+ynRg;#{kyw%q%nsN@ zodH*6I(1eSh*VXsMTw;R0Fs&bAV-J0pkAJB`Kn%&JJ}n0NX76p@x<sOU?OP@lC9D) zv%O?!4>M3_2HDPIL+c|E@Us<vOJ+kapMgn|L)b#<KSqFz<59;I3Z^(ik|xqjZ(Gj^ zamA2(p=|Q=cV1x=u<r50!O&J#IpHOnA!Kvn8M5;b`sP#dK~t7yBGSMmIS3h>I%|0x z@IrY>tjEnSNFuF1NkX;JeS^a5+^JojrnP0w6#nafJ7kI+e-bMFn`mQ@i?DI^P;I=~ z8qS75toa2#&Pa~!{>FauG$)v3C6g>-2c#Z1U#E4nL#^W5IHHB);4uA{=KkIS?{0Yl zbKL3v-rk&S?rct{{l0GMd$=Usax((DAAWywuYRlP=GJyj4T=O>9o^i`g)K4N?^=2h z?B<h622Txo`XtvSvQi$vPj>_dv~a}3Lskk9dpJaPCoDRmWEg!o2xK?XcMhO_`?3S> zWSt0hH=n<{x4UsCk=O7d(%3-I9fTtx0UyGJ`(py3{nn&waj{=bxN=qu$I*aTj{s>M z7pt^U<NgO^nCyd$5Z`%tB#}+_{`ehwxek=_C8C;8Xx@!KTVg=K-#$~oD%HP8tuJgX zFV!eo@KSrwK?ND-JyaOzPMcSKie?@~vpJqhHYE<PC7c*VMP2YEoUC@6Mn>i?kegd6 zl&YHTl!e?3g}gx{>|kOOt;v9nT{MjaZ52yukrUM3gv-d4<kR%DBy&{735T&H!_t?% z>F*Z73ORT>2(poHp8C^aL6|0`TQ1ik-ba2<zvi@8$L{A+XTC_2petY^@5z@keGD|9 zxi!bKbo_FeB!|`fCYCP|i-?`sf!Ew1Ncu+sOvj#>)z$Feq`P7<P%yy8@Elkj>FnU0 z18ljXp#1rl#%7^T|7jgqK1@-6R%{;;Ms%H|MqA{QBy>KdUlJ){-EfAn5QVu^o*C;T zxi#=~JpOeyfyAIn&{SKp4b|1VaAxNG96JSf^aaotz(x0|fd)bQ!qu&OW|^(IcE=Lx zH3pxBIq%Q8xt<6+Hy1#)M_ex@v1{!rzIA-a-Gklx$e$jNxNlMFzGL-m*gVo%9hYim ztt!Q8X3zlx_^BJVuiArn?`D-TYc;{$NQlpz9esH^)zduh1sbTRR7rN<hf+e-S?G$y zBzlnz(*RmK4&?1Pie|S`GAt1rqa`Ot9dIHZVs4<vnI<&RnF7E<o4};4(f@g8!VD=& z6Y~B`#Zjx?itTZ6BWk?Y_}pxV7YBF8SarC;<M~pfM75c1&M-hJK|}pYeWF+1JCW@S zBj>jf(RcYiB+M3Jt6?0L;J|;t50mkSN$^xyosMlVS^evnQF^%TZElr~7RZg!oGd+9 zh_8+g;0X$I1P0jtNn<>TwT=OqgUv^(zs^C=d$lp_3<!b<&>nxt4WPm2&ZnC3n4=KP z2=k}%DsVA!R)EH4x_3|bP*FuiZ7|p9vk=>7DAW^k-6HP~f-D;Any|#30glB|sPf@< zW6yrC<3HMgBIvl$1Ku7ySd3XY&G8Alm?({d^1KE6IRJ<kBRlv&qg#56a0oe_`8N>Q z=Hkm3go^B+2=x$l=Efr8&X$F3W+ki!AMyxr1;bhDELs5ZgBisKJnla#j`*Xm+^zIO zVjVx`%^{70dA4{=&27z$?a(uLqu?@{ktNiGB`NscwQ^msT(c8>wYI~H{B5asGK<0m zkpUV>M+V#vZ|jF{b`P99!70*j-27P)!Ip$w!jwAzI})JXA{r}&0hhnp262k_#^EPj zZW~nx1}-$8%)L_d_@Ea4d)D=Z%r0dQUnwO%gjfd~0c3SYBtsIA2ME_hNvtZEMwqkg z3xb-<huhh)?%xT@A)dT=<Q``2JN1YZI?0Vn@&ZWKMM?g4T)3pHWP*F==|@eFC<&$a z$>YtG7Ln6AurrnDD_TMxv4jt)f5vN>63jt8&x^Lv$9$x-(v&<diUBKMD%|T&T_<j_ zh*&>m&#zPMvzQ$|N#~&Q<Cl;SQafXSB*W{4I>ri4=5R8x9^=Aj4!{nuSJG9%{XMuR z-2{x-b<+4)3RN}@N(`LO72lyV^6hF8?N$@t08f10pU{V!{ThWvwTV5KhvwHRi*jY^ z{5d`s-E=qwvhM1`smv<C$2I}yK~KD!OcT$w%Vmuzc8RzR42;T1T0@}`Vb#*SkO*I@ zL~=x)VrE{o9jIwfJh5jB*-+Glz%@o=g9T(v4M}&#!>Jc!eU)(K2^`J_$5Vqwf9ktf z5}v&__T?Y4L%MsuoIf@SHQ&@S=R4&)-?nKWK)_OhYjX#>?4b#ZS?`_AIQh2yE-D>r zt-&g|>G#)s)}hr+cFAfrQd={AL~}o8^SJI7YpL-c%~Tn?^M!T|py&^|YM74em;=VO zceER_hyWw}tFTZ{#Ub8GkCV&;e8hX}S0k-~tHl<`Yv6XPSxzAw$Q_pOi3XbAp1&>; z)hN^2g@XpF{1(JmxMi@-JHe@n-U=mU4Z}@qeUC!plb5x_i+aMT4j7Zv$I9+6ANi*n zzVvsIFpFr&<qVu>a8rLPi-ftw82~w_2q-ZGPJd$r=?yVp@Px^OND51W8R@EVhcg7l zD+)}Ci<HSIWDERQ2{Ci+#Jv2KBS1O%Ha?GCU8Imw5C*jo#!?_mY^0Dbijonc%2UMx z^i!T}sK8Y1WU?@fuiA*id=q$c@$p)NNlRd83gA;;TJMzCuV^m|Max<+VE|E>YM3xc zqBm#Bm!l@}-(utVu%`>x-8Aa**G%rPC#k?^Ev+prtv~JA9&c0mCHH4cjb8&&Wb7gT z>O|35#Qb!0mN+=X(d)(UiL)RnQ>Q}y>?J<1tvo&|SEd&S@55G^zyV7I({&|kt<@B- zFwV9rCYK%ymmQ<mW7a7ruK>pW@xM5?P{5zSW@NFJF_?NS5ckw1OIDtXMT<E-D_wHu z^XSv<2EA*Wo-A2kxj1jzONjGfzyPGBuE!)-pro{pW?-tDV8*YeuvD}#fLYJXTP?I; zv1D}0*kw`+2koP_Ofby)i0%=lUfLufkEV;A?ay^*=G|-N=`Y9C@&FeG`*l$nAUO%A zySQ?sL_YI4ZQ#TR-dO2Uy8WVVRrGm+Y7?ZTN^L8u)@KvOoX`+LjPo^JWvj-w5AS71 zv;~`D=}^N|q<v^<g3{9j&QT`m*YGI%{lTD|kA!43<ddy`HHP(Hp-(r=fFgi+=R@K9 z4BrY^qxJE`Yk8yX@c`njywUP_;-#!`pddhfVqa|dvad4k3)$jYXX%|W4aYv_@4~Dh zTC%vItgz=Mih%ot8T{}+R!HEZdf2xJc0<#Y{;)L@s(c|^Jfc`-;U%st;<c()K{Dml zV(YAdb5equQ$<}GYGh&-qDFs@kas(vu;xuYQsR!BdLrK<X8@WINoNwk5Z_@vO6h@% zvBG?k@d;tH0t8c*8Tq9B6iB<yc|scz`yB(|nLIUoFyRW+nzV%}bwMT+)YbcN1?}Y& zf$~I<rJ;qIWl3dv$smWdZPx#=KnM{Oj)CV6HtCDwkevK++5<s6c;lfzDi}<_6s$&| zAt!@wmZWo|DFF`n3Y(x@oIRVmVSpAH?9^%GuzLk-7;PtuI$6t2n2nc)Ir>TTeSCaR z7XXxq{)h)KYl>sDg6k7ifEn`ILNyZt{kLE1Ghu3T1CeL^b~W>Gj)?FK3WE^e0Tlu} zijkQP!|=O&q~ULA>CwxilJjS7Tw-STDg{|w7ntZ&2Y|Em>xtBeUW6YxQxkl;jDeUE z{5U0{o?$(pIMcx2<%o;daoxAPP(u4CI6iqn;OY4TD+PSSiRfDl0Y36v&5V_LIVUbJ zJNKp^a90?utfriOAcrnAVSGT#Ec-b7#gF382y5{ZxJ_4qL?HkZB?j>kJ<*2NM~A;# zLI?SB3~>DohVMRKBH`#$UVg~428FADKu_Tk&u?*`;(?5M*(Fe;jawJ_e$M;HkZ2!X z(RFl&>Y5*q5w!npN%Vr9qLEw3{>X`qWdYaeNM=%=yfNV!i=tO6cMn9|1QR5?->l?A z=-C8k<B7Vj;_hCys_VenA(0kF@Yj|_w<q>A2>^|6JV3X!81%d5(2(@qm_)(D$HQ&p z&S{%k3pwi~XB!}}K`Dl7DWamL8BZ#?&;xR7IM-8vDk7BHGW_W?lil6J1GerM#<N)H z-0@*N+M236?nJ$R;(9&|g!$EE(W@%bK17W}b|jW^Anq_(=r(aJ8hTi_jl3zq|E-pE z2$;sKJVMJS9NaU}c)>cPBe>1^`9NW#)gnj|Mi2{014oF(4ne}GXQHZ!w^5K3p5KaO zxRR=#*DN@gY@jyQdXQ8Lm~mqmU+a4M<?P|-@&WGgACYEGzIP8f#1~gbOd)E`iG0VU zlWyjl93ISf&97taxa(ZT&r}nVI#bP@0t~FFV-*`wU=F_3yliz88ytU(0)8FCH*uEL z)#gG8bwyZ<8cDkF$~CQLYqVot&jg=Y1o<mT<yL#ND_`&op)Xg5#X12_^EkTn@b2X! z$(Vl0siAuEK?8n<{b*uTYU*!RE4Do%-rgTp_#WU1l}cy|T{fe1<mf5rh1jSsfSnzJ z#u1|{IC=x^l*&CXcYwJkPgwA(GwKpc%<m9^v%gv01wDZfIr_gj66BP*yi^c%XV-7i zQFXKmoY1xDR=vC1PBdxI3p11$WhUP#a=>HAsvTB?&JgPxBvQyOh9YCbP%9DS)Ga8C z_75WQ=*{^~e?XgZ(c60DYT-Btz~u@ADipe<YF)W?fT9LMSjP72IPksUNM3mA0xL~u zKvlN^gjLrk6xt6u?xyKFk_@Xj-{~3dsVVBkY#xYe_9hlYKj+?_v!l^(7km89VDd!* zTaG;HbEZ?woeG1S5aw#WS*b-Tt9DLXWozB3-*P!j#q~b%g6~m{-XaADAo)HRzS`l5 zL?;j|jhI&)Hal=nDlyAaX5hMohQ}5%X5c%-YthAP(p0AY4~u3UD&2_c7ov*@RA3yb zaHP{0;z-17-&DmH++wN>CB*pgyYa<olHo3;S*sj;br#%g(B)Ky4*N9iXJMc)=U}44 zSv@M)5ldIMNm`qKrERbeAYSTNe(=xReRocfZk^4BVV&#igx6sz<vgePAA<2Wz>dSi z-?tZn@7Vzp^Viwwel%+1V|Y0C$I(ftx6M*lmsekxS5G&~Kv!2^x2wJF?f2*M;NaK) z*XQ}SKgxMorPtm~I+?ftUb18Pr}?D;uimk`8%y>*M^8Ckt8iWrz+r;9LUi}QsB3gh ztD8G+_2D(pTNj#{RdM_r2wtu%%xW=Z6Y{i<Vqpd5Sq*$LrK=T-`<#wld&d0j9L?bc z+K@NuLS=Cpo+&VRpl&qp86w$JQ0!?nzrY^~vpowGxqI9Z?XutS&?U-9XHBC&`~s&T z*x2kz^o84!EE@L=a3i9>-dgu2$BAGmV(3TA!_ns<>fr5VErv1ahVb+WcX1%HxFdHo zh>B7Sgwpf1!Q`l|NhWEDx10U*JpR)QSQl7!50hd17hNozNlrYXj-<%g=MD8FI(@bX zELclOi0@EE=)1#bzc+F+`<WZN3gCNx({OSy>MZM3G=eb$$TAK8cN?Ovk?e>kMuSO4 zHb|*g=1p=+J#E?P(0##etoEY!wxVxMSY$z`?eoCBa;j5X?PpYZv$U$FOr!ki#mQ6f z57s5|4BIHqKAeQmY1nD1f!_NI@Y)PPGLJY!$|GGRvF0{`;ntXa66>!PC*87z)Noo| zTl~ij4sTIF5PI4{DvIpAEgc!bQSpmUbITMuNpo)#&@0B?jzu%3%d!Exyjfsgt?c+n zqcGK2$T16<qSdE^6}0NkhRwkbNsB%ytq90BCdcCf-7aVcx>p$c(D5NG*xkqft`&Vc zZ)yJ$-O>=PwWrl4!$N7btxAiANAVimg+3XR0&MKYft-#92CI|d5MN;u(v4@Fb8Tu- zBhP0^z7|9*Yaxq;fxic2ei}sQXS>c$$s>QrY9cAsT*u#pC$Fp|k#(JiFp)cFaqfpl z9F+Cx5?jp3==R5;+?|Ls^}DUgKe_W5rz~GEQn;_>uG5u<QB5VrOpOgF>sKZqw&{uq z0V$Eg<<OfXX#yHs4G+LvGO*i$9lOd188+Is=&MwCU9Q-evLX<vj{<BVj_VB_X!JVj zn1jQU`uwBarr1_h&_`y)qCz2_T2xA&eWlW!ARUfWz}4~eG{su=jDEp>XR?I|ePM7L z4F4RUGh}MT#T8xZVs?cxvKzzEGJ}HG{=*%1(C2WPW#>^`_T!<L`^}yb$1a*wC(QA* zqlQ+TXSV2jAb~b?qWafdI_$JLU!>eT^z&Slb&6Cx=a{(<ld^A<Rn-%+>rue;Ot&Lp zW)6)=8^wRNy*=8+5Ya@lQgmJn_oqE{9R3STpN8+PxDFhhw_q^GD}qrJyI&Dx0G9)` zgX%9NTGqxC!a(p*mB=$q&dd}P57b)%Q~Ur1zD#{{d2*(NQi^|POe%ZmNQCE0`kdJm z1uv%^1n5c3*2*zXn)?{3I?Ab54(-jZ!KYC*#ih{E2L5JrGtnpyJ6Um;Yn{H0s`J=1 z<#UEU!U{Rsbc}%Eli1-M?cvi702vK`9OE%xYYN#UN~})3dRNn7N{G!@bjd%iaK*a? z4IE@4(*T*(dCYekbeARNn4cjV?{zZ+(*zs(Mnzg}%9U@kivbd`N1G)3c9JOS@r|$9 zD>xHE8g9~AuufKLSlO=kspLs5@U;p~1@x>W*+o|uqn!iUe-xNe{RU&40H^N;%M;zc z@~lMp-6BvsVDb53q+XA>EzAO&w>u`E&)T(>-u~_{X)BOS8Xsvp*+2Fr3zg%$2EHTB z4A2&V7q`vexb);!y1eYFUO1SMZ>pB_)-6HB?~a()&ytDaL{9Fr3}l0VQrRQ#;aMeS zs*<)lWu)H4&Z@CAMs68lz~o5Hn<C=~usG^%ZSA6%T|pxI-JbW$Urm43qPJz!w_(vy zm`Ehoe3T47TSH(ySXr_9p4nyQ$!kXHB^W%YwyDlu7A#R<{9u0u!p6?_JOT6SWonS= zvk+9LBcZ{Hy@aE1Bre7@xqaXVTAKNTQZzl74dC4Mxxj-ZGF}~3fF9L;8(GmXit`D$ zuks*a5Dlc-2%7fkS|eNiS?t?3<fufc1=`y+_F!9GP(g$Et(QHNppFG%V@T#4IsRFW z+obN)CIsZvha6|NDUa_7QE#w8d&{nFl}8F$;`$7TeBTx^CN{&K5eOCk#!-M4a7Iq( z%o;HN9|0{)ME|fJK*%ayJ`a7UQFm|sKwsm$$IC!BC6afQ`$>HQs$v;R63US!+#-&? zXu*t`oUQ9`F(`~^5I8}HSHED!@hMc#%G_-DIl~WAeP)~qnRiIrbi?yp<;<H=AE+$C z+<2!}NmpBAwJLU5w8R4Z1UikgOnQrdSIcobN~De|#<0yN;8HO12c=mYRiYinVwfuT z34vZoky4{rn$}7DPfHe!(iohxQh_0Fj%Y-I@3%oBE^6!9Kbl&Ja0ZP9&Oz-O%PWL- z=aA>g+KGjW8RxJywvc-N{CMREHwEN`4M}~B5DN)Gj$;mRGXpF$)1dLAuLfmgZ+KAg z!gB`y{C`R%0J~}Pyq#s@M=!fE3z84rPTpJH2l6>6S5fW_VYaj3a)Lu}Rm+2;XcKg0 z)1Qu?n*N=}8?y##xB&?4LGLGZ+6f@bCwgnG2W6U|qj`9Z5u2{-i|Cm@T6ejDH1j8| z)XnQmW$-|L;q}Dlp0jHrY%u5;q!zh+>X}s=KMJ!>0Jo`JR+n6V2MqV5C6Kyda`*vQ z<90Mv$cfzAIj>@^rudq3-obhz|Hsn)*6FT8^y#l;GFmq%$vT7$#aQ~3@K>O97YNUM zBE*hYN>%wy-7y;m;|S^YieaqeX>z#DsJbOL{q|tSr=7P+@KW^1U1*IL@is}slT$Dm zF=;~?z;L=>4R=?S^cP&SC)e_tpeq&nd%JZ*fRL7(7gzE+(K1SgOJuuCdf;T1n?mz@ zDnn<qdUdLSe7n4@Kf~aL%*FcbVsmb}gMfmaf6K^^>~`nhA3Z_H)slO}k_SB<ECE*L z%muhU2K(UhL<1vg0{t~RDOvWPEwhTyk*H20fRNatu+ieGw2w$~n~4zqyaopqkrN&F zhrm(ku|*_ao-JYY`Ha??!1rc1OFCNTR$izoIVfocGU3{t5odm%t&;5Dk6gkfk{F8% z5~tJrZB0*805A=X2RCf&LU&JOTR*yKDJO^29PDQ&Ct<R2;m~@m{Hd6TjHoV|-#}d{ zV4OjA$G;ysXODOtIIUXoFJ#Ppp7c20m2~~|BMA?dEb|crxeq7ec%+<<=}D*Wxypi* zem%V2e6^bH+f6cakO8ihsoQE;W5@InG1N7cJ?It})(~l^X6E;DW@G*_2Hr2JPq;dk z<;>WB`V-O|m~@JkTQ&m-KI;K!PJ#dy!25BW=u;)(6lIv8_NAPRbE^<<=aUOCVzKdz zBK9}tdoN7W*=;RtZ9JXG82C)H236i(i^k-m`>#^q0`k1^a*|BCCCBd=xrmEUH#L0= zW><(AChj0D(y5E2%*5Pze>S1Jjqi91gp=%MWc7IMc++`KfHY#cFe*PX!06xyz~Q34 zb61IarewD=ap?z9W$9&gVHs-56;5~Aeqlb+@jq$PBTDBH(~8~<{>Md^!Xe(xdkTv~ z$%TJ3|3z{H8yPO_hwi1GRgAu`j0|kB4#Li@Y1%B*guwM*i<}d2kOUj&Mxf~(v0U^E zZgLeh;g9uI^X;?+&%?LE+f(`|nIXu+Er~Jk_vABdHN%{Jlgi?#455!!W=TQXu@|T; zd!Ev}eG;$*W}*dFq6H%09H5oKsc}8`V9*?kP;@;l_W?`w@_J42^Pf_5Yvs<aj%jt+ z|NgJZvg1+pNC(A;SofYi^Skhq`@i^MJ%83tje%A`H-FQ$+l>}MUzE-!KvSaj^5tru z{G<2N{gLjsvzhz@P`T=}nf%?`zP<idvv}#+y5m3jV{z44320}*K0eZU`tG&T{v7<^ zy<4rt^VoL3O*0Iez_IzR9}XpesJ(9V;<WD`<Va?Fdl<*wus3{NR|qp$Q)sdITnWaX zt)-Yz^f&R~o9N(ybo9>+;Yh-8T>F2BwaQrV9~`Vp7Tz$tr`1)llb09{*zT571As^l zq9+3E!xMM6e&wQ#TPqa5g(5ctd928sP4Z<JKR!YCh>0#K%vC7YF&FX}Hta$`IwQL< z+Gq8S^}Qvu?3d*})$5tnk?y8Y8$F>B6qm^JQD{=Rcik3bbl_b0GWYY%-PIbr!1HoT z?}Q4Tr(oEC+1~g+pHY0TS6~95BtYp<llu$%hix>Z%392EECrVF$iql<?r0cyqxRHf zxFRH0WSAmWZ0-46VVO#;MdK{v1DJ6YF|?YJ5G8B#fNDhtK`BkGjQDz#TWE!Or2=;8 z%x=BN8xv*++trT89d8atgNhBdmkV#wTWdR}c0f_KHDL?1CChK5L5;2XFTf5VS|-fD zu%v`Bm~){Vfw+^@i@ixw<y<r$dqc|M!d+Ae`@VI=bBXvrbY6ZS(S?&0p$W<r4f`9q zGnaYh)^#04drt(N$V&YvUb&c@PQI>97a2WQpm}r0+T7Dp$3m&u=~+T_c9$t!@c1=P z8W(S05KXL1)!`*W&?wf>0<d&!J{yaRRbhxoiWb;HSyeE(UyE6CyH8D0+Ub$UoZxUh z%XGdAU#kdcE94qGQKD=%I{>4MIay2>l$dSH9>WTb4>5&$I#0%2(|)Nb=#6d(G!ul8 z>muHc4jw+bRX-biBIgN!0gF*R!P@YrG#({lOxv=<!d<d>gH~)90C)-dVaRINK=$2q z>G0rdk{E*q1~adiil6oWAG`&UIBV-WB7q5UJLDZMk`e?&+5?8QiaPtOmA{+<q6}xw zm6DjXsiq5J*HEWMqEayjs&55c`yX3(RtbX?PF|HmS?f2c9eRaHHS{0$B=lm0p4J0< zdQa`&ncp7;AVU7Bw+>SrW&;{T+A)`8HC1q1@jQYPfw;Qm=_)`vyf*)vXbZFM`!-HY zWeRDq=?uh)8Z_MU6e@P0lu`mMD9;OKRgIs(Gmd~}A`u#6!UziI(6r%=zfMQW5pU#{ z0Y{oc_F1<oCu_>s9?rw)7=B460fW5oA6YyXWKTK(fPj+!-%@D$_>smi6rAzy#eEDW za7fULCY=Qa2W;>>RJ;l`m=4BhC`5M>CNwrldIkzE{t1G)1&@u9>aWJ-M9W)QSb#el zVO%>u2x2RH-TY8{<8ULE<c#F#tDZ0c{P#E~dh+6|#i;b#v(yylU$7D9L;&I%@I^o2 zjPPLqDQ~ER^+1*}#^P1~1r%@MU}m-b5M_zAQ4V9=W8S4$&}r*-BdPePwv;PjHglTs zUkD611;ySHqIXndNQw|0dV`WY*#<*PIm$X<Ygm<dR)lDQzhKNaSEs#(?1my(c0xW! z>l~<?64@^}xU_8A%O&<<Z-8_}HshsVZHaOC!{3nyqv8;!If%NOf_Hi>C^L+T_;*7m zg5wFzY&B|Qf6(UjcH<e>lYk?hP>CO6-?V*3lv}0`gCxj%`-S=kicL2{PkFS~LFhUT zaigR4Wf~>eisMHlZx!>s)4<~+?!574Ei2J><V$wLZ4Av;u9X8s2tcRxuA7`C)>sAu z4G@mNwU6t>?}Nzj$1NW>))tz?&HMc^Opg6eE0o7a>U99;Vf#C<-;nJdunuAN0!swX zoT&L@9in!i0)x=UG=${l89ISjY&VP_>?xgZM8aYr;=vftgctl@uNFU~w?Y2rVXh-f z<Ce)KU(z`_&VSNohyVz1IaWx4?3Z$l*}@k+<25-D<nAa0$0+w(yzCYMd_4bm8P^}} z!an)t<%8a}kFg#dkJBV6wu$nw1{J;D#;<2bZk<(r8kBwu9z1d0svt(I78RRTD|8$l z)EXtnFp{jMpiFHzv!oQBIFc68T%~{%HhepWl?++1i1^y<7=XuSbE&PO<PB@pkkJQt z(f9S<-Qc`KX;iq++p!9_R@k>&J*C!YEB4a%Wi8<+0j)!6onQ-RcCnz=sl^HA+}8n! zzpn0BvjzRJg-|QrIkGWN$jg&|p+@k>`I``-t~#}FX!spL-xJ%v&g^OXQomIzj|~SK z5`9Sa3~Hk9YCvqTRo#k@%7SA#qx%&!bqi}@-Z{t!uJBKHXniE(j7*;z9>J)Z=^!{A zcyW9EpFB*2|AurRcpGkTaE;w{7nvdma?6ICu~{Ox3@|~7Qk?KeYfiT(U*<CTmT<Ag zFu*$KGU^%!pGC|SKsp$#r!I>)cMmUtDHWL~YJFoOp#WXT=odx|p1j+pTXG4_@S?J? z{o@3=I9=*=5E&q~Ajx_S0u9#)2M&EvAnEt)hEVYI%4%;k;4+~V6F|izT2}F<cLt;; zyz<JVFi_D+KZ;vS5GIhF%kfR(sxgqJ=uGhBK>?Clc>y+29^+Vq0UWPkF9<UmZry?t zSRyxNG=PkuP3LvV&H#=kU(0vnJI*uXVcd1O8Jqlh5_*R)49d#9r6BITLp%9o%f!Ow zWrWKz#iceNZ@~+hm#fUcK$x#dgiG=KEULLuMJuDO4uZIEY%v*_Djppu!!f~q2O1?b z=DaxMlyr4mEITl=KtvIUd#7t3Cd*9D4ZRa!CP0(Yl-O0!IQfTm?YQTixwSs8uR9vp z+2veL$Q9;ifU&YgP!!R*gN->HX`y)5u;`Ej6;e|I0pXVLndrM$E0m=#W4eb@9%Fnl z$%ee0p@43O6%B{U*!~0z_a>aU>SB=bNMQPMmX`F^v}ef@EUJxolkD|jv6N*zj$1(L z4q${m6t;o`XNZs)v^4B(K><#n`-|(S!*<pJOSyf<2Yi?8HuiwgVh4_fNnjIIkiPih zE5ocfF1~%XpduGzC3__~V?T(t41&feSO34+CC&6t(vFHTm~0x0gJ4AP4?R~M`BBqX z8734m?xF+(+DsHDSmHn81ACcaaL78KdH`z#5UnU_VP%lacGhE-6unPl45bdss=wZZ zl|t0TPHzu>#fcaO*vUyw)yu*->V*5drPi|k<BmfcX5dPH{_;b91TjODEa&NIB0YQ2 zVQAu<$Q{uX`bc&SKMpVdGj6t84KPzHGNaOlT#&=rT`yEJQPcO*?)I%PTcT|IR{>yX zI2*NzwilT$SLu}RmaV?;4%A$(hc$A!h%?C5gNU%S33RCl6-0Z(xu`K$tJ!YQD$#FW zM6Oinnz5?tD5-NatEV>3&vvPZJl5@z=EHE4_w#Nk8mOH{JGZT+$E_)r#YzjfwKF2p zLE<^lWS{(VC}tj`+-^2PU<R%brwIVof2_x{xi&sv%{q6181SAqKC|xhyh<Q299F|y zJ-aw<D6=MyGQzo*g%O>c`<E8(F1L6Wjn1{&XmBvm8A{>)vo#B=P>xhn0aEocs?8>L z4Z<!DDOL>O?9fV;^IlTtO9Fc#kFapc)~$KnfV99UJUx-SR)}y?{)?sds|t|m1!YT3 z>0;qT+R~Q1giAu$0u@oEk)3@=-@=bn45*kuc!i<NM9=c@tWApOT$zO@<LQJ%lP&bj zw5P)hea2e4F)K{;las^{NJAecrtd&r#S%_XiMrDKirMzgUCy@3BPqbZ_O7p_Sm?l8 zHmnC=t31!54l^v4gHih=K?2ahsUu?Ems+4Wab`u7h=7ynC}d@KgPI_W5S&WGo>V0n zt>Grj5RfQw`I`Km&eE1`zW>pK`hstf@Z#mC^!K5t<D8ni*n+WB8&J(5E8*q~h|U}H z71&owT}F>(3jg55Gl0v>aSBupM9@d}VPqA6`PNK~mBM{nBFCo)E&#wNlo+5{AF%hi zBdpUEJ7u~5&DWzKkdz+=&=;cktc&z=U~{Bj!dI;i6%+be;7Tu|a{cQwXfBph*a&?U zD*)LzsS5<Q&kY+~Cvq0;|Fa*Kh!+UGWl}jKTRpy&d&;_mqt)QysFE{CwDFIYuFHuN zNND+i$P<&2s6t{w#2LV*yXWBrRI!VW^<2WsILr={=B<LFc~sCZB*v?O5`RNAEObuW z3C7sJi%dIyX9nTik<MnuXw$<Nft-wBTK}|Un6r8pAiW!bdlpPCHc94QhJuYtUJi+* z-NeAw5?WU?H(z!Dy1Ri@Mr)m&5h=s_>U=hJ!>%Jagy%{m!vv5eI&6@%We_*+q(s@2 zA@0|}2i{4is-2SE8)#Hzz-<q|(<QHl9LW*QSixy;CLak<YDs%A<W9U_u~Zw93j@t5 zFR~}rTV(D#eDyuFh#ijpY&R1Oy#;;m#xn&1M=gTC{K2?jz`nZ&pJhX8pI9}9KmXBs zDH9T?JqUU$SOf4idnZ`6R}@x?Xy^Y)v6NyJrT0PdZgSQ6tH}GQTqbvYH-cMiIH;!r zIrusM_jG)eB7x6`Gm<zR#vDdPCuLpGKWx9u1UI>Jidkz6&v`j`wvA#!>nln7AQs|< zw#|n516l<2X>$+5T*!hVo~Vp`^<2`u?7L<tBg?b|bq}!O56iTm`X|6dewASed}A26 z!kixzXCW*DvN8HEHDwAS%dRL2+j1EH-(ea!^75#mw|r9vQPbx^POa}#q`)fAIEo5m zu;IJe8d96Iyb%Ah_WWD925}kl-@XHJ^l3Z`)qcmdB|>J%+*r9AEVREL$LTN$1PC&* zSk<OcjsQS(Yf0L-P<9Wssa~wJfwUKDx3mK{N04&O7z|GK`^I=VanR=DRf;&D<ySsC z76bex%IU0U`GD-GMQ2FK*icWUt!ai`1=PGnkBbCFTS)MzI=neY5Bi#=@5!)!#)UR{ z;>kxPT4W}9N`Z%vwh5E%gGtl-Z?Mkq<$QmF#53TroK8v=gu5l=_^Ib67UOkz`e9V& zxmU$p$Te+?E4A!oi>b16DaIxwBO89oDJ<!I3?p==t8zUGD(8)=cS(<P5F6n(y_QyH zD<t?>lr2mQWE4&rlfuPN=jTp*`zc%O+V~%RlaHmYX14gDYBw=ADb%6(`@&qCk`Lr= z3k|>+-YY*g-p=dVehz`X<hkG0pDiu6<?pBp8~N9iLcsCyo-^MwONUPZX(G2?Gch7l zzcTOnui1iNPXAMAfArM)*PK~W?n-cT!Xd>8YInEPIlZpLxxerY??=WM8@X2|4sj;V z_=6$?t1cN%9+1iF<Y+_5(^)XhYEiu$xe)+s)Hr!$mQW@za)xAJYXq8-)pxvFrXSV! z3(GA8Ld|!|Ku3Zvv*s1c7Mm|iLSWMbG9Dz0=-cbJG)SbS72xcCcW>zMYMZ+=K-B$J z;^Gy&B{Ov*V)GZsz?zw%0xIHh3Y?8D#MGK0YJdH=1qZ$MNeqn|;X=m)Sb#)Va~+T@ ziIFy7&gg&7nQ~kzZ!|9Fq|3Hxzc!F0_}ylZ2T>hanNhmB`e){m@l2mzqmc3Jk=L&v zKGHlyJxXKrr`povk`m)Vu5Xb#p<zRzJqOEWPKq(jBD86&=y1%njAodoAILME{SJNX zDc`_Q>MjFC*yt67Igs$%OAI=J*B%h8r;2I~{Bz=B7o*PranAr`BKRG3M8v7>;Gzh+ z8s&`k;JS?1QRLkr99p$a-RLUhCl>-H7?%je`B6BC>aX|-{NVv_Fs0%xBOF35S&bn; zTamJPF0D?pHFuC4xyq#$i8tBIf}ug&s`X=wg`m0<l1Zbbm^U(YjjkX@E({Rh3qsL8 zpLAZ)Syp_(TJKAuRA6cuY{t)alzXS_A7MNs3_%r=?i_v<?@8xtw0Y#kmAe%`&rc)L z7y2{$uO!wcZow>8j6d4y?9q9+732ws0K8OOY}|n>GhEuhkV5IKH}IK0X22Lkel7IP zr=G23dTA)fN$|s~wuqaAs{-)5;hkS~mj1CjnU)AsUDR$=Q?^oi<(AwaIz^sGlmnNn zs+HjgB8+ovtSVy1;D58p*;G~lE$NJ%F0^;{Cq9?JL<ud&y8f#Cjh^<otWwr%9sD6g zLD@X_h`~2f;1aACLyU9<p82Ui_RGj0Lt@F@w5*@(4})>jK%0cR(K29~NtZRYavaI1 zS=1*HROvP$5^k2%<adhITS7^ED8iPTo(*eO59V*HiTGU~lt|^=bY+wD|L=P0rTqS# Nw7nPv0xbgp`X8)H&@%u4 diff --git a/packages/curl b/packages/curl new file mode 100644 index 0000000..a429ea9 --- /dev/null +++ b/packages/curl @@ -0,0 +1,34 @@ +{'author': 'Th.L. (thl-cmk[at]outlook[dot]com)', + 'description': 'cURL agent plugin\n' + '\n' + 'Monitor URLs from the perspective of your monitored hosts, ' + 'not the monitoring server ;-). \n' + '\n' + 'The plugin is based on a idea by Christian Wirtz ' + 'doc[at]snowheaven[dot]de and Ingo Hambrock.\n' + '\n' + 'This agent plugin for Linux and Windows is build around the ' + 'curl command line tool from https://curl.se.\n' + '\n' + 'The executable is not included in this package. The plugin ' + 'will use by default the system provided curl executable.\n' + '\n' + 'Deployment and configuration of the plugin is integrated in ' + 'the CMK bakery.\n' + '\n' + 'Note: this plugin needs at least curl version 7.70.0 from ' + 'April 29 2020 to work. You can use my CMK package ' + '"curl_executable.mkp" to deploy a compatible version.\n', + 'download_url': 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/curl', + 'files': {'agent_based': ['curl.py'], + 'agents': ['plugins/curl.sh', 'plugins/curl.ps1'], + 'gui': ['metrics/curl.py', + 'wato/check_parameters/curl.py', + 'wato/check_parameters/curl_bakery.py'], + 'lib': ['python3/cmk/base/cee/plugins/bakery/curl.py']}, + 'name': 'curl', + 'title': 'cURL agent plugin', + 'version': '0.2.1-20231021', + 'version.min_required': '2.2.0b1', + 'version.packaged': '2.2.0p11', + 'version.usable_until': '2.3.0b1'} -- GitLab