From 3ae427459adfcb35dc1babd69ce20e437ee63829 Mon Sep 17 00:00:00 2001 From: mellis Date: Fri, 26 Jan 2024 15:05:03 +0100 Subject: [PATCH] MKP 1.2.0, added version check --- README.md | 33 ++++-- .../base/plugins/agent_based/mailcow_info.py | 32 +++++- .../check_mk/agents/special/agent_mailcow | 100 ++++++++++++++++-- local/share/check_mk/checkman/mailcow_info | 1 + local/share/check_mk/checks/agent_mailcow | 1 + .../web/plugins/wato/mailcow_info_rules.py | 10 ++ .../plugins/wato/mailcow_mailboxes_rules.py | 2 + .../web/plugins/wato/mailcow_params.py | 2 + mkp/Mailcow-1.2.0.mkp | Bin 0 -> 14374 bytes 9 files changed, 165 insertions(+), 16 deletions(-) create mode 100644 mkp/Mailcow-1.2.0.mkp diff --git a/README.md b/README.md index 4dbb3b9..b9745d3 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,6 @@ Email domains with quota usage, number of mailboxes, number of aliases Mailboxes with quota usage, number of messages, last logins via IMAP/POP3/SMTP -You have to create an API Key for the MKP to work. Login to your Mailcow instance and go to - - System|Configuration - -Take a note of the section API, expand it and activate "Activate API" in the section "Read-Only Access" - -Don't forget to insert the IP of your CheckMK instance into the textbox "Allow API access from these IPs/CIDR networks". ## Caveats: Tested only with dockerized versions of Mailcow @@ -23,6 +16,19 @@ Tested with Mailcow version 2022-07a and higher Please feel free to add other working environments via review/email. +## Upgrade from older MKPs (before 1.2.0): +If you upgrade from a already installed version before 1.2.0, you have to re-create your rules for the "Mailcow Server Information" (reason: massive parameter changes). + +## Steps to accomplish this without problems: + +1. Take a screenshot of your settings in the above mentioned ruleset +2. Assure that you have access to the API keys you used within the current rules +3. Delete all rules for "Mailcow Server Information" +4. Install and enable the new MKP +5. Re-create your rules with the previously saved information from steps 1 and 2 +6. Apply your changes + + ## General installation instructions: 1. Upload and enable the MKP @@ -38,8 +44,21 @@ Please feel free to add other working environments via review/email. 11. Execute a service discovery on your Mailcow host(s) and accept the newly detected services 12. Feel free to adjust the various parameters for thresholds within the available parameter sections (Setup, "Service monitoring rules" after searching for "Mailcow") +## How to create the API Key: + +1. Login to your Mailcow instance with an administrative user +2. Got to "System|Configuration" +3. Take a note of the Section "API" and expand it +4. Within the section "Read-Only Access": + 4.1 Activate the checkbox "Activate API" + 4.2 Take care that the IP your CheckMK server ist listed in the input field "Allow API access from these IPs/CIDR network notations" +5. Save your changes and transfer the show API key to your CheckMK instance + + ## Version history: +2024/01/26 1.2.0 Added password store option for the API key, added version check + 2023/09/22: 1.1.4 Bugfix for the last bugfix :-) 2023/09/22: 1.1.3 Bugfix for usage calculation if quota is not set (mailboxes) diff --git a/local/lib/python3/cmk/base/plugins/agent_based/mailcow_info.py b/local/lib/python3/cmk/base/plugins/agent_based/mailcow_info.py index def00bf..2c7568c 100644 --- a/local/lib/python3/cmk/base/plugins/agent_based/mailcow_info.py +++ b/local/lib/python3/cmk/base/plugins/agent_based/mailcow_info.py @@ -40,6 +40,9 @@ def check_mailcow_info(params, section): levels_solr_documents = params["levels_solr_documents"] version = section[key]["version"] + git_version = section[key]["git_version"] + check_version_enabled = section[key]["check_version_enabled"] + update_available = section[key]["update_available"] num_domains = section[key]["num_domains"] num_mailboxes = section[key]["num_mailboxes"] num_global_messages = section[key]["num_global_messages"] @@ -58,9 +61,18 @@ def check_mailcow_info(params, section): yield(Metric("mc_solr_documents", solr_documents, levels=levels_solr_documents)) # create overall result - summary = f"Version is {version}" + if check_version_enabled: + if update_available: + summary = f"Update available: Running version is {version}, Github version is: {git_version}" + state = State.WARN + else: + summary = f"No update available: Running version is {version}, Github version is: {git_version}" + state = State.OK + else: + summary = f"Version is {version}, Update check is disabled" + state = State.OK details = f"Mailcow version: {version}\nNumber of domains: {num_domains}\nNumber of mailboxes: {num_mailboxes}\nNumber of messages: {num_global_messages}\n\nSolr size: {render.bytes(solr_size)}\nNumber of Solr documents: {solr_documents}" - yield Result(state=State.OK, summary=summary, details=details) + yield Result(state=state, summary=summary, details=details) # Create result for number of domains warn, crit = levels_num_domains @@ -103,6 +115,7 @@ def check_mailcow_info(params, section): def parse_mailcow_info_section(string_table): + #pprint(string_table) parsed_data = { "mailcow" : {}, } @@ -125,6 +138,17 @@ def parse_mailcow_info_section(string_table): elif solr_size_unit == "GB": solr_size = solr_size * 1024.0 * 1024.0 * 1024.0 solr_documents = int(line[6]) + git_version = line[7] + update_available = line[8] + if update_available == "True": + update_available = True + else: + update_available = False + check_version_enabled = line[9] + if check_version_enabled == "True": + check_version_enabled = True + else: + check_version_enabled = False parsed_data["mailcow"]["version"] = version parsed_data["mailcow"]["num_domains"] = num_domains parsed_data["mailcow"]["num_mailboxes"] = num_mailboxes @@ -132,6 +156,10 @@ def parse_mailcow_info_section(string_table): parsed_data["mailcow"]["solr_enabled"] = solr_enabled parsed_data["mailcow"]["solr_size"] = solr_size parsed_data["mailcow"]["solr_documents"] = solr_documents + parsed_data["mailcow"]["git_version"] = git_version + parsed_data["mailcow"]["update_available"] = update_available + parsed_data["mailcow"]["check_version_enabled"] = check_version_enabled + #pprint(parsed_data) return parsed_data diff --git a/local/share/check_mk/agents/special/agent_mailcow b/local/share/check_mk/agents/special/agent_mailcow index c2718cc..c27eed9 100755 --- a/local/share/check_mk/agents/special/agent_mailcow +++ b/local/share/check_mk/agents/special/agent_mailcow @@ -24,6 +24,7 @@ OPTIONS: -H, --hostname Hostname (FQDN or IP) of Nextcloud server -k, --apikey API Key -P, --port Port + --check-version True|False If "True": Running version will be checked against official Github repo --no-https True|False If "True": Disable HTTPS, use HTTP (not recommended!) --no-cert-check True|False If "True": Disable TLS certificate check (not recommended!) -h, --help Show this help message and exit @@ -40,12 +41,13 @@ mc_api_base = "api/v1/get" opt_hostname = "" opt_apikey = "" opt_port = "" +opt_check_version = False opt_no_https = False opt_no_cert_check = False short_options = 'hH:k:P:' long_options = [ - 'hostname=', 'apikey=', 'port=', 'no-https=', 'no-cert-check=', 'help' + 'hostname=', 'apikey=', 'port=', 'check-version=', 'no-https=', 'no-cert-check=', 'help' ] domain_data = {} @@ -121,6 +123,7 @@ def getOptions() -> None: global opt_hostname global opt_apikey global opt_port + global opt_check_version global opt_no_https global opt_no_cert_check @@ -132,6 +135,11 @@ def getOptions() -> None: opt_apikey = arg elif opt in ['-P', '--port']: opt_port = arg + elif opt in ['--check-version']: + if arg == 'True': + opt_check_version = True + else: + opt_check_version = False elif opt in ['--no-https']: if arg == 'True': opt_no_https = True @@ -158,6 +166,7 @@ def showOptions() -> None: print(f"Hostname: {opt_hostname}") print(f"Username: {opt_apikey}") print(f"Port: {opt_port}") + print(f"Check Version: {opt_check_version}") print(f"No HTTPS: {opt_no_https}") print(f"No TLS Check: {opt_no_cert_check}") home_path = os.getenv("HOME") @@ -272,8 +281,60 @@ def getMailboxInfo(headers: str, verify: bool, base_url: str) -> tuple: sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n") sys.exit(1) +def getGitVersion() -> str: + url = "https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest" + git_version = "" + response = requests.get(url) + if (response.status_code == 200): + jsdata = response.text + data = json.loads(jsdata) # returns a dictionary + git_version = data["tag_name"] + #print(git_version) + else: + sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n") + sys.exit(1) + return git_version + +def compareVersions(mc_ver: str, git_ver: str) -> bool: + update_available = False + try: + mc_year, mc_month = mc_ver.split("-") + git_year, git_month = git_ver.split("-") + #print(mc_year, mc_month, git_year, git_month) + mc_year_int = int(mc_year) + git_year_int = int(git_year) + if git_year_int > mc_year_int: + update_available = True + else: + len_mc_month = len(mc_month) + len_git_month = len(git_month) + if len_mc_month == 2: + mc_month_int = int(mc_month) + elif len_mc_month == 3: + mc_month_int = int(mc_month[0:2]) + mc_month_ver = mc_month[-1] + else: + pass + if len_git_month == 2: + git_month_int = int(git_month) + elif len_git_month == 3: + git_month_int = int(git_month[0:2]) + git_month_ver = git_month[-1] + else: + pass + if git_month_int > mc_month_int: + update_available = True + elif len_mc_month == 2 and len_git_month == 3: + update_available = True + elif git_month_ver > mc_month_ver: + update_available = True + except: + pass + return update_available + #@debugLog -def getMailcowInfo(headers: str, verify: bool, base_url: str) -> str: +def getMailcowInfo(headers: str, verify: bool, base_url: str) -> dict: + info_data = {} url = f"{base_url}/status/version" response = requests.get(url, headers=headers, verify=verify) if (response.status_code == 200): @@ -282,7 +343,20 @@ def getMailcowInfo(headers: str, verify: bool, base_url: str) -> str: #pprint(data) # get Mailcow version mc_version = data["version"] - return mc_version + # if enabled, check this version against the official Mailcow repo on Github + if opt_check_version: + git_version = getGitVersion() + update_available = compareVersions(mc_version, git_version) + #update_available = compareVersions("2023-01a", "2023-02") + #print(update_available) + info_data["git_version"] = git_version + info_data["update_available"] = update_available + else: + info_data["git_version"] = "Version check disabled" + info_data["update_available"] = False + info_data["mc_version"] = mc_version + info_data["check_version_enabled"] = opt_check_version + return info_data else: sys.stderr.write(f"Request response code is {response.status_code} with URL {url}\n") sys.exit(1) @@ -346,11 +420,15 @@ def doCmkOutputMailboxes() -> None: print(f"{mb};{active};{created};{modified};{name};{num_messages};{percent_in_use};{quota};{quota_used};{last_imap_login};{last_pop3_login};{last_smtp_login}") #@debugLog -def doCmkOutputMailcow(version: str, num_domains: int, num_mailboxes: int, num_global_messages: int, solr_enabled: bool, solr_size: float, solr_documents: int) -> None: +def doCmkOutputMailcow(version: str, + num_domains: int, num_mailboxes: int, num_global_messages: int, + solr_enabled: bool, solr_size: float, solr_documents: int, + git_version: str, update_available: bool, check_version_enabled: bool + ) -> None: print("<<>>") # strip semicolons, if present, since we use it as delimiter version = version.replace(";", "") - print(f"{version};{num_domains};{num_mailboxes};{num_global_messages};{solr_enabled};{solr_size};{solr_documents}") + print(f"{version};{num_domains};{num_mailboxes};{num_global_messages};{solr_enabled};{solr_size};{solr_documents};{git_version};{update_available};{check_version_enabled}") ''' @@ -449,11 +527,19 @@ def main(): num_mailboxes, num_global_messages = getMailboxInfo(headers, verify, base_url) # get global Mailcow info solr_enabled, solr_size, solr_documents = getSolrInfo(headers, verify, base_url) - mailcow_version = getMailcowInfo(headers, verify, base_url) + mailcow_info = getMailcowInfo(headers, verify, base_url) + mailcow_version = mailcow_info["mc_version"] + github_version = mailcow_info["git_version"] + update_available = mailcow_info["update_available"] + check_version_enabled = mailcow_info["check_version_enabled"] # create agent output doCmkOutputDomains() doCmkOutputMailboxes() - doCmkOutputMailcow(mailcow_version, num_domains, num_mailboxes, num_global_messages, solr_enabled, solr_size, solr_documents) + doCmkOutputMailcow(mailcow_version, + num_domains, num_mailboxes, num_global_messages, + solr_enabled, solr_size, solr_documents, + github_version, update_available, check_version_enabled + ) if __name__ == "__main__": diff --git a/local/share/check_mk/checkman/mailcow_info b/local/share/check_mk/checkman/mailcow_info index 6f69dd8..08415b6 100644 --- a/local/share/check_mk/checkman/mailcow_info +++ b/local/share/check_mk/checkman/mailcow_info @@ -10,6 +10,7 @@ description: Got to configuration settings and create a new API key (read-only access is sufficient). Allow API access to at least your CheckMK server IP address. Shows several information about a Mailcow instance, e.g. number of domains/mailboxes/messages. + The check will raise WARN if an update is available (and if this check is enabled) The check will raise WARN/CRIT if the number of domains is above the configurable levels. The check will raise WARN/CRIT if the number of mailboxes is above the configurable levels. The check will raise WARN/CRIT if the number of messages is above the configurable levels. diff --git a/local/share/check_mk/checks/agent_mailcow b/local/share/check_mk/checks/agent_mailcow index 6218ba8..da216ae 100644 --- a/local/share/check_mk/checks/agent_mailcow +++ b/local/share/check_mk/checks/agent_mailcow @@ -3,6 +3,7 @@ def agent_mailcow_arguments(params, hostname, ipaddress): "--hostname", params["hostname"], "--apikey", passwordstore_get_cmdline("%s", params["apikey"]), "--port", params["port"], + "--check-version", params["check_version"], "--no-https", params["no_https"], "--no-cert-check", params["no_cert_check"], ipaddress, diff --git a/local/share/check_mk/web/plugins/wato/mailcow_info_rules.py b/local/share/check_mk/web/plugins/wato/mailcow_info_rules.py index 4674162..cb1fb52 100644 --- a/local/share/check_mk/web/plugins/wato/mailcow_info_rules.py +++ b/local/share/check_mk/web/plugins/wato/mailcow_info_rules.py @@ -22,10 +22,12 @@ def _parameter_spec_mailcow_info(): elements=[ Integer( title=_("Warning at"), + size=32, default_value=100, ), Integer( title=_("Critical at"), + size=32, default_value=200, ) ], @@ -35,10 +37,12 @@ def _parameter_spec_mailcow_info(): elements=[ Integer( title=_("Warning at"), + size=32, default_value=500, ), Integer( title=_("Critical at"), + size=32, default_value=1000, ) ], @@ -48,10 +52,12 @@ def _parameter_spec_mailcow_info(): elements=[ Integer( title=_("Warning at"), + size=32, default_value=100000, ), Integer( title=_("Critical at"), + size=32, default_value=250000, ) ], @@ -61,11 +67,13 @@ def _parameter_spec_mailcow_info(): elements=[ Float( title=_("Warning at"), + size=32, default_value=4096.0, unit="MBytes", ), Float( title=_("Critical at"), + size=32, default_value=8192.0, unit="MBytes", ) @@ -76,10 +84,12 @@ def _parameter_spec_mailcow_info(): elements=[ Integer( title=_("Warning at"), + size=32, default_value=20000, ), Integer( title=_("Critical at"), + size=32, default_value=40000, ) ], diff --git a/local/share/check_mk/web/plugins/wato/mailcow_mailboxes_rules.py b/local/share/check_mk/web/plugins/wato/mailcow_mailboxes_rules.py index 5b36a5f..85c5f6d 100644 --- a/local/share/check_mk/web/plugins/wato/mailcow_mailboxes_rules.py +++ b/local/share/check_mk/web/plugins/wato/mailcow_mailboxes_rules.py @@ -40,10 +40,12 @@ def _parameter_spec_mailcow_mailboxes(): elements=[ Integer( title=_("Warning at"), + size=32, default_value=1000, ), Integer( title=_("Critical at"), + size=32, default_value=2500, ) ], diff --git a/local/share/check_mk/web/plugins/wato/mailcow_params.py b/local/share/check_mk/web/plugins/wato/mailcow_params.py index 47856e6..6e7052c 100644 --- a/local/share/check_mk/web/plugins/wato/mailcow_params.py +++ b/local/share/check_mk/web/plugins/wato/mailcow_params.py @@ -43,6 +43,8 @@ def _valuespec_special_agent_mailcow(): ("port", TextAscii(title=_("Port"), allow_empty=True, help=_("Specify port if not listening to HTTPS/HTTP, optional"))), + ("check_version", Checkbox(title=_("Check version"), + help=_("Checks the running version against the public Github repo"))), ("no_https", Checkbox(title=_("Disable HTTPS"), help=_("Activate to disable TLS encryption (not recommended), optional"))), ("no_cert_check", Checkbox(title=_("Disable certificate validation"), diff --git a/mkp/Mailcow-1.2.0.mkp b/mkp/Mailcow-1.2.0.mkp new file mode 100644 index 0000000000000000000000000000000000000000..b5fbcc2aa3c09fca0a89068327f3702b7c16f421 GIT binary patch literal 14374 zcmajFQ;;QG)McB_O53WmZB*K}ZQHh0m9}l;q;1=_?Q_3B?o)R}cR%ci`MhGU*s;f$ zL@~cWj$50|z<}3&b{?A?3zhfM7GR@QM zVi&{}TsupzZM!&K;1JZ~@j0dB8)z{{hD;z(eTL>tzX})MvUueQBmp6y-*Dp}o7c|~ zgASb``;JT(ub&~rUAqKs;k$`5oUe(gRwv|&N1OrEPgavU=BG}L>=Zf<-FL}d4}(mS zQYJjTmU%-;zNo-OnT7Lrv4gzgYc6gZ`xa|co0GCdGEGwmZ$)_%X^5_x&e)z6c|0;cUza?l+W|D*nhHbA3lkuOiNwZ~WuN2h)KY@N2Et%(N0#_l`rHx6R9!YL+*&(k+Rgah>ZqG!^!IlNBw-sb$7h~NB^b4PI= zNNapAEN~nJVeO==ZgYkdp-mym3E1jmQ0lwCrnJ=Sc`x0_bzf#jLfHitY>7o= z5qZsRB}b0+ivGBr;I@wIV5!XC$7W!0c^j^TQImwdqzk#o23hrwenl=J(3FC3p3|pv%e+MmAwH1Cj$%o^+Fpso3hHwy}W~`SYQj_@`1hCsL#k`k$;0lg|1@6tPeK zhwlu4o`l_l2?*JORVXUmLu4xtG>fF~0}(Q`*_^ zd5i4Nl$;C)1t-wWnmx0 zbsgZV+2ieF7_L7xXt&A4{S^=fcui6+-x&|wY*ebLtTmd0hC`S$iHu6f(+KRu22Cmb zAuuK!NQEHgx`s(eGt&JZ>FCpyvxejmBo`4H_3BdrSYH~AJKQN7j)UTY&4i#hHn8i)koK(;PrI&twx`_Zd5JiLe)J2y7Z_q;iWJY~b z;+v@&!m9oyv_TxDzG^|d512ylXE-zCvv-g-2+)+8kg-2)Z+B*(|CX)xB3Sc(4~c>K zq)pPfQbX0F!%2DMc6okaipz!?F*H(wehqh zeuKM4+U_fWCz_rY);b~!3cC^Cn}vd1nr4KS!*kCzit+5MbHlOh+GK4c<(z*RXPv9L zzcqJHkKznN7Ut%G{Zr6st{;cFsGI^z;*5S#M}{CD8XLF}%=nE4+x#o7yna1snIER~P6NF!7fwga zT=#9?0 zrfI>?zb@rq)aVc6C%zHFBJQ`XSg*6nl7+y1md+7F`itAM-m;{MY3Mr&=Rgj@(bu$UN2}&_n!trf&h5;2!ZU4GduX z0P+y@JU1xYCnN0g*#31yoZGukF-OQZ;Anx`5`V6{e0L~X!ovL`tsD?I;Q1<&v^wQf zA9kqr5Qw%fS4z8hEV8s4e!Fhov#IJzkZv}YxU_ChD3o(um6)$-UQmYgQ}J_YaPE*y z(%q4in8%Gi+_Z|)quB@hzEx_DH*eF8%UWGerLr_Tyh}~|Si`6uW^0ttC9k@iM4qQ_ zu9089fbw3-qLE9W!#GEw$*?a&-d6ndqE#!?sq<1TT`Oj%Q#RQVx9EruesJFylT%BO zP=2+f#ZB1C6t~l4b5c#ULUE{OG5ruKf#-9!26y61x%`P&eD_FR{OM5hli=3dMWOkO zX7`Z@w@#2pYg0U0*9H`+Sp!H_94J#+cp2_?Z&WTw_>dtJ>_upiOT5OUrbf~daOQI6KzAu zBtd9m5!;xb`KyxRUVOd~Z)=8ISk%eZ_gGPTzOeW1x4lq8?5J;4+PmCh{A^N9r1wO{ zQVYw=;%0;{P1WZd#v|>DSyo@)_#meHLHPu?dWa9BxB6FPJxolJt7MdiX3h?0ubha- zxvEimulS@AVnkgvCGg}~`T6kjQaZ-vu{bW4gGTtq0LY5F=O6-HypDDEp#7fpBeS9C zy8O9~hXP#P)>D23czC1$9W2%&Ue8dsI%a<;kFw|hbYVKr-?VZ_J=yoO5)^(dnEV<;KM55ukt# z;)g^+3P~z(wOD6jbs}kKk|J8&{*u(jxO2RRVgdT)kg(fRd%V;0K3w=}=If>64eY_` z2p+ zl?*_Zj(-FPXJVw@eUIVQYv0u9?eWUQ70FtaY_zlGZnds$_PqmIDPzv@18)JH?Bp;! z6i}2p$CivEFl86t+$oWT#Zal~oJjATE03FnJKQin4SyY0 z>pW;~HivxYAREENEaO0KQ`wxMk_J_cgQ?*?lfE$S!`FH$kiHvhSa5w-A!Yi+I=gaL z+nf<_N2525n|@7b*WuZ(77ZC3vs@`IzpM*GUzwTRleTS^471fWx2POz zrXlEGpIMGwbWoZ)>?gnVU}FTxrxQd_R3&Mmz0Nj$Xn~gWp}^tQi(NSOy&uZLs!ya&0`5k_C4sW(>c;g%wbe$PB3|p$`Y-+yg^m^3VNX1msFV3wn_Zt zIdSVUh!%3xk*ktF?ae8^8!7#c$`gb)RWYTl-+aXOT|U7D z7oI`G7w6AX^bK_RZL?;e0;tDc1sQb5x1%{>TVi5WB*Z-=V95{}xgg{rd><(h!m|=E z17PiZe118|G%xT2v+%sP*2coKeRepVxn7Vyru1Cg@;YLysnF$2gqTiqsAlFaMA!Yq zkXBqCN$KBohPlL&imPC>`aiD}T#BHQt{T;oIto_p*jkEf>|2dne+JnI@I>u13d=!j zf;@`Mn^&*cahrUf0`@W4Az?p?k3`O83}^^!MYE>lmL=+E<^4qb*!DNc1w$DS zli=ZwdCH+>$1VK}4XAS0A&zj6B7ex>{$7xQZ97ftX4l~`IG-0)fqG{vrgd{YNMVcG z;_oAEby`7o-`4x(ZU9IVJ#B8H|4M$e_-s{owDzLrmw_I6bf8wx_E+0oN#RGp8}@+` zTB6Oe<-$QTmXZ!-SmHox!Tp4B)98rL$?U65ubs)~FBCctnq1;}3mF2=`mSA3ptOpt zf{cZrs7}J5D%LkI7MWfe!y#AqpHW;2!=3Y%0^XGtk#Kcf-?J*>H*#?CpzZnURz{l$ z2P`-REsbX?kjRj(9w?YGB%c-|TPj6bLu-MVQRpwigXLr0fNQyetv5q;Ci`xCGP8WQ={3uLY4N{G% z6sy2+Pq9pD792U;&|+VD)T>~cu-meu*c0gycD8ogwnWf7k`~QGgI;4+7u0Ji()uIK z`O#^k_jcmZpnki*GNK2%mZ`dO)kx9X$tNay9sWgxdI*9>9Tt68ys?1;4A!zz#MzfE zl61?Ltvd@>?nBj!goeS3^}W0qpYb+_8ja>C#K8sMXnDf} z0}Cw{qIbx)o_!Cl>P`qYfGL}c-KfLmjR zIYGvIT+rnG3#Dy;pA5@O`FqREFW3%RN24q#FTrAR#K63e^BS1k=Laz&=e2_(xF^w$ zIqn5fUlKBq9||YPZgwm&310L2wdkaSDwMOt{&b>zBoRNx4x80bHj$Y?(D#u!!J_|M z4Mi~l+>3}()`&GqA*WR4Zvmr6pbZETHk0EKIy6`=G))*$XZb$#2L2v^gMK)l6q&neghN{F#x z{c`nR-dy6m_TnuX_!`s7JM%7#<*jCDln#HOw~gtYFVWazRTtxi4}M^fZCGzC`hHN6 zqUYYZ?<@lKvML>dx7XcL`%Nn2(<-7g9S=^5^xyLxm>!-n@_nDO;KW_x?yEm7+8d5W z>X)f|OT9N^;UsFrHr--y&9B7%BD4n|KjWGq^{Frt8GJC`Zn|}7-rMi&;}247*QBnq z^JIb7EiU6X-;C6&bGocyx z-T0-?K2mW#Uvolry$NB56N9AX16&@Q;FH~oJGDr5WwV=%$?;-lgSpN4xOTR9vD}NB zq-`tQKB_vftCx%aTIS45FfbTco1}Xk`2bm-X=M85o;w*t8|Khb!&X^=PIYtF=fyTreBV+xrAYePTLNhNn=hEYF_PAZn{;0TGCK?I@y7R z0$(rO!Yw}_p|+!1&LGSz@>{kd>(YIy@$i4Sgx zw+qZQvK>`+x#e%{AVW8;a4^}_7e!h*D)yzWWv6ECjGL-r+4)pyf}w5kau%G+g+#X; zc9<50i9+_zGpp~U%#gYY=BGbdzjQjr3sUfWOZq9|RQtvuWnB89Uc*bMov48m9fR6{ zY~i4v5o}egwqL8PHAl&AaJ&pGx-JT}YAZI4x4$g*n2cC68Z|Ws`0*Sx@J6i2U74ondkQDl_N$w=q4Biu0rR6PLleve z?pst+K`7KF7tZ;NLw)iVfT!7KRyu!zX`hjXcdpdW1aq=uENrqGy={;9RRGl!iS>=K zH^8N^zv$Hp`v>=uldT<2eRJ-hRy6Cgtz&oO$UfS%jj6NF#6_Y~^m72w>oSSoy0(qK ziTlSO1*eB)5N`z%?+?_Xd4Ed*$ZE2iTvyW@>oTN?dsdhh+RiB*yhzA@sp(h_4uJMp zmKl080MeOpt}<^7<@bhfK09l3=c=iWes9oW7p76nQmZA6R_q%x8p1R>8A(HJ#Q#y3Hw5Kt zU$*4r&#*b+D(!h0)Yq32p-l#A&4ka64DMn~)oYUq=j==#oX$BJ<+f3;*-?;ZSAy$e zxODx-LPEj2Tc8vU)jinFcn9c=4Sgu_%Hk3t5W=5U{s)C!yKOME=%Pj-(?!F=rxmPJ z05kE=3ckITx&DdjWNt${8)++L=63bAYN7JTt$ZVYfk&i+ zHb;YJOQS?>{8F(+yu`uOaT%hCx4Heore5cR<8vpIZbEUQ#|F_m%3Yz%4mOHn6&aHS3-PP2pUPMj_+_2zXkQoR8Jm8~Zo)CjF&>(Z#0S^%sM5gN$eg~Z2L8EM28tzrDBapWdfpJ7fq61iaqtNiZUmChU`k6v-QYvAPuWbJC-Zs4$ue6fCKHRE;2Mm7d-o z*PZb!SpK^v`t_m7n#X4OJKyPqQtFchhNV}jZZ$6-dc4XYw26k>lr)}i1Vy9j+xzWp zUBSei&JgvTsL2YO(ZckT+be*u497bgKj|9oVMhnTv%mb#ZrqHkZg~g4%jf>&nRub` zyGyLK4H9az=)A*o_wC(4!2s=v z-}rVC5D8$5ABiN*U%*O>#Sad`L-E20+z zIVgMGneVZf=_#YzW}9sPYKtLPWtUwokWS!4&_D5)3P^$fsYeUIojpCf+;In0^~RxP43jkRM|9&1$z*AcGq`s+#8~D7YTdNT9sUs`&c1*E^;mAh}#TVv^k5Ysj1meF?<~g8PAox6?Aph(CgBqh|v`FG2W@|gH9&W9Q*3P z(!iCK^#1u0v+5IUys7C5tJ??L7w&M*C5dL7sWE3=KZ(*D4!tixX}<~6$-kOfN(Dc zaS-8<79}xz{f!_gt8?TK0B9dFm~J>}xC(eGo0PSYaer@48;H)b!A{O{++)(Pd3?eq zTsY)%hNRKBu}qzvi5JC=n?dYu*t!S}CXzejjJ-r=mz{e1+y6N0K9BTf1Tn#FJEHP@ za7t&$uDgbL=F5?_ ze$UF5J0RMNxk>Lrg9m=v%$y0h==S!SeZPHP9%u7~s}~6P7a%-+>WugUNBG$Y$9N4D zwO~_)W{v!nTY7&Wl7^lsrFnqUI{yTs2p0*p+U~HDo!%Vq5fL!Q4m<7DYdK2~i3R5F z@Uq%XSr5)@kKk8{=tlWC`uflFZeipdcTHZ2DcPq{KdXlhh~t^e$2|{(?)Q*hw4e$1 zLPltWF0gg+iX4jL%zyYL^g+H!Er;2!e3Xe+xwiwQ%d(>Lj2lKfNPhX{bZsRA>DBw_ zcw}nvHIff>@fi&j*QvDYzx)jxP#MX~Zg?3KI#l6fL+D)NfG=mg-oI@l*%m$?N3$r_TDxYEQ!3ym^OYDQ zrKR1qgXa#$xH?60aO4bZb`(pN4cqYh(dgxo0aF_HrDwQU;j z^6`R77C219atw-ozNV}4xc>aEf&n?BfeDBc#y4@Uj^Yvl`AstT9Df%FB>UB#JJ;DJ zWL|ywBm-Xw<&dsv>QMO>Hr&Z((Q84H^zGjNf>hfV&oRw?FuK^VTi~uY}x|BK>1Cakz0;NM1@a#J3Vb)7AHS+t!*_WLoC#rZc*d zY(4=9R4~y8pNA9~%A5`phaD%EM8rn5HN%2s&`A5u^cu_&AIR|IscZ}9!J-pyHBVeO zIY~02ALW^al1 z(Y_j^H92QcJl6kHQo~uJq${`{%77`RlEptW^pxs1g$~;yE^XS*_0YontHynZ(-~5! zfSwRe2s|Vfgn_wkuLS-8&kLs*M_&{HHSj|oD^>ftUtrjpvPB(Ig$^+O(EOuYi)&8q zMG9MQpdv8ew`OVtUN8~?zNcy!P-@Nm;K1?0mCa0Y4PR$cTD)IxY|GWBcqu$jt2mqo z_Ty3=+-91Bur_4)w@}|21u(5sT)upkMu}Zo++Ko>r==VE4@!-CkDx%X9r{5*x+E5} zGBkLxin+T5SCokHnR1d$mE_kGausTqnH+^-tw(yUF*KPSF-0-lo@!oh>v5$n6#%r4 zejsN1G(c?p^C}^m92ZOq@35Qh9yJgLAtnebvU22GfhOVE`ECkW{-XMyNN5&{zBjiD zF|$+_%8Ftwugur57{xwKuy5`nn*+Op5{D0+bTQ2g{K~nJr)Hrg1MAgIC4@(Gc7n6? zM8I55Wq4tufv+L4y7Q#@EmEkDr54BJpiNnjAs;yj(*?iL>2{q&o;|@u55?!B64Bfd z1&+Q#2fbH~`^U;Y)PcHM#5+|UyIrp7OIaD>&%jJw3oSz^fvZPqp&!jP8OCH?HRsy; z>4Sg0k@IjR2qZpC&iNkW?#Ecv)kN-}ShS6YdiFfH_U=9u4#D{22J91&T1J8jn?%N9FLcWn;YdNR*Q6){ zxu0TGkl4&r4C<J5^ZblmSFTmDjYE`&kgazL;g~UtjDT zR5qL$F^>c?hlDAcJ=mhoj&7)^FM1tQj76G{Hd6akNExPc>kX+f zmL_R^2X7pYfg}$)gqg$C@8HqnHqSj#kN?2{NX|jr60`ee+o2T;xB&iAE&isDs|i~O zo=FS!JePs@w6u^jWWvSy>kn)3j(H-2xs^VgI(YD5=z;W6KbkiN;iqzF4Lgh!a`cUC zrdF*os)P{`lPr_y2g`4M1_V@hyQtHN-h=M+W`afq;;pY@**j|O=bl~8&Nf*^WgV?- z=k|I7mb0*5%gz{IX2&!PvVY)MYNc$`TNx;RFIUXibTx%wDQ7E5G`z6o-sU%H(j>bb z+`iD|>Ic3(8U#Rr6eJYBA&%I~bZj6ZTdIeANK;?JNh!r|XGw9Jb$OrUFxw?yhNhh7 zvYnk^U7N*0vuD2fVcq-glk?wxH#6rQcrZx0Sd#M;NlxRKFc-a+L|b-;uMaT!f{)Pd zg4^Di5K5>LO-Wmo#UN8vJJ`6I{DsF4fbo=%qy=}l(xs-ysw|aThWKNywI%XHp%4v4 zs*lrvgV0XoI#pVJWyvxqnaqh?fE2o7O%)t7YL2Mrz!>VPs_jyvXle9zG_8Elw9b#{ zy2N8fdgTy$=V!6SYma#v7;iy0nBN8%wOL!96WUO7s7cAjNZYN^XRPWBYU(AP3ErsU ztNwufIeF$hf9pR>;tN;1(q8-gLlIRs&t!vT)H;F$RL9AiZH)38xuex8T3HkChYpX(@|3ZIV^5*c6Udq3AAy0| ztd{m(E;Wdl>Vf+lZwA;?4<#~KHi*D<)~;hw<4g^?R#tLwnL_Or*!ES!uWb@;VqtYx z1kq>xven%i*ZUd#AArRzgO0DIxtvOPrE-ES+69(y_8`c${mwC1 zaQApyt}!R^`{ho8c$gHmw!x+2bj2_?TG^I7es32Zfx?-}&si*z@rM?^isuNO z&fnZvLI6c_#Qb;xfUcfFdyBf&qQ~&=+UWzzM>vj*u79gOJm2pZ0l?2)mxhVaie5{2 zB94H!{fJIy!Q4DcD8kOvswidccKEpCucNo_Iai|9hmk10dPQ%dRs5AGzV@X6qE$LP zb;1>O!+D=N%)aGEL)?g^dt6N~P!}p$2>O}hv?tWxsHR-Rv-xEwpt(HFGJ^tk$kiea z7n~Ofis;KHUXwq^zv%-`GZ#buyH*v42`-$d$4yLrU69nV%8+{>W|{UTviwCg5dsVy zMy0>oO8@>BJpLzSt!^X(0{t`dbOlg3nfVVmDQEW545{Cw2q=jB7JQ%yrzs+^n!|<+ zHk9uSm-V}p*ijwOQ&tfbq-}l=QrSgz<0PpdP#pRrm@M@)M><`1yu{O9-e5Ls~%9x|x zjKyksa{gLD(d)~rIf-cVPKuGRwY%48W$l{Vz|6gJ7n65qr#7p>KrKDddBlK;qe^hn zQsii+|2^>vFo!x`lN|2(+Wh0>lO?m|d-hiD1wx~RM9xOeagqd~YMdnEeV(N6z=t0j zQ6#ZOn9xap3=Tzrj6%3yXKw)^tmIM0dA9Vx@@s68|M|mOZfNpUk06;f4b>#@3_t} z=`Y#JaNZ<^B0&!%E|T=A30fUX|DYLCv>)~mD@vBAxf1f{8k7dl?qFv~8~@-vqIZ0I z!AUuL@)E8}6L>$$<+*q)m9y} zHLE5%KeBnHGk>+4bcB+Xl`J8g;v0-Sc{Z<@IB}CDYd=)2xs=SX)8<$f-T5WUgV#U77uD{9Ak3?n4x$9hC+24wM0HokJEWFN zi<(_F>Sb1>9|B+2!_Bm4LLd)h(}~iq7b&RTUOTqRj4Le2Q*8WV!;;D#{(TGMfT$wI zVBCJ?HenCj>{M(|79!MpQ@#`jvlWRfAL`_bBcrYb;cs1%d>tj4)^w2M>5n>>F3r}5 zvaGjom8hoA4ba`A4On4t8*xNU)#7sTB`IV5 zjA_#h-CA^pUOsU{dji@Io^AgO_x=kT?g z7tJ$}KDJ2|m&mT`vmNsJf|D}&qwH@nxt8dB)p&)Jd9d~jqo$;O*wLa?!@f3E<4?1XEV5qwETO-wnr~`+ zL7r%DREzvK>Fgg{h9YRdz~L5Ce_J%D?*9uP{x5v7j#CLbvjc1U7-8=CRp3?kzatJ< z{+@mP>zdHya~3fAJ^NodmJ9h+Ai>?E0SGAC$?pWd^DASTdYUHre8-LVc9TV>Rrt@F z$8DnsP#7W83#^ptpM>47r_&WBcbcyb7+y$F8+y6lHFCh+`JRlk6j6$11nhhjay%|t zp(-+NJH>T9s<@(+67H(E7bn-dSde1oz4CF1q#Z-$o;^k z9@)2&zi`3UVgCvbYA2zoEjsCf?Ix1@MtA;J=psAafp#*xi7*tG83IYncsdZb7|6kb zK3kKMLWx|n8&5W7M2(*beA1G2*v%T5w{mQiU8@*hn!Quz!Sq*YcltNhn@qTX;K{vN ziXoVH!6r6(=PEV$$B50e?+ZJ@V*wr?Ec)+}^2X5cVm3#9W&}#mmS-N5;Sg)u6RfaX zRiy_iBkS)``$Ek7O$)ngJ~9u`?Y}#DyaR`5fIVGZciHa=G{(T6&+Gfm)h7-bl+sv2 zwUnMF1l#z%A7#y<4xxozs+me<3}FmLMOESe-7ca*>!UbvcqVHEL_03CT7NQ*np z7{Tu};x+*+`=>f`Hn3TOM47*|uQ$u&5cAN1u~xBie71pKFs#2$(4m>}T-RV<{;T>l zduV%I?CgKcLQi680Ri6(dO$$PeEsJ2;K%OeZrztg4bb>Ipv3o`=rJb9QJX*XJ{oIg z@$i;XV;eSW2^`Xizqgu8`S#I~#bV0} z2}&Sb#$ywm{qay8L&#P;vKPv#1;aoOsSUe#S0t`D@ujNwhgTds9$@aZ^psDhrP*KY zviIoj(*Ot<{Q3~Mc&&T}%2Wf{@Il2X(f$d>e97pi7TuKkk&_&&+5inauk+wA8&dr` zR>tsBd!DrYLzWXX><^C^mpu)Z)|iFod1<4>1qD9Zk(rEWXphlk_S98J6$v;YZ~t zd2m@ZKo=!E`dvx3F!%qcVUiA*3hhwsn4=`BaiEZQazTslo0Vj)t0p^W=&yrcEIQSs zGWd2uyAF8&6Gz%U`0fb%8w2Ovu3i)He{lo<%X0t!<5*bDX82A5xcz*YrvOju5C5Bi zN9TXZuUM>|uSn_Ziw>rgE|Vw@dLVZ*rfPNr$N-%64`6FP2)KIT`Tn0#Erd9Dq*^t) zpo;SPKLyd%Ha>D)tjZ^O%9lRIysWcWLT`6RLb$l*lA%4KS>$n8V1X_hzS=CNEfM_n zqRsuEN|OMm4f{23cdQ;jvnyUdCeN~z2SG`hC`qcQ7A*0uYWi~Px~V5N7~b$=(G$na z?Z+86!YSARI)ng8Umq};+~A;|N>fx+V{jwo)UhIVUL}JSXoI=qqOlSC#@SD$i-zl6 z6bQ1i#0NiB2a-bz1n91J_iLNgS$pX>+rCUkQd#@O{37(I%7#FJcTfRau?};F@)TbW z-AYGVxq`!pvK5P=63#8(GwbrZi7kE}Ef-%Z9cF^HXactH19W zCZtb^E!3JnCIS!U6S;f+PrjCgDq!%RDc5=mdoiRy%3fB30r`*2yTd+Z8S_(Eo+OH5 zcV`K9JIjwvoymAW(Y~YRct8O9d!68z%9oIYLydX2rB6&4(KN2{n?Akc(@H^i85R@K zgHVMNj;~r-BtMRXfvG(o=n=WwqoM4d=h?M4X!usL^@$j-8etgO{cdxC{b{$y1eF%l z_2~(TB7fo1{|YBJzU0f!jP;xqYas)ff2hhgz%8~3%P0O&)oq8Qz*j^u;l8m0<8FV- zlX=6XP`;O=Kiq18Y5JJAXKDSi|IE&IV}2|=*b+Z_Mmqcuq%X65BPuqWvDL4z3^rIUcT%*e!b6v3uFw)LbKxf_@kKpWaQOH zv(NY5!&}YV0SSdW^A1kG5qhAr8kWx9uTy4*j6Co6LE&Z{8%I3Wgo{e=jFw za=3_yZxnZWOp5z#M|97s?QXhcFFf0`2oT`5#eZ{~SM1~&|HJh?;f@Xflj3U&vuq+N zJfQU;sr1uS&12nim1V-6_t(QZXAdv?{dsw#tlHC&CO1o2ZnIuDYbcXnpy~tD5wXs= zX;_|J4K3o}&+=GKa70cZ@pJ>~W(PU9i*C2lHUE<*M*2bL17v>zhRoDP1D7x3diwE< zKGeSI7`uUt>LAT=o+?&8|2ZosQ~RMf;>V@QE;juE|Igo1vxyysOT{&AAbEn&!;dY3 zc|PM;3o>6VfxGy%km0YxeWAM(=R~nDn&v{5+`V0q=KT?dtxp4-Fgv-YvxuIeP0D_f z_xPo%)Hc?-xuaGT{F!ccehhZ7B@hmQMx7WylNYWWDA3h9>}S7gi#tarkTH6!V)DE8wfoTgL)NB76ICJl;LRZt<8m z@~vVbsL#5-Zq*%Om(>v(@3LR-B!CUzI-H6CQtM|UXUjOwU{CUA!ekfQ%mr z@nssJi11zNR5Oy1&d?Z;P3e69BSRgbfp&k>cTA4M7)YiygXy__(HoxP+UqC8TJod7 zN}n)`OmUQiJsA#i3V0H?Wm+_I1098P`?u=(f(n`dNTj8t>56Ss!vHjT|Ev6qZSr3U*Q6dccsbP)zgEGj{ zkHbHCmO2}%64rA7%O?C;)(_vkkT(t*&W{>pF8h^$ocO5 zquD{WyjgsYY%ogUdP=Q+Zx)kL~^;X5Ye+=QS65q|7XF$U?=)V z^uPZ|ZmFDN|J$Cat`m59<$s}fN7;nQ3*R3=hZZw9{`aMq=BdlNCvLG-sa3~$f@mj8!1A0v zi+PY59$+tkSMg_;^a)7#47Du{R5=gm!TnCaXU4M(9KzXuRudE}*%F5-9{Gq{66EtK zf%>@M42njv??xaKBEvqMJrwvVyCeLOHj}gKeHbp5lNy{pl9PCGBb(V0uWIa3|G!H9 U|Lq}{k^vI6$A0#L?16&(e|?Zt{Qv*} literal 0 HcmV?d00001