Skip to content

Commit 53594f1

Browse files
authored
Merge pull request #435 from ton-blockchain/dev
Dev
2 parents ee82cb6 + 2bfb150 commit 53594f1

File tree

9 files changed

+153
-35
lines changed

9 files changed

+153
-35
lines changed

modules/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ class Setting:
5959
'defaultCustomOverlaysUrl': Setting(None, 'https://ton-blockchain.github.io/fallback_custom_overlays.json', 'Default custom overlays config url'),
6060
'debug': Setting(None, False, 'Debug mtc console mode. Prints Traceback on errors'),
6161
'subscribe_tg_channel': Setting('validator', False, 'Disables warning about subscribing to the `TON STATUS` channel'),
62-
'BotToken': Setting('alert-bot', None, 'Alerting Telegram bot token'),
63-
'ChatId': Setting('alert-bot', None, 'Alerting Telegram chat id'),
6462
'auto_backup': Setting('validator', None, 'Make validator backup every election'),
6563
'auto_backup_path': Setting('validator', '/tmp/mytoncore/auto_backups/', 'Path to store auto-backups'),
6664
'prometheus_url': Setting('prometheus', None, 'Prometheus pushgateway url'),

modules/alert_bot.py

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
@dataclasses.dataclass
1212
class Alert:
1313
severity: str
14+
description: str
1415
text: str
1516
timeout: int
1617

