/[smecontribs]/rpms/smeserver-zabbix-server/contribs10/smeserver-zabbix-server-0.1-bz10802-checkcert-telegram.patch
ViewVC logotype

Contents of /rpms/smeserver-zabbix-server/contribs10/smeserver-zabbix-server-0.1-bz10802-checkcert-telegram.patch

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.1 - (show annotations) (download)
Tue Nov 9 04:28:17 2021 UTC (3 years ago) by jpp
Branch: MAIN
CVS Tags: smeserver-zabbix-server-0_1-31_el7_sme, smeserver-zabbix-server-0_1-29_el7_sme, smeserver-zabbix-server-0_1-30_el7_sme, smeserver-zabbix-server-0_1-28_el7_sme, HEAD
* Mon Nov 08 2021 Jean-Philippe Pialasse <tests@pialasse.com> 0.1-28.sme
- add check cert scripts and telegram [SME: 10802]
  no template provided to use with

1 diff -Nur --no-dereference smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/cert_expire.pl smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/cert_expire.pl
2 --- smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/cert_expire.pl 1969-12-31 19:00:00.000000000 -0500
3 +++ smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/cert_expire.pl 2021-11-08 22:00:50.335000000 -0500
4 @@ -0,0 +1,139 @@
5 +#!/usr/bin/perl -w
6 +# Check peer certificate validity for Zabbix
7 +# Require perl module : IO::Socket, Net::SSLeay, Date::Parse
8 +# Require unix programs : openssl, echo, sendmail
9 +#
10 +# Based on sslexpire from Emmanuel Lacour <elacour@home-dn.net>
11 +#
12 +# This file is free software; you can redistribute it and/or modify it
13 +# under the terms of the GNU General Public License as published by the
14 +# Free Software Foundation; either version 2, or (at your option) any
15 +# later version.
16 +#
17 +# This file is distributed in the hope that it will be
18 +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty
19 +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 +# General Public License for more details.
21 +#
22 +# You should have received a copy of the GNU General Public License
23 +# along with this file; see the file COPYING. If not, write to the Free
24 +# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
25 +# 02110-1301, USA.
26 +#
27 +
28 +
29 +use strict;
30 +use IO::Socket;
31 +use Net::SSLeay;
32 +use Getopt::Long;
33 +use Date::Parse;
34 +
35 +Net::SSLeay::SSLeay_add_ssl_algorithms();
36 +Net::SSLeay::randomize();
37 +
38 +# Default values
39 +my $opensslpath = "/usr/bin/openssl";
40 +my $host = '127.0.0.1';
41 +my $port = '443';
42 +
43 +my %opts;
44 +GetOptions (\%opts,
45 + 'host|h=s',
46 + 'port|p=s',
47 + 'help',
48 +);
49 +
50 +if ($opts{'host'}) {
51 + $host = $opts{'host'};
52 +}
53 +if ($opts{'port'}){
54 + $port = $opts{'port'};
55 +}
56 +
57 +if ($opts{'help'}) {
58 + &usage;
59 +}
60 +
61 +# Print program usage
62 +sub usage {
63 + print "Usage: sslexpire [OPTION]...
64 +-h, --host=HOST check this host
65 +-p, --port=TCPPORT check this port on the previous host
66 + --help print this help, then exit
67 +";
68 + exit;
69 +}
70 +
71 +# This will return the expiration date
72 +sub getExpire {
73 +
74 + my ($l_host,$l_port) = @_;
75 + my ($l_expdate,$l_comment);
76 +
77 + # Connect to $l_host:$l_port
78 + my $socket = IO::Socket::INET->new(
79 + Proto => "tcp",
80 + PeerAddr => $l_host,
81 + PeerPort => $l_port
82 + );
83 + # If we connected successfully
84 + if ($socket) {
85 + # Intiate ssl
86 + my $l_ctx = Net::SSLeay::CTX_new();
87 + my $l_ssl = Net::SSLeay::new($l_ctx);
88 +
89 + Net::SSLeay::set_fd($l_ssl, fileno($socket));
90 + my $res = Net::SSLeay::connect($l_ssl);
91 +
92 + # Get peer certificate
93 + my $l_x509 = Net::SSLeay::get_peer_certificate($l_ssl);
94 + if ($l_x509) {
95 + my $l_string = Net::SSLeay::PEM_get_string_X509($l_x509);
96 + # Get the expiration date, using openssl
97 + $l_expdate = `echo "$l_string" | $opensslpath x509 -enddate -noout 2>&1`;
98 + $l_expdate =~ s/.*=//;
99 + chomp($l_expdate);
100 + }
101 + else {
102 + $l_expdate = 1;
103 + }
104 +
105 + # Close and cleanup
106 + Net::SSLeay::free($l_ssl);
107 + Net::SSLeay::CTX_free($l_ctx);
108 + close $socket;
109 + }
110 + else {
111 + $l_expdate = 1;
112 + }
113 + return $l_expdate;
114 +}
115 +
116 +
117 +# Print remaining days before expiration
118 +sub report {
119 + # Convert date into epoch using date command
120 + my ($l_expdate) = @_;
121 +
122 + if ($l_expdate ne "1") {
123 + # The current date
124 + my $l_today = time;
125 + my $l_epochdate = str2time($l_expdate);
126 +
127 + # Calculate diff between expiration date and today
128 + my $l_diff = ($l_epochdate - $l_today)/(3600*24);
129 +
130 + # Report if needed
131 + printf "%.0f\n", $l_diff;
132 + }
133 + else {
134 + print "Unable to read certificate!\n";
135 + exit (1);
136 + }
137 +}
138 +
139 +# Get expiration date
140 +my $expdate = getExpire($host,$port);
141 +
142 +# Report
143 +report("$expdate");
144 diff -Nur --no-dereference smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/check_cert.pl smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/check_cert.pl
145 --- smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/check_cert.pl 1969-12-31 19:00:00.000000000 -0500
146 +++ smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/check_cert.pl 2021-11-08 22:00:50.719000000 -0500
147 @@ -0,0 +1,109 @@
148 +#!/usr/bin/perl
149 +
150 +=head1 SYNOPSIS
151 +
152 + check_ssl_certificate.pl
153 + --url,-u URL
154 + --sni,-s HOSTNAME SNI servername (SSL vhost) that will be requested during SSL handshake.
155 + This tells the server which certificate to return.
156 + Default to the host passed with --url
157 +
158 +=cut
159 +
160 +use strict;
161 +use warnings;
162 +use IO::Socket::SSL;
163 +use LWP::UserAgent;
164 +use URI::URL;
165 +use DateTime::Format::ISO8601;
166 +use Getopt::Long qw/:config auto_help/;
167 +use Pod::Usage;
168 +use JSON qw(to_json);
169 +
170 +use constant TIMEOUT => 10;
171 +
172 +my ($url, $sni, $status, @san);
173 +
174 +sub ssl_opts {
175 + my ($sni, $expiration_date_ref, $status_ref, $san_ref) = @_;
176 + return (
177 + 'verify_hostname' => 0,
178 + 'SSL_ca_file' => '/etc/pki/tls/certs/ca-bundle.crt',
179 + 'SSL_hostname' => $sni,
180 + 'SSL_verifycn_name' => $sni,
181 + 'SSL_verify_scheme' => 'http',
182 + 'SSL_verify_callback' => sub {
183 + my (undef, $ctx_store) = @_;
184 + # Get the error message from openssl verification
185 + $$status_ref = Net::SSLeay::X509_verify_cert_error_string(Net::SSLeay::X509_STORE_CTX_get_error($ctx_store));
186 + # Get the raw cert, to extract the expiration
187 + my $cert = Net::SSLeay::X509_STORE_CTX_get_current_cert($ctx_store);
188 + $$expiration_date_ref = Net::SSLeay::P_ASN1_TIME_get_isotime(Net::SSLeay::X509_get_notAfter($cert));
189 + # Get Alt names so we can check later if the hostname match
190 + @$san_ref = Net::SSLeay::X509_get_subjectAltNames($cert);
191 + # Keep only odd elements. Even ones contains subject types which we're not interested in
192 + @$san_ref = @$san_ref[grep $_ % 2, 0..scalar(@$san_ref)];
193 + # Always return success
194 + return 1;
195 + }
196 + )
197 +}
198 +
199 +sub https_get {
200 + my ($url, $sni, $expiration_date_ref, $status_ref, $san_ref) = @_;
201 +
202 + my $ua = LWP::UserAgent->new();
203 + $ua->timeout(TIMEOUT);
204 + $ua->ssl_opts( ssl_opts($sni, $expiration_date_ref, $status_ref, $san_ref) );
205 + my $request = HTTP::Request->new('GET', $url);
206 + $request->header(Host => $sni);
207 + my $response = $ua->simple_request($request);
208 + return $response;
209 +}
210 +
211 +sub wildcard_match {
212 + my ($cn, $host) = @_;
213 + my $match = 0;
214 + return 0 if $cn !~ m/^\*\.(.*)$/;
215 + my $cn_dom = $1;
216 + my $host_dom = ($sni =~ m/^[^\.]+\.(.*)$/)[0];
217 + return ($cn_dom eq $host_dom);
218 +}
219 +
220 +GetOptions ("url|u=s" => \$url,
221 + "sni|s=s" => \$sni) or pod2usage(1);
222 +if (@ARGV) {
223 + print "This script takes no arguments...\n";
224 + pod2usage(1);
225 +}
226 +pod2usage(1) if (!$url);
227 +
228 +my $expiration_date;
229 +my $uri = URI->new($url);
230 +die "Only https urls are supported\n" unless $uri->scheme eq 'https';
231 +$sni ||= $uri->host;
232 +my $response = https_get($url, $sni, \$expiration_date, \$status, \@san);
233 +
234 +my $out = {
235 + code => $response->code,
236 + status => $response->message,
237 + days_left => undef,
238 + cert_cn => undef,
239 + issuer => undef
240 +};
241 +
242 +if ($response->code != 500) { # Even a 404 is good enough, as far as cert validation goes...
243 + my $now = DateTime->now;
244 + $expiration_date = DateTime::Format::ISO8601->parse_datetime( $expiration_date );
245 +
246 + $out->{issuer} = $response->headers->{'client-ssl-cert-issuer'};
247 + $out->{cert_cn} = ($response->headers->{'client-ssl-cert-subject'} =~ m/CN=(.*)$/)[0];
248 + $status = "no common name" if !$out->{cert_cn};
249 + $out->{status} = ($status eq 'ok' and !grep { $sni eq $_ } @san and !wildcard_match($out->{cert_cn},$sni)) ?
250 + $out->{status} = "hostname mismatch ($sni doesn't match any of " . join(" ", @san) . ")" :
251 + $status;
252 + $out->{days_left} = ($expiration_date < $now) ? -1 * $expiration_date->delta_days($now)->delta_days :
253 + $expiration_date->delta_days($now)->delta_days
254 +}
255 +
256 +print to_json($out, { pretty => 1 });
257 diff -Nur --no-dereference smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/zbxtg_group.py smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/zbxtg_group.py
258 --- smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/zbxtg_group.py 1969-12-31 19:00:00.000000000 -0500
259 +++ smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/zbxtg_group.py 2021-11-08 22:00:51.153000000 -0500
260 @@ -0,0 +1,939 @@
261 +#!/usr/bin/env python
262 +# coding: utf-8
263 +
264 +import sys
265 +import os
266 +import time
267 +import random
268 +import string
269 +import requests
270 +import json
271 +import re
272 +import stat
273 +import hashlib
274 +import subprocess
275 +#import sqlite3
276 +from os.path import dirname
277 +import zbxtg_settings
278 +
279 +
280 +class Cache:
281 + def __init__(self, database):
282 + self.database = database
283 +
284 + def create_db(self, database):
285 + pass
286 +
287 +
288 +class TelegramAPI:
289 + tg_url_bot_general = "https://api.telegram.org/bot"
290 +
291 + def http_get(self, url):
292 + answer = requests.get(url, proxies=self.proxies)
293 + self.result = answer.json()
294 + self.ok_update()
295 + return self.result
296 +
297 + def __init__(self, key):
298 + self.debug = False
299 + self.key = key
300 + self.proxies = {}
301 + self.type = "private" # 'private' for private chats or 'group' for group chats
302 + self.markdown = False
303 + self.html = False
304 + self.disable_web_page_preview = False
305 + self.disable_notification = False
306 + self.reply_to_message_id = 0
307 + self.tmp_dir = None
308 + self.tmp_uids = None
309 + self.location = {"latitude": None, "longitude": None}
310 + self.update_offset = 0
311 + self.image_buttons = False
312 + self.result = None
313 + self.ok = None
314 + self.error = None
315 + self.get_updates_from_file = False
316 +
317 + def get_me(self):
318 + url = self.tg_url_bot_general + self.key + "/getMe"
319 + me = self.http_get(url)
320 + return me
321 +
322 + def get_updates(self):
323 + url = self.tg_url_bot_general + self.key + "/getUpdates"
324 + params = {"offset": self.update_offset}
325 + if self.debug:
326 + print_message(url)
327 + answer = requests.post(url, params=params, proxies=self.proxies)
328 + self.result = answer.json()
329 + if self.get_updates_from_file:
330 + print_message("Getting updated from file getUpdates.txt")
331 + self.result = json.loads("".join(file_read("getUpdates.txt")))
332 + if self.debug:
333 + print_message("Content of /getUpdates:")
334 + print_message(json.dumps(self.result))
335 + self.ok_update()
336 + return self.result
337 +
338 + def send_message(self, to, message):
339 + url = self.tg_url_bot_general + self.key + "/sendMessage"
340 + message = "\n".join(message)
341 + params = {"chat_id": to, "text": message, "disable_web_page_preview": self.disable_web_page_preview,
342 + "disable_notification": self.disable_notification}
343 + if self.reply_to_message_id:
344 + params["reply_to_message_id"] = self.reply_to_message_id
345 + if self.markdown or self.html:
346 + parse_mode = "HTML"
347 + if self.markdown:
348 + parse_mode = "Markdown"
349 + params["parse_mode"] = parse_mode
350 + if self.debug:
351 + print_message("Trying to /sendMessage:")
352 + print_message(url)
353 + print_message("post params: " + str(params))
354 + answer = requests.post(url, params=params, proxies=self.proxies)
355 + if answer.status_code == 414:
356 + self.result = {"ok": False, "description": "414 URI Too Long"}
357 + else:
358 + self.result = answer.json()
359 + self.ok_update()
360 + return self.result
361 +
362 + def update_message(self, to, message_id, message):
363 + url = self.tg_url_bot_general + self.key + "/editMessageText"
364 + message = "\n".join(message)
365 + params = {"chat_id": to, "message_id": message_id, "text": message,
366 + "disable_web_page_preview": self.disable_web_page_preview,
367 + "disable_notification": self.disable_notification}
368 + if self.markdown or self.html:
369 + parse_mode = "HTML"
370 + if self.markdown:
371 + parse_mode = "Markdown"
372 + params["parse_mode"] = parse_mode
373 + if self.debug:
374 + print_message("Trying to /editMessageText:")
375 + print_message(url)
376 + print_message("post params: " + str(params))
377 + answer = requests.post(url, params=params, proxies=self.proxies)
378 + self.result = answer.json()
379 + self.ok_update()
380 + return self.result
381 +
382 + def send_photo(self, to, message, path):
383 + url = self.tg_url_bot_general + self.key + "/sendPhoto"
384 + message = "\n".join(message)
385 + if self.image_buttons:
386 + reply_markup = json.dumps({"inline_keyboard": [[
387 + {"text": "R", "callback_data": "graph_refresh"},
388 + {"text": "1h", "callback_data": "graph_period_3600"},
389 + {"text": "3h", "callback_data": "graph_period_10800"},
390 + {"text": "6h", "callback_data": "graph_period_21600"},
391 + {"text": "12h", "callback_data": "graph_period_43200"},
392 + {"text": "24h", "callback_data": "graph_period_86400"},
393 + ], ]})
394 + else:
395 + reply_markup = json.dumps({})
396 + params = {"chat_id": to, "caption": message, "disable_notification": self.disable_notification,
397 + "reply_markup": reply_markup}
398 + if self.reply_to_message_id:
399 + params["reply_to_message_id"] = self.reply_to_message_id
400 + files = {"photo": open(path, 'rb')}
401 + if self.debug:
402 + print_message("Trying to /sendPhoto:")
403 + print_message(url)
404 + print_message(params)
405 + print_message("files: " + str(files))
406 + answer = requests.post(url, params=params, files=files, proxies=self.proxies)
407 + self.result = answer.json()
408 + self.ok_update()
409 + return self.result
410 +
411 + def send_txt(self, to, text, text_name=None):
412 + path = self.tmp_dir + "/" + "zbxtg_txt_"
413 + url = self.tg_url_bot_general + self.key + "/sendDocument"
414 + text = "\n".join(text)
415 + if not text_name:
416 + path += "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))
417 + else:
418 + path += text_name
419 + path += ".txt"
420 + file_write(path, text)
421 + params = {"chat_id": to, "caption": path.split("/")[-1], "disable_notification": self.disable_notification}
422 + if self.reply_to_message_id:
423 + params["reply_to_message_id"] = self.reply_to_message_id
424 + files = {"document": open(path, 'rb')}
425 + if self.debug:
426 + print_message("Trying to /sendDocument:")
427 + print_message(url)
428 + print_message(params)
429 + print_message("files: " + str(files))
430 + answer = requests.post(url, params=params, files=files, proxies=self.proxies)
431 + self.result = answer.json()
432 + self.ok_update()
433 + return self.result
434 +
435 + def get_uid(self, name):
436 + uid = 0
437 + if self.debug:
438 + print_message("Getting uid from /getUpdates...")
439 + updates = self.get_updates()
440 + for m in updates["result"]:
441 + if "message" in m:
442 + chat = m["message"]["chat"]
443 + elif "edited_message" in m:
444 + chat = m["edited_message"]["chat"]
445 + else:
446 + continue
447 + if chat["type"] == self.type == "private":
448 + if "username" in chat:
449 + if chat["username"] == name:
450 + uid = chat["id"]
451 + if (chat["type"] == "group" or chat["type"] == "supergroup") and self.type == "group":
452 + if "title" in chat:
453 + if sys.version_info[0] < 3:
454 + if chat["title"] == name.decode("utf-8"):
455 + uid = chat["id"]
456 + else:
457 + if chat["title"] == name:
458 + uid = chat["id"]
459 + return uid
460 +
461 + def error_need_to_contact(self, to):
462 + if self.type == "private":
463 + print_message("User '{0}' needs to send some text bot in private".format(to))
464 + if self.type == "group":
465 + print_message("You need start a conversation with your bot first in '{0}' group chat, type '/start@{1}'"
466 + .format(to, self.get_me()["result"]["username"]))
467 +
468 + def update_cache_uid(self, name, uid, message="Add new string to cache file"):
469 + cache_string = "{0};{1};{2}\n".format(name, self.type, str(uid).rstrip())
470 + # FIXME
471 + if self.debug:
472 + print_message("{0}: {1}".format(message, cache_string))
473 + with open(self.tmp_uids, "a") as cache_file_uids:
474 + cache_file_uids.write(cache_string)
475 + return True
476 +
477 + def get_uid_from_cache(self, name):
478 + if self.debug:
479 + print_message("Trying to read cached uid for {0}, {1}, from {2}".format(name, self.type, self.tmp_uids))
480 + uid = 0
481 + if os.path.isfile(self.tmp_uids):
482 + with open(self.tmp_uids, 'r') as cache_file_uids:
483 + cache_uids_old = cache_file_uids.readlines()
484 + for u in cache_uids_old:
485 + u_splitted = u.split(";")
486 + if name == u_splitted[0] and self.type == u_splitted[1]:
487 + uid = u_splitted[2]
488 + return uid
489 +
490 + def send_location(self, to, coordinates):
491 + url = self.tg_url_bot_general + self.key + "/sendLocation"
492 + params = {"chat_id": to, "disable_notification": self.disable_notification,
493 + "latitude": coordinates["latitude"], "longitude": coordinates["longitude"]}
494 + if self.reply_to_message_id:
495 + params["reply_to_message_id"] = self.reply_to_message_id
496 + if self.debug:
497 + print_message("Trying to /sendLocation:")
498 + print_message(url)
499 + print_message("post params: " + str(params))
500 + answer = requests.post(url, params=params, proxies=self.proxies)
501 + self.result = answer.json()
502 + self.ok_update()
503 + return self.result
504 +
505 + def answer_callback_query(self, callback_query_id, text=None):
506 + url = self.tg_url_bot_general + self.key + "/answerCallbackQuery"
507 + if not text:
508 + params = {"callback_query_id": callback_query_id}
509 + else:
510 + params = {"callback_query_id": callback_query_id, "text": text}
511 + answer = requests.post(url, params=params, proxies=self.proxies)
512 + self.result = answer.json()
513 + self.ok_update()
514 + return self.result
515 +
516 + def ok_update(self):
517 + self.ok = self.result["ok"]
518 + if self.ok:
519 + self.error = None
520 + else:
521 + self.error = self.result["description"]
522 + print_message(self.error)
523 + return True
524 +
525 +
526 +def markdown_fix(message, offset, emoji=False):
527 + offset = int(offset)
528 + if emoji: # https://github.com/ableev/Zabbix-in-Telegram/issues/152
529 + offset -= 2
530 + message = "\n".join(message)
531 + message = message[:offset] + message[offset+1:]
532 + message = message.split("\n")
533 + return message
534 +
535 +
536 +class ZabbixWeb:
537 + def __init__(self, server, username, password):
538 + self.debug = False
539 + self.server = server
540 + self.username = username
541 + self.password = password
542 + self.proxies = {}
543 + self.verify = True
544 + self.cookie = None
545 + self.basic_auth_user = None
546 + self.basic_auth_pass = None
547 + self.tmp_dir = None
548 +
549 + def login(self):
550 + if not self.verify:
551 + requests.packages.urllib3.disable_warnings()
552 +
553 + data_api = {"name": self.username, "password": self.password, "enter": "Sign in"}
554 + answer = requests.post(self.server + "/", data=data_api, proxies=self.proxies, verify=self.verify,
555 + auth=requests.auth.HTTPBasicAuth(self.basic_auth_user, self.basic_auth_pass))
556 + cookie = answer.cookies
557 + if len(answer.history) > 1 and answer.history[0].status_code == 302:
558 + print_message("probably the server in your config file has not full URL (for example "
559 + "'{0}' instead of '{1}')".format(self.server, self.server + "/zabbix"))
560 + if not cookie:
561 + print_message("authorization has failed, url: {0}".format(self.server + "/"))
562 + cookie = None
563 +
564 + self.cookie = cookie
565 +
566 + def graph_get(self, itemid, period, title, width, height, version=3):
567 + file_img = self.tmp_dir + "/{0}.png".format("".join(itemid))
568 +
569 + title = requests.utils.quote(title)
570 +
571 + colors = {
572 + 0: "00CC00",
573 + 1: "CC0000",
574 + 2: "0000CC",
575 + 3: "CCCC00",
576 + 4: "00CCCC",
577 + 5: "CC00CC",
578 + }
579 +
580 + drawtype = 5
581 + if len(itemid) > 1:
582 + drawtype = 2
583 +
584 + zbx_img_url_itemids = []
585 + for i in range(0, len(itemid)):
586 + itemid_url = "&items[{0}][itemid]={1}&items[{0}][sortorder]={0}&" \
587 + "items[{0}][drawtype]={3}&items[{0}][color]={2}".format(i, itemid[i], colors[i], drawtype)
588 + zbx_img_url_itemids.append(itemid_url)
589 +
590 + zbx_img_url = self.server + "/chart3.php?"
591 + if version < 4:
592 + zbx_img_url += "period={0}".format(period)
593 + else:
594 + zbx_img_url += "from=now-{0}&to=now".format(period)
595 + zbx_img_url += "&name={0}&width={1}&height={2}&graphtype=0&legend=1".format(title, width, height)
596 + zbx_img_url += "".join(zbx_img_url_itemids)
597 +
598 + if self.debug:
599 + print_message(zbx_img_url)
600 + answer = requests.get(zbx_img_url, cookies=self.cookie, proxies=self.proxies, verify=self.verify,
601 + auth=requests.auth.HTTPBasicAuth(self.basic_auth_user, self.basic_auth_pass))
602 + status_code = answer.status_code
603 + if status_code == 404:
604 + print_message("can't get image from '{0}'".format(zbx_img_url))
605 + return False
606 + res_img = answer.content
607 + file_bwrite(file_img, res_img)
608 + return file_img
609 +
610 + def api_test(self):
611 + headers = {'Content-type': 'application/json'}
612 + api_data = json.dumps({"jsonrpc": "2.0", "method": "user.login", "params":
613 + {"user": self.username, "password": self.password}, "id": 1})
614 + api_url = self.server + "/api_jsonrpc.php"
615 + api = requests.post(api_url, data=api_data, proxies=self.proxies, headers=headers)
616 + return api.text
617 +
618 +
619 +def print_message(message):
620 + message = str(message) + "\n"
621 + filename = sys.argv[0].split("/")[-1]
622 + sys.stderr.write(filename + ": " + message)
623 +
624 +
625 +def list_cut(elements, symbols_limit):
626 + symbols_count = symbols_count_now = 0
627 + elements_new = []
628 + element_last_list = []
629 + for e in elements:
630 + symbols_count_now = symbols_count + len(e)
631 + if symbols_count_now > symbols_limit:
632 + limit_idx = symbols_limit - symbols_count
633 + e_list = list(e)
634 + for idx, ee in enumerate(e_list):
635 + if idx < limit_idx:
636 + element_last_list.append(ee)
637 + else:
638 + break
639 + break
640 + else:
641 + symbols_count = symbols_count_now + 1
642 + elements_new.append(e)
643 + if symbols_count_now < symbols_limit:
644 + return elements, False
645 + else:
646 + element_last = "".join(element_last_list)
647 + elements_new.append(element_last)
648 + return elements_new, True
649 +
650 +
651 +class Maps:
652 + # https://developers.google.com/maps/documentation/geocoding/intro
653 + def __init__(self):
654 + self.key = None
655 + self.proxies = {}
656 +
657 + def get_coordinates_by_address(self, address):
658 + coordinates = {"latitude": 0, "longitude": 0}
659 + url_api = "https://maps.googleapis.com/maps/api/geocode/json?key={0}&address={1}".format(self.key, address)
660 + url = url_api
661 + answer = requests.get(url, proxies=self.proxies)
662 + result = answer.json()
663 + try:
664 + coordinates_dict = result["results"][0]["geometry"]["location"]
665 + except:
666 + if "error_message" in result:
667 + print_message("[" + result["status"] + "]: " + result["error_message"])
668 + return coordinates
669 + coordinates = {"latitude": coordinates_dict["lat"], "longitude": coordinates_dict["lng"]}
670 + return coordinates
671 +
672 +
673 +def file_write(filename, text):
674 + with open(filename, "w") as fd:
675 + fd.write(str(text))
676 + return True
677 +
678 +
679 +def file_bwrite(filename, data):
680 + with open(filename, "wb") as fd:
681 + fd.write(data)
682 + return True
683 +
684 +
685 +def file_read(filename):
686 + with open(filename, "r") as fd:
687 + text = fd.readlines()
688 + return text
689 +
690 +
691 +def file_append(filename, text):
692 + with open(filename, "a") as fd:
693 + fd.write(str(text))
694 + return True
695 +
696 +
697 +def external_image_get(url, tmp_dir, timeout=6):
698 + image_hash = hashlib.md5()
699 + image_hash.update(url.encode())
700 + file_img = tmp_dir + "/external_{0}.png".format(image_hash.hexdigest())
701 + try:
702 + answer = requests.get(url, timeout=timeout, allow_redirects=True)
703 + except requests.exceptions.ReadTimeout as ex:
704 + print_message("Can't get external image from '{0}': timeout".format(url))
705 + return False
706 + status_code = answer.status_code
707 + if status_code == 404:
708 + print_message("Can't get external image from '{0}': HTTP 404 error".format(url))
709 + return False
710 + answer_image = answer.content
711 + file_bwrite(file_img, answer_image)
712 + return file_img
713 +
714 +
715 +def age2sec(age_str):
716 + age_sec = 0
717 + age_regex = "([0-9]+d)?\s?([0-9]+h)?\s?([0-9]+m)?"
718 + age_pattern = re.compile(age_regex)
719 + intervals = age_pattern.match(age_str).groups()
720 + for i in intervals:
721 + if i:
722 + metric = i[-1]
723 + if metric == "d":
724 + age_sec += int(i[0:-1])*86400
725 + if metric == "h":
726 + age_sec += int(i[0:-1])*3600
727 + if metric == "m":
728 + age_sec += int(i[0:-1])*60
729 + return age_sec
730 +
731 +
732 +def main():
733 +
734 + tmp_dir = zbxtg_settings.zbx_tg_tmp_dir
735 + if tmp_dir == "/tmp/" + zbxtg_settings.zbx_tg_prefix:
736 + print_message("WARNING: it is strongly recommended to change `zbx_tg_tmp_dir` variable in config!!!")
737 + print_message("https://github.com/ableev/Zabbix-in-Telegram/wiki/Change-zbx_tg_tmp_dir-in-settings")
738 +
739 + tmp_cookie = tmp_dir + "/cookie.py.txt"
740 + tmp_uids = tmp_dir + "/uids.txt"
741 + tmp_need_update = False # do we need to update cache file with uids or not
742 +
743 + rnd = random.randint(0, 999)
744 + ts = time.time()
745 + hash_ts = str(ts) + "." + str(rnd)
746 +
747 + log_file = "/dev/null"
748 +
749 + args = sys.argv
750 +
751 + settings = {
752 + "zbxtg_itemid": "0", # itemid for graph
753 + "zbxtg_title": None, # title for graph
754 + "zbxtg_image_period": None,
755 + "zbxtg_image_age": "3600",
756 + "zbxtg_image_width": "900",
757 + "zbxtg_image_height": "200",
758 + "tg_method_image": False, # if True - default send images, False - send text
759 + "tg_chat": False, # send message to chat or in private
760 + "tg_group": False, # send message to chat or in private
761 + "is_debug": False,
762 + "is_channel": False,
763 + "disable_web_page_preview": False,
764 + "location": None, # address
765 + "lat": 0, # latitude
766 + "lon": 0, # longitude
767 + "is_single_message": False,
768 + "markdown": False,
769 + "html": False,
770 + "signature": None,
771 + "signature_disable": False,
772 + "graph_buttons": False,
773 + "extimg": None,
774 + "to": None,
775 + "to_group": None,
776 + "forked": False,
777 + }
778 +
779 + url_github = "https://github.com/ableev/Zabbix-in-Telegram"
780 + url_wiki_base = "https://github.com/ableev/Zabbix-in-Telegram/wiki"
781 + url_tg_group = "https://t.me/ZbxTg"
782 + url_tg_channel = "https://t.me/Zabbix_in_Telegram"
783 +
784 + settings_description = {
785 + "itemid": {"name": "zbxtg_itemid", "type": "list",
786 + "help": "script will attach a graph with that itemid (could be multiple)", "url": "Graphs"},
787 + "title": {"name": "zbxtg_title", "type": "str", "help": "title for attached graph", "url": "Graphs"},
788 + "graphs_period": {"name": "zbxtg_image_period", "type": "int", "help": "graph period", "url": "Graphs"},
789 + "graphs_age": {"name": "zbxtg_image_age", "type": "str", "help": "graph period as age", "url": "Graphs"},
790 + "graphs_width": {"name": "zbxtg_image_width", "type": "int", "help": "graph width", "url": "Graphs"},
791 + "graphs_height": {"name": "zbxtg_image_height", "type": "int", "help": "graph height", "url": "Graphs"},
792 + "graphs": {"name": "tg_method_image", "type": "bool", "help": "enables graph sending", "url": "Graphs"},
793 + "chat": {"name": "tg_chat", "type": "bool", "help": "deprecated, don't use it, see 'group'",
794 + "url": "How-to-send-message-to-the-group-chat"},
795 + "group": {"name": "tg_group", "type": "bool", "help": "sends message to a group",
796 + "url": "How-to-send-message-to-the-group-chat"},
797 + "debug": {"name": "is_debug", "type": "bool", "help": "enables 'debug'",
798 + "url": "How-to-test-script-in-command-line"},
799 + "channel": {"name": "is_channel", "type": "bool", "help": "sends message to a channel",
800 + "url": "Channel-support"},
801 + "disable_web_page_preview": {"name": "disable_web_page_preview", "type": "bool",
802 + "help": "disable web page preview", "url": "Disable-web-page-preview"},
803 + "location": {"name": "location", "type": "str", "help": "address of location", "url": "Location"},
804 + "lat": {"name": "lat", "type": "str", "help": "specify latitude (and lon too!)", "url": "Location"},
805 + "lon": {"name": "lon", "type": "str", "help": "specify longitude (and lat too!)", "url": "Location"},
806 + "single_message": {"name": "is_single_message", "type": "bool", "help": "do not split message and graph",
807 + "url": "Why-am-I-getting-two-messages-instead-of-one"},
808 + "markdown": {"name": "markdown", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML"},
809 + "html": {"name": "html", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML"},
810 + "signature": {"name": "signature", "type": "str",
811 + "help": "bot's signature", "url": "Bot-signature"},
812 + "signature_disable": {"name": "signature_disable", "type": "bool",
813 + "help": "enables/disables bot's signature", "url": "Bot-signature"},
814 + "graph_buttons": {"name": "graph_buttons", "type": "bool",
815 + "help": "activates buttons under graph, could be using in ZbxTgDaemon",
816 + "url": "Interactive-bot"},
817 + "external_image": {"name": "extimg", "type": "str",
818 + "help": "should be url; attaches external image from different source",
819 + "url": "External-image-as-graph"},
820 + "to": {"name": "to", "type": "str", "help": "rewrite zabbix username, use that instead of arguments",
821 + "url": "Custom-to-and-to_group"},
822 + "to_group": {"name": "to_group", "type": "str",
823 + "help": "rewrite zabbix username, use that instead of arguments", "url": "Custom-to-and-to_group"},
824 + "forked": {"name": "forked", "type": "bool", "help": "internal variable, do not use it. Ever.", "url": ""},
825 + }
826 +
827 + if len(args) < 4:
828 + do_not_exit = False
829 + if "--features" in args:
830 + print(("List of available settings, see {0}/Settings\n---".format(url_wiki_base)))
831 + for sett, proprt in list(settings_description.items()):
832 + print(("{0}: {1}\ndoc: {2}/{3}\n--".format(sett, proprt["help"], url_wiki_base, proprt["url"])))
833 +
834 + elif "--show-settings" in args:
835 + do_not_exit = True
836 + print_message("Settings: " + str(json.dumps(settings, indent=2)))
837 +
838 + else:
839 + print(("Hi. You should provide at least three arguments.\n"
840 + "zbxtg.py [TO] [SUBJECT] [BODY]\n\n"
841 + "1. Read main page and/or wiki: {0} + {1}\n"
842 + "2. Public Telegram group (discussion): {2}\n"
843 + "3. Public Telegram channel: {3}\n"
844 + "4. Try dev branch for test purposes (new features, etc): {0}/tree/dev"
845 + .format(url_github, url_wiki_base, url_tg_group, url_tg_channel)))
846 + if not do_not_exit:
847 + sys.exit(0)
848 +
849 +
850 + zbx_to = args[1]
851 + zbx_subject = args[2]
852 + zbx_body = args[3]
853 +
854 + tg = TelegramAPI(key=zbxtg_settings.tg_key)
855 +
856 + tg.tmp_dir = tmp_dir
857 + tg.tmp_uids = tmp_uids
858 +
859 + if zbxtg_settings.proxy_to_tg:
860 + proxy_to_tg = zbxtg_settings.proxy_to_tg
861 + if not proxy_to_tg.find("http") and not proxy_to_tg.find("socks"):
862 + proxy_to_tg = "https://" + proxy_to_tg
863 + tg.proxies = {
864 + "https": "{0}".format(proxy_to_tg),
865 + }
866 +
867 + zbx = ZabbixWeb(server=zbxtg_settings.zbx_server, username=zbxtg_settings.zbx_api_user,
868 + password=zbxtg_settings.zbx_api_pass)
869 +
870 + zbx.tmp_dir = tmp_dir
871 +
872 + # workaround for Zabbix 4.x
873 + zbx_version = 3
874 +
875 + try:
876 + zbx_version = zbxtg_settings.zbx_server_version
877 + except:
878 + pass
879 +
880 + if zbxtg_settings.proxy_to_zbx:
881 + zbx.proxies = {
882 + "http": "http://{0}/".format(zbxtg_settings.proxy_to_zbx),
883 + "https": "https://{0}/".format(zbxtg_settings.proxy_to_zbx)
884 + }
885 +
886 + # https://github.com/ableev/Zabbix-in-Telegram/issues/55
887 + try:
888 + if zbxtg_settings.zbx_basic_auth:
889 + zbx.basic_auth_user = zbxtg_settings.zbx_basic_auth_user
890 + zbx.basic_auth_pass = zbxtg_settings.zbx_basic_auth_pass
891 + except:
892 + pass
893 +
894 + try:
895 + zbx_api_verify = zbxtg_settings.zbx_api_verify
896 + zbx.verify = zbx_api_verify
897 + except:
898 + pass
899 +
900 + map = Maps()
901 + # api key to resolve address to coordinates via google api
902 + try:
903 + if zbxtg_settings.google_maps_api_key:
904 + map.key = zbxtg_settings.google_maps_api_key
905 + if zbxtg_settings.proxy_to_tg:
906 + map.proxies = {
907 + "http": "http://{0}/".format(zbxtg_settings.proxy_to_tg),
908 + "https": "https://{0}/".format(zbxtg_settings.proxy_to_tg)
909 + }
910 + except:
911 + pass
912 +
913 + zbxtg_body = (zbx_subject + "\n" + zbx_body).splitlines()
914 + zbxtg_body_text = []
915 +
916 + for line in zbxtg_body:
917 + if line.find(zbxtg_settings.zbx_tg_prefix) > -1:
918 + setting = re.split("[\s:=]+", line, maxsplit=1)
919 + key = setting[0].replace(zbxtg_settings.zbx_tg_prefix + ";", "")
920 + if key not in settings_description:
921 + if "--debug" in args:
922 + print_message("[ERROR] There is no '{0}' method, use --features to get help".format(key))
923 + continue
924 + if settings_description[key]["type"] == "list":
925 + value = setting[1].split(",")
926 + elif len(setting) > 1 and len(setting[1]) > 0:
927 + value = setting[1]
928 + elif settings_description[key]["type"] == "bool":
929 + value = True
930 + else:
931 + value = settings[settings_description[key]["name"]]
932 + if key in settings_description:
933 + settings[settings_description[key]["name"]] = value
934 + else:
935 + zbxtg_body_text.append(line)
936 +
937 + tg_method_image = bool(settings["tg_method_image"])
938 + tg_chat = bool(settings["tg_chat"])
939 + tg_group = bool(settings["tg_group"])
940 + is_debug = bool(settings["is_debug"])
941 + is_channel = bool(settings["is_channel"])
942 + disable_web_page_preview = bool(settings["disable_web_page_preview"])
943 + is_single_message = bool(settings["is_single_message"])
944 +
945 + # experimental way to send message to the group https://github.com/ableev/Zabbix-in-Telegram/issues/15
946 + if args[0].split("/")[-1] == "zbxtg_group.py" or "--group" in args or tg_chat or tg_group:
947 + tg_chat = True
948 + tg_group = True
949 + tg.type = "group"
950 +
951 + if "--debug" in args or is_debug:
952 + is_debug = True
953 + tg.debug = True
954 + zbx.debug = True
955 + print_message(tg.get_me())
956 + print_message("Cache file with uids: " + tg.tmp_uids)
957 + log_file = tmp_dir + ".debug." + hash_ts + ".log"
958 + #print_message(log_file)
959 +
960 + if "--markdown" in args or settings["markdown"]:
961 + tg.markdown = True
962 +
963 + if "--html" in args or settings["html"]:
964 + tg.html = True
965 +
966 + if "--channel" in args or is_channel:
967 + tg.type = "channel"
968 +
969 + if "--disable_web_page_preview" in args or disable_web_page_preview:
970 + if is_debug:
971 + print_message("'disable_web_page_preview' option has been enabled")
972 + tg.disable_web_page_preview = True
973 +
974 + if "--graph_buttons" in args or settings["graph_buttons"]:
975 + tg.image_buttons = True
976 +
977 + if "--forked" in args:
978 + settings["forked"] = True
979 +
980 + if "--tg-key" in args:
981 + tg.key = args[args.index("--tg-key") + 1]
982 +
983 + location_coordinates = {"latitude": None, "longitude": None}
984 + if settings["lat"] > 0 and settings["lat"] > 0:
985 + location_coordinates = {"latitude": settings["lat"], "longitude": settings["lon"]}
986 + tg.location = location_coordinates
987 + else:
988 + if settings["location"]:
989 + location_coordinates = map.get_coordinates_by_address(settings["location"])
990 + if location_coordinates:
991 + settings["lat"] = location_coordinates["latitude"]
992 + settings["lon"] = location_coordinates["longitude"]
993 + tg.location = location_coordinates
994 +
995 + if not os.path.isdir(tmp_dir):
996 + if is_debug:
997 + print_message("Tmp dir doesn't exist, creating new one...")
998 + try:
999 + os.makedirs(tmp_dir)
1000 + open(tg.tmp_uids, "a").close()
1001 + os.chmod(tmp_dir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
1002 + os.chmod(tg.tmp_uids, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
1003 + except:
1004 + tmp_dir = "/tmp"
1005 + if is_debug:
1006 + print_message("Using {0} as a temporary dir".format(tmp_dir))
1007 +
1008 + done_all_work_in_the_fork = False
1009 + # issue75
1010 +
1011 + to_types = ["to", "to_group", "to_channel"]
1012 + to_types_to_telegram = {"to": "private", "to_group": "group", "to_channel": "channel"}
1013 + multiple_to = {}
1014 + for i in to_types:
1015 + multiple_to[i]=[]
1016 +
1017 + for t in to_types:
1018 + try:
1019 + if settings[t] and not settings["forked"]:
1020 + # zbx_to = settings["to"]
1021 + multiple_to[t] = re.split(",", settings[t])
1022 + except KeyError:
1023 + pass
1024 +
1025 + # example:
1026 + # {'to_channel': [], 'to': ['usr1', 'usr2', 'usr3'], 'to_group': []}
1027 +
1028 + if (sum([len(v) for k, v in list(multiple_to.items())])) == 1:
1029 + # if we have only one recipient, we don't need fork to send message, just re-write "to" vaiable
1030 + tmp_max = 0
1031 + for t in to_types:
1032 + if len(multiple_to[t]) > tmp_max:
1033 + tmp_max = len(multiple_to[t])
1034 + tg.type = to_types_to_telegram[t]
1035 + zbx_to = multiple_to[t][0]
1036 + else:
1037 + for t in to_types:
1038 + for i in multiple_to[t]:
1039 + args_new = list(args)
1040 + args_new[1] = i
1041 + if t == "to_group":
1042 + args_new.append("--group")
1043 + args_new.append("--forked")
1044 + args_new.insert(0, sys.executable)
1045 + if is_debug:
1046 + print_message("Fork for custom recipient ({1}), new args: {0}".format(args_new,
1047 + to_types_to_telegram[t]))
1048 + subprocess.call(args_new)
1049 + done_all_work_in_the_fork = True
1050 +
1051 + if done_all_work_in_the_fork:
1052 + sys.exit(0)
1053 +
1054 + uid = None
1055 +
1056 + if tg.type == "channel":
1057 + uid = zbx_to
1058 + if tg.type == "private":
1059 + zbx_to = zbx_to.replace("@", "")
1060 +
1061 + if zbx_to.isdigit():
1062 + uid = zbx_to
1063 +
1064 + if not uid:
1065 + uid = tg.get_uid_from_cache(zbx_to)
1066 +
1067 + if not uid:
1068 + uid = tg.get_uid(zbx_to)
1069 + if uid:
1070 + tmp_need_update = True
1071 + if not uid:
1072 + tg.error_need_to_contact(zbx_to)
1073 + sys.exit(1)
1074 +
1075 + if tmp_need_update:
1076 + tg.update_cache_uid(zbx_to, str(uid).rstrip())
1077 +
1078 + if is_debug:
1079 + print_message("Telegram uid of {0} '{1}': {2}".format(tg.type, zbx_to, uid))
1080 +
1081 + # add signature, turned off by default, you can turn it on in config
1082 + try:
1083 + if "--signature" in args or settings["signature"] or zbxtg_settings.zbx_tg_signature\
1084 + and not "--signature_disable" in args and not settings["signature_disable"]:
1085 + if "--signature" in args:
1086 + settings["signature"] = args[args.index("--signature") + 1]
1087 + if not settings["signature"]:
1088 + settings["signature"] = zbxtg_settings.zbx_server
1089 + zbxtg_body_text.append("--")
1090 + zbxtg_body_text.append(settings["signature"])
1091 + except:
1092 + pass
1093 +
1094 + # replace text with emojis
1095 + internal_using_emoji = False # I hate that, but... https://github.com/ableev/Zabbix-in-Telegram/issues/152
1096 + if hasattr(zbxtg_settings, "emoji_map"):
1097 + zbxtg_body_text_emoji_support = []
1098 + for l in zbxtg_body_text:
1099 + l_new = l
1100 + for k, v in list(zbxtg_settings.emoji_map.items()):
1101 + l_new = l_new.replace("{{" + k + "}}", v)
1102 + zbxtg_body_text_emoji_support.append(l_new)
1103 + if len("".join(zbxtg_body_text)) - len("".join(zbxtg_body_text_emoji_support)):
1104 + internal_using_emoji = True
1105 + zbxtg_body_text = zbxtg_body_text_emoji_support
1106 +
1107 + if not is_single_message:
1108 + tg.send_message(uid, zbxtg_body_text)
1109 + if not tg.ok:
1110 + # first case – if group has been migrated to a supergroup, we need to update chat_id of that group
1111 + if tg.error.find("migrated") > -1 and tg.error.find("supergroup") > -1:
1112 + migrate_to_chat_id = tg.result["parameters"]["migrate_to_chat_id"]
1113 + tg.update_cache_uid(zbx_to, migrate_to_chat_id, message="Group chat is migrated to supergroup, "
1114 + "updating cache file")
1115 + uid = migrate_to_chat_id
1116 + tg.send_message(uid, zbxtg_body_text)
1117 +
1118 + # another case if markdown is enabled and we got parse error, try to remove "bad" symbols from message
1119 + if tg.markdown and tg.error.find("Can't find end of the entity starting at byte offset") > -1:
1120 + markdown_warning = "Original message has been fixed due to {0}. " \
1121 + "Please, fix the markdown, it's slowing down messages sending."\
1122 + .format(url_wiki_base + "/" + settings_description["markdown"]["url"])
1123 + markdown_fix_attempts = 0
1124 + while not tg.ok and markdown_fix_attempts != 3:
1125 + offset = re.search("Can't find end of the entity starting at byte offset ([0-9]+)", tg.error).group(1)
1126 + zbxtg_body_text = markdown_fix(zbxtg_body_text, offset, emoji=internal_using_emoji) + \
1127 + ["\n"] + [markdown_warning]
1128 + tg.disable_web_page_preview = True
1129 + tg.send_message(uid, zbxtg_body_text)
1130 + markdown_fix_attempts += 1
1131 + if tg.ok:
1132 + print_message(markdown_warning)
1133 +
1134 + if is_debug:
1135 + print((tg.result))
1136 +
1137 + if settings["zbxtg_image_age"]:
1138 + age_sec = age2sec(settings["zbxtg_image_age"])
1139 + if age_sec > 0 and age_sec > 3600:
1140 + settings["zbxtg_image_period"] = age_sec
1141 +
1142 + message_id = 0
1143 + if tg_method_image:
1144 + zbx.login()
1145 + if not zbx.cookie:
1146 + text_warn = "Login to Zabbix web UI has failed (web url, user or password are incorrect), "\
1147 + "unable to send graphs check manually"
1148 + tg.send_message(uid, [text_warn])
1149 + print_message(text_warn)
1150 + else:
1151 + if not settings["extimg"]:
1152 + zbxtg_file_img = zbx.graph_get(settings["zbxtg_itemid"], settings["zbxtg_image_period"],
1153 + settings["zbxtg_title"], settings["zbxtg_image_width"],
1154 + settings["zbxtg_image_height"], version=zbx_version)
1155 + else:
1156 + zbxtg_file_img = external_image_get(settings["extimg"], tmp_dir=zbx.tmp_dir)
1157 + zbxtg_body_text, is_modified = list_cut(zbxtg_body_text, 200)
1158 + if tg.ok:
1159 + message_id = tg.result["result"]["message_id"]
1160 + tg.reply_to_message_id = message_id
1161 + if not zbxtg_file_img:
1162 + text_warn = "Can't get graph image, check script manually, see logs, or disable graphs"
1163 + tg.send_message(uid, [text_warn])
1164 + print_message(text_warn)
1165 + else:
1166 + if not is_single_message:
1167 + zbxtg_body_text = ""
1168 + else:
1169 + if is_modified:
1170 + text_warn = "probably you will see MEDIA_CAPTION_TOO_LONG error, "\
1171 + "the message has been cut to 200 symbols, "\
1172 + "https://github.com/ableev/Zabbix-in-Telegram/issues/9"\
1173 + "#issuecomment-166895044"
1174 + print_message(text_warn)
1175 + if not is_single_message:
1176 + tg.disable_notification = True
1177 + tg.send_photo(uid, zbxtg_body_text, zbxtg_file_img)
1178 + if tg.ok:
1179 + settings["zbxtg_body_text"] = zbxtg_body_text
1180 + os.remove(zbxtg_file_img)
1181 + else:
1182 + if tg.error.find("PHOTO_INVALID_DIMENSIONS") > -1:
1183 + if not tg.disable_web_page_preview:
1184 + tg.disable_web_page_preview = True
1185 + text_warn = "Zabbix user couldn't get graph (probably has no rights to get data from host), " \
1186 + "check script manually, see {0}".format(url_wiki_base + "/" +
1187 + settings_description["graphs"]["url"])
1188 + tg.send_message(uid, [text_warn])
1189 + print_message(text_warn)
1190 + if tg.location and location_coordinates["latitude"] and location_coordinates["longitude"]:
1191 + tg.reply_to_message_id = message_id
1192 + tg.disable_notification = True
1193 + tg.send_location(to=uid, coordinates=location_coordinates)
1194 +
1195 + if "--show-settings" in args:
1196 + print_message("Settings: " + str(json.dumps(settings, indent=2)))
1197 +
1198 +if __name__ == "__main__":
1199 + main()
1200 diff -Nur --no-dereference smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/zbxtg.py smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/zbxtg.py
1201 --- smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/zbxtg.py 1969-12-31 19:00:00.000000000 -0500
1202 +++ smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/zbxtg.py 2021-11-08 22:00:51.689000000 -0500
1203 @@ -0,0 +1,939 @@
1204 +#!/usr/bin/env python
1205 +# coding: utf-8
1206 +
1207 +import sys
1208 +import os
1209 +import time
1210 +import random
1211 +import string
1212 +import requests
1213 +import json
1214 +import re
1215 +import stat
1216 +import hashlib
1217 +import subprocess
1218 +#import sqlite3
1219 +from os.path import dirname
1220 +import zbxtg_settings
1221 +
1222 +
1223 +class Cache:
1224 + def __init__(self, database):
1225 + self.database = database
1226 +
1227 + def create_db(self, database):
1228 + pass
1229 +
1230 +
1231 +class TelegramAPI:
1232 + tg_url_bot_general = "https://api.telegram.org/bot"
1233 +
1234 + def http_get(self, url):
1235 + answer = requests.get(url, proxies=self.proxies)
1236 + self.result = answer.json()
1237 + self.ok_update()
1238 + return self.result
1239 +
1240 + def __init__(self, key):
1241 + self.debug = False
1242 + self.key = key
1243 + self.proxies = {}
1244 + self.type = "private" # 'private' for private chats or 'group' for group chats
1245 + self.markdown = False
1246 + self.html = False
1247 + self.disable_web_page_preview = False
1248 + self.disable_notification = False
1249 + self.reply_to_message_id = 0
1250 + self.tmp_dir = None
1251 + self.tmp_uids = None
1252 + self.location = {"latitude": None, "longitude": None}
1253 + self.update_offset = 0
1254 + self.image_buttons = False
1255 + self.result = None
1256 + self.ok = None
1257 + self.error = None
1258 + self.get_updates_from_file = False
1259 +
1260 + def get_me(self):
1261 + url = self.tg_url_bot_general + self.key + "/getMe"
1262 + me = self.http_get(url)
1263 + return me
1264 +
1265 + def get_updates(self):
1266 + url = self.tg_url_bot_general + self.key + "/getUpdates"
1267 + params = {"offset": self.update_offset}
1268 + if self.debug:
1269 + print_message(url)
1270 + answer = requests.post(url, params=params, proxies=self.proxies)
1271 + self.result = answer.json()
1272 + if self.get_updates_from_file:
1273 + print_message("Getting updated from file getUpdates.txt")
1274 + self.result = json.loads("".join(file_read("getUpdates.txt")))
1275 + if self.debug:
1276 + print_message("Content of /getUpdates:")
1277 + print_message(json.dumps(self.result))
1278 + self.ok_update()
1279 + return self.result
1280 +
1281 + def send_message(self, to, message):
1282 + url = self.tg_url_bot_general + self.key + "/sendMessage"
1283 + message = "\n".join(message)
1284 + params = {"chat_id": to, "text": message, "disable_web_page_preview": self.disable_web_page_preview,
1285 + "disable_notification": self.disable_notification}
1286 + if self.reply_to_message_id:
1287 + params["reply_to_message_id"] = self.reply_to_message_id
1288 + if self.markdown or self.html:
1289 + parse_mode = "HTML"
1290 + if self.markdown:
1291 + parse_mode = "Markdown"
1292 + params["parse_mode"] = parse_mode
1293 + if self.debug:
1294 + print_message("Trying to /sendMessage:")
1295 + print_message(url)
1296 + print_message("post params: " + str(params))
1297 + answer = requests.post(url, params=params, proxies=self.proxies)
1298 + if answer.status_code == 414:
1299 + self.result = {"ok": False, "description": "414 URI Too Long"}
1300 + else:
1301 + self.result = answer.json()
1302 + self.ok_update()
1303 + return self.result
1304 +
1305 + def update_message(self, to, message_id, message):
1306 + url = self.tg_url_bot_general + self.key + "/editMessageText"
1307 + message = "\n".join(message)
1308 + params = {"chat_id": to, "message_id": message_id, "text": message,
1309 + "disable_web_page_preview": self.disable_web_page_preview,
1310 + "disable_notification": self.disable_notification}
1311 + if self.markdown or self.html:
1312 + parse_mode = "HTML"
1313 + if self.markdown:
1314 + parse_mode = "Markdown"
1315 + params["parse_mode"] = parse_mode
1316 + if self.debug:
1317 + print_message("Trying to /editMessageText:")
1318 + print_message(url)
1319 + print_message("post params: " + str(params))
1320 + answer = requests.post(url, params=params, proxies=self.proxies)
1321 + self.result = answer.json()
1322 + self.ok_update()
1323 + return self.result
1324 +
1325 + def send_photo(self, to, message, path):
1326 + url = self.tg_url_bot_general + self.key + "/sendPhoto"
1327 + message = "\n".join(message)
1328 + if self.image_buttons:
1329 + reply_markup = json.dumps({"inline_keyboard": [[
1330 + {"text": "R", "callback_data": "graph_refresh"},
1331 + {"text": "1h", "callback_data": "graph_period_3600"},
1332 + {"text": "3h", "callback_data": "graph_period_10800"},
1333 + {"text": "6h", "callback_data": "graph_period_21600"},
1334 + {"text": "12h", "callback_data": "graph_period_43200"},
1335 + {"text": "24h", "callback_data": "graph_period_86400"},
1336 + ], ]})
1337 + else:
1338 + reply_markup = json.dumps({})
1339 + params = {"chat_id": to, "caption": message, "disable_notification": self.disable_notification,
1340 + "reply_markup": reply_markup}
1341 + if self.reply_to_message_id:
1342 + params["reply_to_message_id"] = self.reply_to_message_id
1343 + files = {"photo": open(path, 'rb')}
1344 + if self.debug:
1345 + print_message("Trying to /sendPhoto:")
1346 + print_message(url)
1347 + print_message(params)
1348 + print_message("files: " + str(files))
1349 + answer = requests.post(url, params=params, files=files, proxies=self.proxies)
1350 + self.result = answer.json()
1351 + self.ok_update()
1352 + return self.result
1353 +
1354 + def send_txt(self, to, text, text_name=None):
1355 + path = self.tmp_dir + "/" + "zbxtg_txt_"
1356 + url = self.tg_url_bot_general + self.key + "/sendDocument"
1357 + text = "\n".join(text)
1358 + if not text_name:
1359 + path += "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(10))
1360 + else:
1361 + path += text_name
1362 + path += ".txt"
1363 + file_write(path, text)
1364 + params = {"chat_id": to, "caption": path.split("/")[-1], "disable_notification": self.disable_notification}
1365 + if self.reply_to_message_id:
1366 + params["reply_to_message_id"] = self.reply_to_message_id
1367 + files = {"document": open(path, 'rb')}
1368 + if self.debug:
1369 + print_message("Trying to /sendDocument:")
1370 + print_message(url)
1371 + print_message(params)
1372 + print_message("files: " + str(files))
1373 + answer = requests.post(url, params=params, files=files, proxies=self.proxies)
1374 + self.result = answer.json()
1375 + self.ok_update()
1376 + return self.result
1377 +
1378 + def get_uid(self, name):
1379 + uid = 0
1380 + if self.debug:
1381 + print_message("Getting uid from /getUpdates...")
1382 + updates = self.get_updates()
1383 + for m in updates["result"]:
1384 + if "message" in m:
1385 + chat = m["message"]["chat"]
1386 + elif "edited_message" in m:
1387 + chat = m["edited_message"]["chat"]
1388 + else:
1389 + continue
1390 + if chat["type"] == self.type == "private":
1391 + if "username" in chat:
1392 + if chat["username"] == name:
1393 + uid = chat["id"]
1394 + if (chat["type"] == "group" or chat["type"] == "supergroup") and self.type == "group":
1395 + if "title" in chat:
1396 + if sys.version_info[0] < 3:
1397 + if chat["title"] == name.decode("utf-8"):
1398 + uid = chat["id"]
1399 + else:
1400 + if chat["title"] == name:
1401 + uid = chat["id"]
1402 + return uid
1403 +
1404 + def error_need_to_contact(self, to):
1405 + if self.type == "private":
1406 + print_message("User '{0}' needs to send some text bot in private".format(to))
1407 + if self.type == "group":
1408 + print_message("You need start a conversation with your bot first in '{0}' group chat, type '/start@{1}'"
1409 + .format(to, self.get_me()["result"]["username"]))
1410 +
1411 + def update_cache_uid(self, name, uid, message="Add new string to cache file"):
1412 + cache_string = "{0};{1};{2}\n".format(name, self.type, str(uid).rstrip())
1413 + # FIXME
1414 + if self.debug:
1415 + print_message("{0}: {1}".format(message, cache_string))
1416 + with open(self.tmp_uids, "a") as cache_file_uids:
1417 + cache_file_uids.write(cache_string)
1418 + return True
1419 +
1420 + def get_uid_from_cache(self, name):
1421 + if self.debug:
1422 + print_message("Trying to read cached uid for {0}, {1}, from {2}".format(name, self.type, self.tmp_uids))
1423 + uid = 0
1424 + if os.path.isfile(self.tmp_uids):
1425 + with open(self.tmp_uids, 'r') as cache_file_uids:
1426 + cache_uids_old = cache_file_uids.readlines()
1427 + for u in cache_uids_old:
1428 + u_splitted = u.split(";")
1429 + if name == u_splitted[0] and self.type == u_splitted[1]:
1430 + uid = u_splitted[2]
1431 + return uid
1432 +
1433 + def send_location(self, to, coordinates):
1434 + url = self.tg_url_bot_general + self.key + "/sendLocation"
1435 + params = {"chat_id": to, "disable_notification": self.disable_notification,
1436 + "latitude": coordinates["latitude"], "longitude": coordinates["longitude"]}
1437 + if self.reply_to_message_id:
1438 + params["reply_to_message_id"] = self.reply_to_message_id
1439 + if self.debug:
1440 + print_message("Trying to /sendLocation:")
1441 + print_message(url)
1442 + print_message("post params: " + str(params))
1443 + answer = requests.post(url, params=params, proxies=self.proxies)
1444 + self.result = answer.json()
1445 + self.ok_update()
1446 + return self.result
1447 +
1448 + def answer_callback_query(self, callback_query_id, text=None):
1449 + url = self.tg_url_bot_general + self.key + "/answerCallbackQuery"
1450 + if not text:
1451 + params = {"callback_query_id": callback_query_id}
1452 + else:
1453 + params = {"callback_query_id": callback_query_id, "text": text}
1454 + answer = requests.post(url, params=params, proxies=self.proxies)
1455 + self.result = answer.json()
1456 + self.ok_update()
1457 + return self.result
1458 +
1459 + def ok_update(self):
1460 + self.ok = self.result["ok"]
1461 + if self.ok:
1462 + self.error = None
1463 + else:
1464 + self.error = self.result["description"]
1465 + print_message(self.error)
1466 + return True
1467 +
1468 +
1469 +def markdown_fix(message, offset, emoji=False):
1470 + offset = int(offset)
1471 + if emoji: # https://github.com/ableev/Zabbix-in-Telegram/issues/152
1472 + offset -= 2
1473 + message = "\n".join(message)
1474 + message = message[:offset] + message[offset+1:]
1475 + message = message.split("\n")
1476 + return message
1477 +
1478 +
1479 +class ZabbixWeb:
1480 + def __init__(self, server, username, password):
1481 + self.debug = False
1482 + self.server = server
1483 + self.username = username
1484 + self.password = password
1485 + self.proxies = {}
1486 + self.verify = True
1487 + self.cookie = None
1488 + self.basic_auth_user = None
1489 + self.basic_auth_pass = None
1490 + self.tmp_dir = None
1491 +
1492 + def login(self):
1493 + if not self.verify:
1494 + requests.packages.urllib3.disable_warnings()
1495 +
1496 + data_api = {"name": self.username, "password": self.password, "enter": "Sign in"}
1497 + answer = requests.post(self.server + "/", data=data_api, proxies=self.proxies, verify=self.verify,
1498 + auth=requests.auth.HTTPBasicAuth(self.basic_auth_user, self.basic_auth_pass))
1499 + cookie = answer.cookies
1500 + if len(answer.history) > 1 and answer.history[0].status_code == 302:
1501 + print_message("probably the server in your config file has not full URL (for example "
1502 + "'{0}' instead of '{1}')".format(self.server, self.server + "/zabbix"))
1503 + if not cookie:
1504 + print_message("authorization has failed, url: {0}".format(self.server + "/"))
1505 + cookie = None
1506 +
1507 + self.cookie = cookie
1508 +
1509 + def graph_get(self, itemid, period, title, width, height, version=3):
1510 + file_img = self.tmp_dir + "/{0}.png".format("".join(itemid))
1511 +
1512 + title = requests.utils.quote(title)
1513 +
1514 + colors = {
1515 + 0: "00CC00",
1516 + 1: "CC0000",
1517 + 2: "0000CC",
1518 + 3: "CCCC00",
1519 + 4: "00CCCC",
1520 + 5: "CC00CC",
1521 + }
1522 +
1523 + drawtype = 5
1524 + if len(itemid) > 1:
1525 + drawtype = 2
1526 +
1527 + zbx_img_url_itemids = []
1528 + for i in range(0, len(itemid)):
1529 + itemid_url = "&items[{0}][itemid]={1}&items[{0}][sortorder]={0}&" \
1530 + "items[{0}][drawtype]={3}&items[{0}][color]={2}".format(i, itemid[i], colors[i], drawtype)
1531 + zbx_img_url_itemids.append(itemid_url)
1532 +
1533 + zbx_img_url = self.server + "/chart3.php?"
1534 + if version < 4:
1535 + zbx_img_url += "period={0}".format(period)
1536 + else:
1537 + zbx_img_url += "from=now-{0}&to=now".format(period)
1538 + zbx_img_url += "&name={0}&width={1}&height={2}&graphtype=0&legend=1".format(title, width, height)
1539 + zbx_img_url += "".join(zbx_img_url_itemids)
1540 +
1541 + if self.debug:
1542 + print_message(zbx_img_url)
1543 + answer = requests.get(zbx_img_url, cookies=self.cookie, proxies=self.proxies, verify=self.verify,
1544 + auth=requests.auth.HTTPBasicAuth(self.basic_auth_user, self.basic_auth_pass))
1545 + status_code = answer.status_code
1546 + if status_code == 404:
1547 + print_message("can't get image from '{0}'".format(zbx_img_url))
1548 + return False
1549 + res_img = answer.content
1550 + file_bwrite(file_img, res_img)
1551 + return file_img
1552 +
1553 + def api_test(self):
1554 + headers = {'Content-type': 'application/json'}
1555 + api_data = json.dumps({"jsonrpc": "2.0", "method": "user.login", "params":
1556 + {"user": self.username, "password": self.password}, "id": 1})
1557 + api_url = self.server + "/api_jsonrpc.php"
1558 + api = requests.post(api_url, data=api_data, proxies=self.proxies, headers=headers)
1559 + return api.text
1560 +
1561 +
1562 +def print_message(message):
1563 + message = str(message) + "\n"
1564 + filename = sys.argv[0].split("/")[-1]
1565 + sys.stderr.write(filename + ": " + message)
1566 +
1567 +
1568 +def list_cut(elements, symbols_limit):
1569 + symbols_count = symbols_count_now = 0
1570 + elements_new = []
1571 + element_last_list = []
1572 + for e in elements:
1573 + symbols_count_now = symbols_count + len(e)
1574 + if symbols_count_now > symbols_limit:
1575 + limit_idx = symbols_limit - symbols_count
1576 + e_list = list(e)
1577 + for idx, ee in enumerate(e_list):
1578 + if idx < limit_idx:
1579 + element_last_list.append(ee)
1580 + else:
1581 + break
1582 + break
1583 + else:
1584 + symbols_count = symbols_count_now + 1
1585 + elements_new.append(e)
1586 + if symbols_count_now < symbols_limit:
1587 + return elements, False
1588 + else:
1589 + element_last = "".join(element_last_list)
1590 + elements_new.append(element_last)
1591 + return elements_new, True
1592 +
1593 +
1594 +class Maps:
1595 + # https://developers.google.com/maps/documentation/geocoding/intro
1596 + def __init__(self):
1597 + self.key = None
1598 + self.proxies = {}
1599 +
1600 + def get_coordinates_by_address(self, address):
1601 + coordinates = {"latitude": 0, "longitude": 0}
1602 + url_api = "https://maps.googleapis.com/maps/api/geocode/json?key={0}&address={1}".format(self.key, address)
1603 + url = url_api
1604 + answer = requests.get(url, proxies=self.proxies)
1605 + result = answer.json()
1606 + try:
1607 + coordinates_dict = result["results"][0]["geometry"]["location"]
1608 + except:
1609 + if "error_message" in result:
1610 + print_message("[" + result["status"] + "]: " + result["error_message"])
1611 + return coordinates
1612 + coordinates = {"latitude": coordinates_dict["lat"], "longitude": coordinates_dict["lng"]}
1613 + return coordinates
1614 +
1615 +
1616 +def file_write(filename, text):
1617 + with open(filename, "w") as fd:
1618 + fd.write(str(text))
1619 + return True
1620 +
1621 +
1622 +def file_bwrite(filename, data):
1623 + with open(filename, "wb") as fd:
1624 + fd.write(data)
1625 + return True
1626 +
1627 +
1628 +def file_read(filename):
1629 + with open(filename, "r") as fd:
1630 + text = fd.readlines()
1631 + return text
1632 +
1633 +
1634 +def file_append(filename, text):
1635 + with open(filename, "a") as fd:
1636 + fd.write(str(text))
1637 + return True
1638 +
1639 +
1640 +def external_image_get(url, tmp_dir, timeout=6):
1641 + image_hash = hashlib.md5()
1642 + image_hash.update(url.encode())
1643 + file_img = tmp_dir + "/external_{0}.png".format(image_hash.hexdigest())
1644 + try:
1645 + answer = requests.get(url, timeout=timeout, allow_redirects=True)
1646 + except requests.exceptions.ReadTimeout as ex:
1647 + print_message("Can't get external image from '{0}': timeout".format(url))
1648 + return False
1649 + status_code = answer.status_code
1650 + if status_code == 404:
1651 + print_message("Can't get external image from '{0}': HTTP 404 error".format(url))
1652 + return False
1653 + answer_image = answer.content
1654 + file_bwrite(file_img, answer_image)
1655 + return file_img
1656 +
1657 +
1658 +def age2sec(age_str):
1659 + age_sec = 0
1660 + age_regex = "([0-9]+d)?\s?([0-9]+h)?\s?([0-9]+m)?"
1661 + age_pattern = re.compile(age_regex)
1662 + intervals = age_pattern.match(age_str).groups()
1663 + for i in intervals:
1664 + if i:
1665 + metric = i[-1]
1666 + if metric == "d":
1667 + age_sec += int(i[0:-1])*86400
1668 + if metric == "h":
1669 + age_sec += int(i[0:-1])*3600
1670 + if metric == "m":
1671 + age_sec += int(i[0:-1])*60
1672 + return age_sec
1673 +
1674 +
1675 +def main():
1676 +
1677 + tmp_dir = zbxtg_settings.zbx_tg_tmp_dir
1678 + if tmp_dir == "/tmp/" + zbxtg_settings.zbx_tg_prefix:
1679 + print_message("WARNING: it is strongly recommended to change `zbx_tg_tmp_dir` variable in config!!!")
1680 + print_message("https://github.com/ableev/Zabbix-in-Telegram/wiki/Change-zbx_tg_tmp_dir-in-settings")
1681 +
1682 + tmp_cookie = tmp_dir + "/cookie.py.txt"
1683 + tmp_uids = tmp_dir + "/uids.txt"
1684 + tmp_need_update = False # do we need to update cache file with uids or not
1685 +
1686 + rnd = random.randint(0, 999)
1687 + ts = time.time()
1688 + hash_ts = str(ts) + "." + str(rnd)
1689 +
1690 + log_file = "/dev/null"
1691 +
1692 + args = sys.argv
1693 +
1694 + settings = {
1695 + "zbxtg_itemid": "0", # itemid for graph
1696 + "zbxtg_title": None, # title for graph
1697 + "zbxtg_image_period": None,
1698 + "zbxtg_image_age": "3600",
1699 + "zbxtg_image_width": "900",
1700 + "zbxtg_image_height": "200",
1701 + "tg_method_image": False, # if True - default send images, False - send text
1702 + "tg_chat": False, # send message to chat or in private
1703 + "tg_group": False, # send message to chat or in private
1704 + "is_debug": False,
1705 + "is_channel": False,
1706 + "disable_web_page_preview": False,
1707 + "location": None, # address
1708 + "lat": 0, # latitude
1709 + "lon": 0, # longitude
1710 + "is_single_message": False,
1711 + "markdown": False,
1712 + "html": False,
1713 + "signature": None,
1714 + "signature_disable": False,
1715 + "graph_buttons": False,
1716 + "extimg": None,
1717 + "to": None,
1718 + "to_group": None,
1719 + "forked": False,
1720 + }
1721 +
1722 + url_github = "https://github.com/ableev/Zabbix-in-Telegram"
1723 + url_wiki_base = "https://github.com/ableev/Zabbix-in-Telegram/wiki"
1724 + url_tg_group = "https://t.me/ZbxTg"
1725 + url_tg_channel = "https://t.me/Zabbix_in_Telegram"
1726 +
1727 + settings_description = {
1728 + "itemid": {"name": "zbxtg_itemid", "type": "list",
1729 + "help": "script will attach a graph with that itemid (could be multiple)", "url": "Graphs"},
1730 + "title": {"name": "zbxtg_title", "type": "str", "help": "title for attached graph", "url": "Graphs"},
1731 + "graphs_period": {"name": "zbxtg_image_period", "type": "int", "help": "graph period", "url": "Graphs"},
1732 + "graphs_age": {"name": "zbxtg_image_age", "type": "str", "help": "graph period as age", "url": "Graphs"},
1733 + "graphs_width": {"name": "zbxtg_image_width", "type": "int", "help": "graph width", "url": "Graphs"},
1734 + "graphs_height": {"name": "zbxtg_image_height", "type": "int", "help": "graph height", "url": "Graphs"},
1735 + "graphs": {"name": "tg_method_image", "type": "bool", "help": "enables graph sending", "url": "Graphs"},
1736 + "chat": {"name": "tg_chat", "type": "bool", "help": "deprecated, don't use it, see 'group'",
1737 + "url": "How-to-send-message-to-the-group-chat"},
1738 + "group": {"name": "tg_group", "type": "bool", "help": "sends message to a group",
1739 + "url": "How-to-send-message-to-the-group-chat"},
1740 + "debug": {"name": "is_debug", "type": "bool", "help": "enables 'debug'",
1741 + "url": "How-to-test-script-in-command-line"},
1742 + "channel": {"name": "is_channel", "type": "bool", "help": "sends message to a channel",
1743 + "url": "Channel-support"},
1744 + "disable_web_page_preview": {"name": "disable_web_page_preview", "type": "bool",
1745 + "help": "disable web page preview", "url": "Disable-web-page-preview"},
1746 + "location": {"name": "location", "type": "str", "help": "address of location", "url": "Location"},
1747 + "lat": {"name": "lat", "type": "str", "help": "specify latitude (and lon too!)", "url": "Location"},
1748 + "lon": {"name": "lon", "type": "str", "help": "specify longitude (and lat too!)", "url": "Location"},
1749 + "single_message": {"name": "is_single_message", "type": "bool", "help": "do not split message and graph",
1750 + "url": "Why-am-I-getting-two-messages-instead-of-one"},
1751 + "markdown": {"name": "markdown", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML"},
1752 + "html": {"name": "html", "type": "bool", "help": "markdown support", "url": "Markdown-and-HTML"},
1753 + "signature": {"name": "signature", "type": "str",
1754 + "help": "bot's signature", "url": "Bot-signature"},
1755 + "signature_disable": {"name": "signature_disable", "type": "bool",
1756 + "help": "enables/disables bot's signature", "url": "Bot-signature"},
1757 + "graph_buttons": {"name": "graph_buttons", "type": "bool",
1758 + "help": "activates buttons under graph, could be using in ZbxTgDaemon",
1759 + "url": "Interactive-bot"},
1760 + "external_image": {"name": "extimg", "type": "str",
1761 + "help": "should be url; attaches external image from different source",
1762 + "url": "External-image-as-graph"},
1763 + "to": {"name": "to", "type": "str", "help": "rewrite zabbix username, use that instead of arguments",
1764 + "url": "Custom-to-and-to_group"},
1765 + "to_group": {"name": "to_group", "type": "str",
1766 + "help": "rewrite zabbix username, use that instead of arguments", "url": "Custom-to-and-to_group"},
1767 + "forked": {"name": "forked", "type": "bool", "help": "internal variable, do not use it. Ever.", "url": ""},
1768 + }
1769 +
1770 + if len(args) < 4:
1771 + do_not_exit = False
1772 + if "--features" in args:
1773 + print(("List of available settings, see {0}/Settings\n---".format(url_wiki_base)))
1774 + for sett, proprt in list(settings_description.items()):
1775 + print(("{0}: {1}\ndoc: {2}/{3}\n--".format(sett, proprt["help"], url_wiki_base, proprt["url"])))
1776 +
1777 + elif "--show-settings" in args:
1778 + do_not_exit = True
1779 + print_message("Settings: " + str(json.dumps(settings, indent=2)))
1780 +
1781 + else:
1782 + print(("Hi. You should provide at least three arguments.\n"
1783 + "zbxtg.py [TO] [SUBJECT] [BODY]\n\n"
1784 + "1. Read main page and/or wiki: {0} + {1}\n"
1785 + "2. Public Telegram group (discussion): {2}\n"
1786 + "3. Public Telegram channel: {3}\n"
1787 + "4. Try dev branch for test purposes (new features, etc): {0}/tree/dev"
1788 + .format(url_github, url_wiki_base, url_tg_group, url_tg_channel)))
1789 + if not do_not_exit:
1790 + sys.exit(0)
1791 +
1792 +
1793 + zbx_to = args[1]
1794 + zbx_subject = args[2]
1795 + zbx_body = args[3]
1796 +
1797 + tg = TelegramAPI(key=zbxtg_settings.tg_key)
1798 +
1799 + tg.tmp_dir = tmp_dir
1800 + tg.tmp_uids = tmp_uids
1801 +
1802 + if zbxtg_settings.proxy_to_tg:
1803 + proxy_to_tg = zbxtg_settings.proxy_to_tg
1804 + if not proxy_to_tg.find("http") and not proxy_to_tg.find("socks"):
1805 + proxy_to_tg = "https://" + proxy_to_tg
1806 + tg.proxies = {
1807 + "https": "{0}".format(proxy_to_tg),
1808 + }
1809 +
1810 + zbx = ZabbixWeb(server=zbxtg_settings.zbx_server, username=zbxtg_settings.zbx_api_user,
1811 + password=zbxtg_settings.zbx_api_pass)
1812 +
1813 + zbx.tmp_dir = tmp_dir
1814 +
1815 + # workaround for Zabbix 4.x
1816 + zbx_version = 3
1817 +
1818 + try:
1819 + zbx_version = zbxtg_settings.zbx_server_version
1820 + except:
1821 + pass
1822 +
1823 + if zbxtg_settings.proxy_to_zbx:
1824 + zbx.proxies = {
1825 + "http": "http://{0}/".format(zbxtg_settings.proxy_to_zbx),
1826 + "https": "https://{0}/".format(zbxtg_settings.proxy_to_zbx)
1827 + }
1828 +
1829 + # https://github.com/ableev/Zabbix-in-Telegram/issues/55
1830 + try:
1831 + if zbxtg_settings.zbx_basic_auth:
1832 + zbx.basic_auth_user = zbxtg_settings.zbx_basic_auth_user
1833 + zbx.basic_auth_pass = zbxtg_settings.zbx_basic_auth_pass
1834 + except:
1835 + pass
1836 +
1837 + try:
1838 + zbx_api_verify = zbxtg_settings.zbx_api_verify
1839 + zbx.verify = zbx_api_verify
1840 + except:
1841 + pass
1842 +
1843 + map = Maps()
1844 + # api key to resolve address to coordinates via google api
1845 + try:
1846 + if zbxtg_settings.google_maps_api_key:
1847 + map.key = zbxtg_settings.google_maps_api_key
1848 + if zbxtg_settings.proxy_to_tg:
1849 + map.proxies = {
1850 + "http": "http://{0}/".format(zbxtg_settings.proxy_to_tg),
1851 + "https": "https://{0}/".format(zbxtg_settings.proxy_to_tg)
1852 + }
1853 + except:
1854 + pass
1855 +
1856 + zbxtg_body = (zbx_subject + "\n" + zbx_body).splitlines()
1857 + zbxtg_body_text = []
1858 +
1859 + for line in zbxtg_body:
1860 + if line.find(zbxtg_settings.zbx_tg_prefix) > -1:
1861 + setting = re.split("[\s:=]+", line, maxsplit=1)
1862 + key = setting[0].replace(zbxtg_settings.zbx_tg_prefix + ";", "")
1863 + if key not in settings_description:
1864 + if "--debug" in args:
1865 + print_message("[ERROR] There is no '{0}' method, use --features to get help".format(key))
1866 + continue
1867 + if settings_description[key]["type"] == "list":
1868 + value = setting[1].split(",")
1869 + elif len(setting) > 1 and len(setting[1]) > 0:
1870 + value = setting[1]
1871 + elif settings_description[key]["type"] == "bool":
1872 + value = True
1873 + else:
1874 + value = settings[settings_description[key]["name"]]
1875 + if key in settings_description:
1876 + settings[settings_description[key]["name"]] = value
1877 + else:
1878 + zbxtg_body_text.append(line)
1879 +
1880 + tg_method_image = bool(settings["tg_method_image"])
1881 + tg_chat = bool(settings["tg_chat"])
1882 + tg_group = bool(settings["tg_group"])
1883 + is_debug = bool(settings["is_debug"])
1884 + is_channel = bool(settings["is_channel"])
1885 + disable_web_page_preview = bool(settings["disable_web_page_preview"])
1886 + is_single_message = bool(settings["is_single_message"])
1887 +
1888 + # experimental way to send message to the group https://github.com/ableev/Zabbix-in-Telegram/issues/15
1889 + if args[0].split("/")[-1] == "zbxtg_group.py" or "--group" in args or tg_chat or tg_group:
1890 + tg_chat = True
1891 + tg_group = True
1892 + tg.type = "group"
1893 +
1894 + if "--debug" in args or is_debug:
1895 + is_debug = True
1896 + tg.debug = True
1897 + zbx.debug = True
1898 + print_message(tg.get_me())
1899 + print_message("Cache file with uids: " + tg.tmp_uids)
1900 + log_file = tmp_dir + ".debug." + hash_ts + ".log"
1901 + #print_message(log_file)
1902 +
1903 + if "--markdown" in args or settings["markdown"]:
1904 + tg.markdown = True
1905 +
1906 + if "--html" in args or settings["html"]:
1907 + tg.html = True
1908 +
1909 + if "--channel" in args or is_channel:
1910 + tg.type = "channel"
1911 +
1912 + if "--disable_web_page_preview" in args or disable_web_page_preview:
1913 + if is_debug:
1914 + print_message("'disable_web_page_preview' option has been enabled")
1915 + tg.disable_web_page_preview = True
1916 +
1917 + if "--graph_buttons" in args or settings["graph_buttons"]:
1918 + tg.image_buttons = True
1919 +
1920 + if "--forked" in args:
1921 + settings["forked"] = True
1922 +
1923 + if "--tg-key" in args:
1924 + tg.key = args[args.index("--tg-key") + 1]
1925 +
1926 + location_coordinates = {"latitude": None, "longitude": None}
1927 + if settings["lat"] > 0 and settings["lat"] > 0:
1928 + location_coordinates = {"latitude": settings["lat"], "longitude": settings["lon"]}
1929 + tg.location = location_coordinates
1930 + else:
1931 + if settings["location"]:
1932 + location_coordinates = map.get_coordinates_by_address(settings["location"])
1933 + if location_coordinates:
1934 + settings["lat"] = location_coordinates["latitude"]
1935 + settings["lon"] = location_coordinates["longitude"]
1936 + tg.location = location_coordinates
1937 +
1938 + if not os.path.isdir(tmp_dir):
1939 + if is_debug:
1940 + print_message("Tmp dir doesn't exist, creating new one...")
1941 + try:
1942 + os.makedirs(tmp_dir)
1943 + open(tg.tmp_uids, "a").close()
1944 + os.chmod(tmp_dir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
1945 + os.chmod(tg.tmp_uids, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
1946 + except:
1947 + tmp_dir = "/tmp"
1948 + if is_debug:
1949 + print_message("Using {0} as a temporary dir".format(tmp_dir))
1950 +
1951 + done_all_work_in_the_fork = False
1952 + # issue75
1953 +
1954 + to_types = ["to", "to_group", "to_channel"]
1955 + to_types_to_telegram = {"to": "private", "to_group": "group", "to_channel": "channel"}
1956 + multiple_to = {}
1957 + for i in to_types:
1958 + multiple_to[i]=[]
1959 +
1960 + for t in to_types:
1961 + try:
1962 + if settings[t] and not settings["forked"]:
1963 + # zbx_to = settings["to"]
1964 + multiple_to[t] = re.split(",", settings[t])
1965 + except KeyError:
1966 + pass
1967 +
1968 + # example:
1969 + # {'to_channel': [], 'to': ['usr1', 'usr2', 'usr3'], 'to_group': []}
1970 +
1971 + if (sum([len(v) for k, v in list(multiple_to.items())])) == 1:
1972 + # if we have only one recipient, we don't need fork to send message, just re-write "to" vaiable
1973 + tmp_max = 0
1974 + for t in to_types:
1975 + if len(multiple_to[t]) > tmp_max:
1976 + tmp_max = len(multiple_to[t])
1977 + tg.type = to_types_to_telegram[t]
1978 + zbx_to = multiple_to[t][0]
1979 + else:
1980 + for t in to_types:
1981 + for i in multiple_to[t]:
1982 + args_new = list(args)
1983 + args_new[1] = i
1984 + if t == "to_group":
1985 + args_new.append("--group")
1986 + args_new.append("--forked")
1987 + args_new.insert(0, sys.executable)
1988 + if is_debug:
1989 + print_message("Fork for custom recipient ({1}), new args: {0}".format(args_new,
1990 + to_types_to_telegram[t]))
1991 + subprocess.call(args_new)
1992 + done_all_work_in_the_fork = True
1993 +
1994 + if done_all_work_in_the_fork:
1995 + sys.exit(0)
1996 +
1997 + uid = None
1998 +
1999 + if tg.type == "channel":
2000 + uid = zbx_to
2001 + if tg.type == "private":
2002 + zbx_to = zbx_to.replace("@", "")
2003 +
2004 + if zbx_to.isdigit():
2005 + uid = zbx_to
2006 +
2007 + if not uid:
2008 + uid = tg.get_uid_from_cache(zbx_to)
2009 +
2010 + if not uid:
2011 + uid = tg.get_uid(zbx_to)
2012 + if uid:
2013 + tmp_need_update = True
2014 + if not uid:
2015 + tg.error_need_to_contact(zbx_to)
2016 + sys.exit(1)
2017 +
2018 + if tmp_need_update:
2019 + tg.update_cache_uid(zbx_to, str(uid).rstrip())
2020 +
2021 + if is_debug:
2022 + print_message("Telegram uid of {0} '{1}': {2}".format(tg.type, zbx_to, uid))
2023 +
2024 + # add signature, turned off by default, you can turn it on in config
2025 + try:
2026 + if "--signature" in args or settings["signature"] or zbxtg_settings.zbx_tg_signature\
2027 + and not "--signature_disable" in args and not settings["signature_disable"]:
2028 + if "--signature" in args:
2029 + settings["signature"] = args[args.index("--signature") + 1]
2030 + if not settings["signature"]:
2031 + settings["signature"] = zbxtg_settings.zbx_server
2032 + zbxtg_body_text.append("--")
2033 + zbxtg_body_text.append(settings["signature"])
2034 + except:
2035 + pass
2036 +
2037 + # replace text with emojis
2038 + internal_using_emoji = False # I hate that, but... https://github.com/ableev/Zabbix-in-Telegram/issues/152
2039 + if hasattr(zbxtg_settings, "emoji_map"):
2040 + zbxtg_body_text_emoji_support = []
2041 + for l in zbxtg_body_text:
2042 + l_new = l
2043 + for k, v in list(zbxtg_settings.emoji_map.items()):
2044 + l_new = l_new.replace("{{" + k + "}}", v)
2045 + zbxtg_body_text_emoji_support.append(l_new)
2046 + if len("".join(zbxtg_body_text)) - len("".join(zbxtg_body_text_emoji_support)):
2047 + internal_using_emoji = True
2048 + zbxtg_body_text = zbxtg_body_text_emoji_support
2049 +
2050 + if not is_single_message:
2051 + tg.send_message(uid, zbxtg_body_text)
2052 + if not tg.ok:
2053 + # first case – if group has been migrated to a supergroup, we need to update chat_id of that group
2054 + if tg.error.find("migrated") > -1 and tg.error.find("supergroup") > -1:
2055 + migrate_to_chat_id = tg.result["parameters"]["migrate_to_chat_id"]
2056 + tg.update_cache_uid(zbx_to, migrate_to_chat_id, message="Group chat is migrated to supergroup, "
2057 + "updating cache file")
2058 + uid = migrate_to_chat_id
2059 + tg.send_message(uid, zbxtg_body_text)
2060 +
2061 + # another case if markdown is enabled and we got parse error, try to remove "bad" symbols from message
2062 + if tg.markdown and tg.error.find("Can't find end of the entity starting at byte offset") > -1:
2063 + markdown_warning = "Original message has been fixed due to {0}. " \
2064 + "Please, fix the markdown, it's slowing down messages sending."\
2065 + .format(url_wiki_base + "/" + settings_description["markdown"]["url"])
2066 + markdown_fix_attempts = 0
2067 + while not tg.ok and markdown_fix_attempts != 3:
2068 + offset = re.search("Can't find end of the entity starting at byte offset ([0-9]+)", tg.error).group(1)
2069 + zbxtg_body_text = markdown_fix(zbxtg_body_text, offset, emoji=internal_using_emoji) + \
2070 + ["\n"] + [markdown_warning]
2071 + tg.disable_web_page_preview = True
2072 + tg.send_message(uid, zbxtg_body_text)
2073 + markdown_fix_attempts += 1
2074 + if tg.ok:
2075 + print_message(markdown_warning)
2076 +
2077 + if is_debug:
2078 + print((tg.result))
2079 +
2080 + if settings["zbxtg_image_age"]:
2081 + age_sec = age2sec(settings["zbxtg_image_age"])
2082 + if age_sec > 0 and age_sec > 3600:
2083 + settings["zbxtg_image_period"] = age_sec
2084 +
2085 + message_id = 0
2086 + if tg_method_image:
2087 + zbx.login()
2088 + if not zbx.cookie:
2089 + text_warn = "Login to Zabbix web UI has failed (web url, user or password are incorrect), "\
2090 + "unable to send graphs check manually"
2091 + tg.send_message(uid, [text_warn])
2092 + print_message(text_warn)
2093 + else:
2094 + if not settings["extimg"]:
2095 + zbxtg_file_img = zbx.graph_get(settings["zbxtg_itemid"], settings["zbxtg_image_period"],
2096 + settings["zbxtg_title"], settings["zbxtg_image_width"],
2097 + settings["zbxtg_image_height"], version=zbx_version)
2098 + else:
2099 + zbxtg_file_img = external_image_get(settings["extimg"], tmp_dir=zbx.tmp_dir)
2100 + zbxtg_body_text, is_modified = list_cut(zbxtg_body_text, 200)
2101 + if tg.ok:
2102 + message_id = tg.result["result"]["message_id"]
2103 + tg.reply_to_message_id = message_id
2104 + if not zbxtg_file_img:
2105 + text_warn = "Can't get graph image, check script manually, see logs, or disable graphs"
2106 + tg.send_message(uid, [text_warn])
2107 + print_message(text_warn)
2108 + else:
2109 + if not is_single_message:
2110 + zbxtg_body_text = ""
2111 + else:
2112 + if is_modified:
2113 + text_warn = "probably you will see MEDIA_CAPTION_TOO_LONG error, "\
2114 + "the message has been cut to 200 symbols, "\
2115 + "https://github.com/ableev/Zabbix-in-Telegram/issues/9"\
2116 + "#issuecomment-166895044"
2117 + print_message(text_warn)
2118 + if not is_single_message:
2119 + tg.disable_notification = True
2120 + tg.send_photo(uid, zbxtg_body_text, zbxtg_file_img)
2121 + if tg.ok:
2122 + settings["zbxtg_body_text"] = zbxtg_body_text
2123 + os.remove(zbxtg_file_img)
2124 + else:
2125 + if tg.error.find("PHOTO_INVALID_DIMENSIONS") > -1:
2126 + if not tg.disable_web_page_preview:
2127 + tg.disable_web_page_preview = True
2128 + text_warn = "Zabbix user couldn't get graph (probably has no rights to get data from host), " \
2129 + "check script manually, see {0}".format(url_wiki_base + "/" +
2130 + settings_description["graphs"]["url"])
2131 + tg.send_message(uid, [text_warn])
2132 + print_message(text_warn)
2133 + if tg.location and location_coordinates["latitude"] and location_coordinates["longitude"]:
2134 + tg.reply_to_message_id = message_id
2135 + tg.disable_notification = True
2136 + tg.send_location(to=uid, coordinates=location_coordinates)
2137 +
2138 + if "--show-settings" in args:
2139 + print_message("Settings: " + str(json.dumps(settings, indent=2)))
2140 +
2141 +if __name__ == "__main__":
2142 + main()
2143 diff -Nur --no-dereference smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/zbxtg_settings.example.py smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/zbxtg_settings.example.py
2144 --- smeserver-zabbix-server-0.1.old/root/var/lib/zabbix/bin/zbxtg_settings.example.py 1969-12-31 19:00:00.000000000 -0500
2145 +++ smeserver-zabbix-server-0.1/root/var/lib/zabbix/bin/zbxtg_settings.example.py 2021-11-08 22:03:56.386000000 -0500
2146 @@ -0,0 +1,68 @@
2147 +# -*- coding: utf-8 -*-
2148 +
2149 +tg_key = "XYZ" # telegram bot api key
2150 +
2151 +zbx_tg_prefix = "zbxtg" # variable for separating text from script info
2152 +zbx_tg_tmp_dir = "/var/tmp/" + zbx_tg_prefix # directory for saving caches, uids, cookies, etc.
2153 +zbx_tg_signature = False
2154 +
2155 +zbx_tg_update_messages = True
2156 +zbx_tg_matches = {
2157 + "problem": "PROBLEM: ",
2158 + "ok": "OK: "
2159 +}
2160 +
2161 +zbx_server = "http://127.0.0.1/zabbix/" # zabbix server full url
2162 +zbx_api_user = "api"
2163 +zbx_api_pass = "api"
2164 +zbx_api_verify = True # True - do not ignore self signed certificates, False - ignore
2165 +
2166 +#zbx_server_version = 2 # for Zabbix 2.x version
2167 +zbx_server_version = 3 # for Zabbix 3.x version, by default, not everyone updated to 4.x yet
2168 +#zbx_server_version = 4 # for Zabbix 4.x version, default will be changed in the future with this
2169 +
2170 +zbx_basic_auth = False
2171 +zbx_basic_auth_user = "zabbix"
2172 +zbx_basic_auth_pass = "zabbix"
2173 +
2174 +proxy_to_zbx = None
2175 +proxy_to_tg = None
2176 +
2177 +# proxy_to_zbx = "http://proxy.local:3128"
2178 +# proxy_to_tg = "https://proxy.local:3128"
2179 +
2180 +# proxy_to_tg = "socks5://user1:password2@hostname:port" # socks5 with username and password
2181 +# proxy_to_tg = "socks5://hostname:port" # socks5 without username and password
2182 +# proxy_to_tg = "socks5h://hostname:port" # hostname resolution on SOCKS proxy.
2183 + # This helps when internet provider alter DNS queries.
2184 + # Found here: https://stackoverflow.com/a/43266186/957508
2185 +
2186 +google_maps_api_key = None # get your key, see https://developers.google.com/maps/documentation/geocoding/intro
2187 +
2188 +zbx_tg_daemon_enabled = False
2189 +zbx_tg_daemon_enabled_ids = [6931850, ]
2190 +zbx_tg_daemon_enabled_users = ["ableev", ]
2191 +zbx_tg_daemon_enabled_chats = ["Zabbix in Telegram Script", ]
2192 +
2193 +zbx_db_host = "localhost"
2194 +zbx_db_database = "zabbix"
2195 +zbx_db_user = "zbxtg"
2196 +zbx_db_password = "zbxtg"
2197 +
2198 +
2199 +emoji_map = {
2200 + "Disaster": "đŸ”Ĩ",
2201 + "High": "🛑",
2202 + "Average": "❗",
2203 + "Warning": "⚠ī¸",
2204 + "Information": "ℹī¸",
2205 + "Not classified": "🔘",
2206 + "OK": "✅",
2207 + "PROBLEM": "❗",
2208 + "info": "ℹī¸",
2209 + "WARNING": "⚠ī¸",
2210 + "DISASTER": "❌",
2211 + "bomb": "đŸ’Ŗ",
2212 + "fire": "đŸ”Ĩ",
2213 + "hankey": "💩",
2214 +}

admin@koozali.org
ViewVC Help
Powered by ViewVC 1.2.1 RSS 2.0 feed