@@ -29,75 +30,89 @@ def init_alerts():
2930
ALERTS = {
3031
"low_wallet_balance": Alert(
3132
"low",
32-
"Validator wallet {wallet} balance is low: {balance} TON.",
33+
"Validator's wallet balance is less than 10 TON",
34+
"Validator's wallet <code>{wallet}</code> balance is less than 10 TON: {balance} TON.",
3335
18 * HOUR
3436
),
3537
"db_usage_80": Alert(
3638
"high",
39+
"Node's db usage is more than 80%",
3740
"""TON DB usage > 80%. Clean the TON database:
3841
https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming
3942
or (and) set node\'s archive ttl to lower value.""",
4043
24 * HOUR
4144
),
4245
"db_usage_95": Alert(
4346
"critical",
47+
"Node's db usage is more than 95%",
4448
"""TON DB usage > 95%. Disk is almost full, clean the TON database immediately:
4549
https://docs.ton.org/participate/nodes/node-maintenance-and-security#database-grooming
4650
or (and) set node\'s archive ttl to lower value.""",
4751
6 * HOUR
4852
),
4953
"low_efficiency": Alert(
5054
"high",
51-
"""Validator efficiency is low: {efficiency}%.""",
55+
"Validator had efficiency less than 90% in the validation round",
56+
"""Validator efficiency is less than 90%: <b>{efficiency}%</b>.""",
5257
VALIDATION_PERIOD // 3
5358
),
5459
"out_of_sync": Alert(
5560
"critical",
56-
"Node is out of sync on {sync} sec.",
61+
"Node is out of sync on more than 20 sec",
62+
"Node is out of sync on more than 20 sec: <b>{sync} sec</b>.",
5763
300
5864
),
5965
"service_down": Alert(
6066
"critical",
67+
"Node is not running (service is down)",
6168
"validator.service is down.",
6269
300
6370
),
6471
"adnl_connection_failed": Alert(
6572
"high",
73+
"Node is not answering to ADNL connection",
6674
"ADNL connection to node failed",
6775
3 * HOUR
6876
),
6977
"zero_block_created": Alert(
7078
"critical",
79+
f"Validator has not created any blocks in the {int(VALIDATION_PERIOD // 3 // 3600)} hours",
7180
"Validator has not created any blocks in the last {hours} hours.",
7281
VALIDATION_PERIOD // 3
7382
),
7483
"validator_slashed": Alert(
7584
"high",
85+
"Validator has been slashed in the previous validation round",
7686
"Validator has been slashed in previous round for {amount} TON",
7787
FREEZE_PERIOD
7888
),
7989
"stake_not_accepted": Alert(
8090
"high",
8191
"Validator's stake has not been accepted",
92+
"Validator's stake has not been accepted",
8293
ELECTIONS_START_BEFORE
8394
),
8495
"stake_accepted": Alert(
8596
"info",
97+
"Validator's stake has been accepted (info alert with no sound)",
8698
"Validator's stake {stake} TON has been accepted",
8799
ELECTIONS_START_BEFORE
88100
),
89101
"stake_returned": Alert(
90102
"info",
91-
"Validator's stake {stake} TON has been returned on address {address}. The reward amount is {reward} TON.",
103+
"Validator's stake has been returned (info alert with no sound)",
104+
"Validator's stake {stake} TON has been returned on address <code>{address}</code>. The reward amount is {reward} TON.",
92105
60
93106
),
94107
"stake_not_returned": Alert(
95108
"high",
96-
"Validator's stake has not been returned on address {address}.",
109+
"Validator's stake has not been returned",
110+
"Validator's stake has not been returned on address <code>{address}.</code>",
97111
60
98112
),
99113
"voting": Alert(
100114
"high",
115+
"There is an active network proposal that has many votes (more than 50% of required) but is not voted by the validator",
101116
"Found proposals with hashes `{hashes}` that have significant amount of votes, but current validator didn't vote for them. Please check @tonstatus for more details.",
102117
VALIDATION_PERIOD
103118
),
@@ -115,18 +130,19 @@ def __init__(self, ton, local, *args, **kwargs):
115130
self.inited = False
116131
self.hostname = None
117132
self.ip = None
133+
self.adnl = None
118134
self.token = None
119135
self.chat_id = None
120136
self.last_db_check = 0
121137

122-
def send_message(self, text: str, silent: bool = False):
138+
def send_message(self, text: str, silent: bool = False, disable_web_page_preview: bool = False):
123139
if self.token is None:
124140
raise Exception("send_message error: token is not initialized")
125141
if self.chat_id is None:
126142
raise Exception("send_message error: chat_id is not initialized")
127143
request_url = f"https://api.telegram.org/bot{self.token}/sendMessage"
128-
data = {'chat_id': self.chat_id, 'text': text, 'parse_mode': 'HTML', 'disable_notification': silent}
129-
response = requests.post(request_url, data=data, timeout=3)
144+
data = {'chat_id': self.chat_id, 'text': text, 'parse_mode': 'HTML', 'disable_notification': silent, 'link_preview_options': {'is_disabled': disable_web_page_preview}}
145+
response = requests.post(request_url, json=data, timeout=3)
130146
if response.status_code != 200:
131147
raise Exception(f"send_message error: {response.text}")
132148
response = response.json()
@@ -141,16 +157,23 @@ def send_alert(self, alert_name: str, *args, **kwargs):
141157
alert = ALERTS.get(alert_name)
142158
if alert is None:
143159
raise Exception(f"Alert {alert_name} not found")
144-
text = f'''
145-
❗️ <b>MyTonCtrl Alert {alert_name}</b> ❗️
160+
alert_name_readable = alert_name.replace('_', ' ').title()
161+
text = '🆘' if alert.severity != 'info' else ''
162+
text += f''' <b>Node {self.hostname}: {alert_name_readable} </b>
163+
164+
{alert.text.format(*args, **kwargs)}
146165
147166
Hostname: <code>{self.hostname}</code>
148167
Node IP: <code>{self.ip}</code>
168+
ADNL: <code>{self.adnl}</code>'''
169+
170+
if self.ton.using_validator():
171+
text += f"\nWallet: <code>{self.wallet}</code>"
172+
173+
text += f'''
149174
Time: <code>{time_}</code> (<code>{int(time.time())}</code>)
175+
Alert name: <code>{alert_name}</code>
150176
Severity: <code>{alert.severity}</code>
151-
152-
Alert text:
153-
<blockquote> {alert.text.format(*args, **kwargs)} </blockquote>
154177
'''
155178
if time.time() - last_sent > alert.timeout:
156179
self.send_message(text, alert.severity == "info") # send info alerts without sound
@@ -174,6 +197,9 @@ def init(self):
174197
from modules.validator import ValidatorModule
175198
self.validator_module = ValidatorModule(self.ton, self.local)
176199
self.hostname = get_hostname()
200+
adnl = self.ton.GetAdnlAddr()
201+
self.adnl = adnl
202+
self.wallet = self.ton.GetValidatorWallet().addrB64
177203
self.ip = self.ton.get_node_ip()
178204
self.set_global_vars()
179205
init_alerts()
@@ -230,6 +256,41 @@ def test_alert(self, args):
230256
self.init()
231257
self.send_message('Test alert')
232258

259+
def setup_alert_bot(self, args):
260+
if len(args) != 2:
261+
raise Exception("Usage: setup_alert_bot <bot_token> <chat_id>")
262+
self.token = args[0]
263+
self.chat_id = args[1]
264+
init_alerts()
265+
try:
266+
self.send_welcome_message()
267+
self.ton.local.db['BotToken'] = args[0]
268+
self.ton.local.db['ChatId'] = args[1]
269+
color_print("setup_alert_bot - {green}OK{endc}")
270+
except Exception as e:
271+
self.local.add_log(f"Error while sending welcome message: {e}", "error")
272+
self.local.add_log(f"If you want the bot to write to a multi-person chat group, make sure the bot is added to that chat group. If it is not - do it and run the command `setup_alert_bot <bot_token> <chat_id>` again.", "info")
273+
color_print("setup_alert_bot - {red}Error{endc}")
274+
275+
def send_welcome_message(self):
276+
message = f"""
277+
This is alert bot. You have connected validator with ADNL <code>{self.ton.GetAdnlAddr()}</code>.
278+
279+
I don't process any commands, I only send notifications.
280+
281+
Current notifications enabled:
282+
283+
"""
284+
for alert in ALERTS.values():
285+
message += f"- {alert.description}\n"
286+
287+
message += """
288+
If you want, you can disable some notifications in mytonctrl by the <a href="https://docs.ton.org/v3/guidelines/nodes/maintenance-guidelines/mytonctrl-private-alerting#endisbling-alerts"> instruction</a>.
289+
290+
Full bot documentation <a href="https://docs.ton.org/v3/guidelines/nodes/maintenance-guidelines/mytonctrl-private-alerting">here</a>.
291+
"""
292+
self.send_message(text=message, disable_web_page_preview=True)
293+
233294
def check_db_usage(self):
234295
if time.time() - self.last_db_check < 600:
235296
return
@@ -303,6 +364,7 @@ def check_adnl_connection_failed(self):
303364
utils_module = UtilitiesModule(self.ton, self.local)
304365
ok, error = utils_module.check_adnl_connection()
305366
if not ok:
367+
self.local.add_log(error, "warning")
306368
self.send_alert("adnl_connection_failed")
307369

308370
def get_myself_from_election(self, config: dict):
@@ -371,7 +433,8 @@ def check_voting(self):
371433
def check_status(self):
372434
if not self.ton.using_alert_bot():
373435
return
374-
if not self.inited:
436+
437+
if not self.inited or self.token != self.ton.local.db.get("BotToken") or self.chat_id != self.ton.local.db.get("ChatId"):
375438
self.init()
376439

377440
self.local.try_function(self.check_db_usage)
@@ -391,3 +454,4 @@ def add_console_commands(self, console):
391454
console.AddItem("disable_alert", self.disable_alert, self.local.translate("disable_alert_cmd"))
392455
console.AddItem("list_alerts", self.print_alerts, self.local.translate("list_alerts_cmd"))
393456
console.AddItem("test_alert", self.test_alert, self.local.translate("test_alert_cmd"))
457+
console.AddItem("setup_alert_bot", self.setup_alert_bot, self.local.translate("setup_alert_bot_cmd"))

modules/backups.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ def run_restore_backup(args):
5656
return run_as_root(["bash", restore_script_path] + args)
5757

5858
def restore_backup(self, args):
59-
if len(args) == 0 or len(args) > 2:
60-
color_print("{red}Bad args. Usage:{endc} restore_backup <filename> [-y]")
59+
if len(args) == 0 or len(args) > 3:
60+
color_print("{red}Bad args. Usage:{endc} restore_backup <filename> [-y] [--skip-create-backup]")
6161
return
6262
if '-y' not in args:
6363
res = input(
@@ -67,11 +67,14 @@ def restore_backup(self, args):
6767
return
6868
else:
6969
args.pop(args.index('-y'))
70-
print('Before proceeding, mtc will create a backup of current configuration.')
71-
try:
72-
self.create_backup([])
73-
except:
74-
color_print("{red}Could not create backup{endc}")
70+
if '--skip-create-backup' in args:
71+
args.pop(args.index('--skip-create-backup'))
72+
else:
73+
print('Before proceeding, mtc will create a backup of current configuration.')
74+
try:
75+
self.create_backup([])
76+
except:
77+
color_print("{red}Could not create backup{endc}")
7578

7679
ip = str(ip2int(get_own_ip()))
7780
command_args = ["-m", self.ton.local.buffer.my_work_dir, "-n", args[0], "-i", ip]

modules/utilities.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,15 +355,15 @@ def check_adnl_connection(self):
355355
response = requests.post(url, json=data, timeout=5).json()
356356
except Exception as e:
357357
ok = False
358-
error = f'{{red}}Failed to check ADNL connection to local node: {type(e)}: {e}{{endc}}'
358+
error = f'Failed to check ADNL connection to local node: {type(e)}: {e}'
359359
continue
360360
result = response.get("ok")
361361
if result:
362362
ok = True
363363
break
364364
if not result:
365365
ok = False
366-
error = f'{{red}}Failed to check ADNL connection to local node: {response.get("message")}{{endc}}'
366+
error = f'Failed to check ADNL connection to local node: {response.get("message")}'
367367
return ok, error
368368

369369
def get_pool_data(self, args):

mytoncore/mytoncore.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,24 @@ def CheckConfigFile(self, fift, liteClient):
117117
subprocess.run(args)
118118
self.dbFile = mconfig_path
119119
self.Refresh()
120-
elif os.path.isfile(backup_path) == False:
121-
self.local.add_log("Create backup config file", "info")
122-
args = ["cp", mconfig_path, backup_path]
123-
subprocess.run(args)
120+
elif not os.path.isfile(backup_path) or time.time() - os.path.getmtime(backup_path) > 3600:
121+
self.local.try_function(self.create_self_db_backup)
124122
#end define
125123

124+
def create_self_db_backup(self):
125+
self.local.add_log("Create backup config file", "info")
126+
mconfig_path = self.local.buffer.db_path
127+
backup_path = mconfig_path + ".backup"
128+
backup_tmp_path = backup_path + '.tmp'
129+
subprocess.run(["cp", mconfig_path, backup_tmp_path])
130+
try:
131+
with open(backup_tmp_path, "r") as file:
132+
json.load(file)
133+
os.rename(backup_tmp_path, backup_path) # atomic opetation
134+
except:
135+
self.local.add_log("Could not update backup, backup_tmp file is broken", "warning")
136+
os.remove(backup_tmp_path)
137+
126138
def GetVarFromWorkerOutput(self, text, search):
127139
if ':' not in search:
128140
search += ':'
@@ -3037,6 +3049,7 @@ def SetSettings(self, name, data):
30373049
except: pass
30383050
self.local.db[name] = data
30393051
self.local.save()
3052+
self.create_self_db_backup()
30403053
#end define
30413054

30423055
def migrate_to_modes(self):

mytonctrl/mytonctrl.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ def check_adnl(local, ton):
488488
utils_module = UtilitiesModule(ton, local)
489489
ok, error = utils_module.check_adnl_connection()
490490
if not ok:
491+
error = "{red}" + error + "{endc}"
491492
print_warning(local, error)
492493
#end define
493494

@@ -728,7 +729,9 @@ def PrintLocalStatus(local, ton, adnlAddr, validatorIndex, validatorEfficiency,
728729
validatorStatus_color = GetColorStatus(validatorStatus_bool)
729730
mytoncoreStatus_text = local.translate("local_status_mytoncore_status").format(mytoncoreStatus_color, mytoncoreUptime_text)
730731
validatorStatus_text = local.translate("local_status_validator_status").format(validatorStatus_color, validatorUptime_text)
731-
validator_out_of_sync_text = local.translate("local_status_validator_out_of_sync").format(GetColorInt(validator_status.out_of_sync, 20, logic="less", ending=" s"))
732+
validator_out_of_sync_text = local.translate("local_status_validator_out_of_sync").format(GetColorInt(validator_status.out_of_sync, 20, logic="less"))
733+
master_out_of_sync_text = local.translate("local_status_master_out_of_sync").format(GetColorInt(validator_status.masterchain_out_of_sync, 20, logic="less", ending=" sec"))
734+
shard_out_of_sync_text = local.translate("local_status_shard_out_of_sync").format(GetColorInt(validator_status.shardchain_out_of_sync, 5, logic="less", ending=" blocks"))
732735

733736
validator_out_of_ser_text = local.translate("local_status_validator_out_of_ser").format(f'{validator_status.out_of_ser} blocks ago')
734737

@@ -776,6 +779,8 @@ def PrintLocalStatus(local, ton, adnlAddr, validatorIndex, validatorEfficiency,
776779
if not is_node_remote:
777780
print(validatorStatus_text)
778781
print(validator_out_of_sync_text)
782+
print(master_out_of_sync_text)
783+
print(shard_out_of_sync_text)
779784
print(validator_out_of_ser_text)
780785
print(dbStatus_text)
781786
print(mtcVersion_text)
@@ -879,7 +884,7 @@ def GetSettings(ton, args):
879884
print(json.dumps(result, indent=2))
880885
#end define
881886

882-
def SetSettings(ton, args):
887+
def SetSettings(local, ton, args):
883888
try:
884889
name = args[0]
885890
value = args[1]

mytonctrl/resources/translate.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,16 @@
324324
"ru": "Рассинхронизация локального валидатора: {0}",
325325
"zh_TW": "本地驗證者不同步: {0}"
326326
},
327+
"local_status_master_out_of_sync": {
328+
"en": "Masterchain out of sync: {0}",
329+
"ru": "Рассинхронизация Мастерчейна локального валидатора: {0}",
330+
"zh_TW": "主鏈不同步: {0}"
331+
},
332+
"local_status_shard_out_of_sync": {
333+
"en": "Shardchain out of sync: {0}",
334+
"ru": "Рассинхронизация Шардчейна локального валидатора: {0}",
335+
"zh_TW": "分片鏈不同步: {0}"
336+
},
327337
"local_status_validator_out_of_ser": {
328338
"en": "Local validator last state serialization: {0}",
329339
"ru": "Серализация стейта локального валидатора была: {0}",
@@ -489,6 +499,11 @@
489499
"ru": "Отправить тестовое оповещение через Telegram Bot",
490500
"zh_TW": "通過 Telegram Bot 發送測試警報"
491501
},
502+
"setup_alert_bot_cmd": {
503+
"en": "Setup Telegram Bot for alerts",
504+
"ru": "Настроить Telegram Bot для оповещений",
505+
"zh_TW": "設置 Telegram Bot 以接收警報"
506+
},
492507
"benchmark_cmd": {
493508
"en": "Run benchmark",
494509
"ru": "Запустить бенчмарк",

0 commit comments

Comments
 (0)