-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathscript.py
More file actions
629 lines (539 loc) · 28.8 KB
/
script.py
File metadata and controls
629 lines (539 loc) · 28.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
vkcloud_find_and_attach_fip.py
VK Cloud (OpenStack) — бесконечно выделяет floating IP из внешней сети,
пока не попадётся IP из 95.163.248.0/22. Неподходящие адреса сразу удаляет,
подходящий привязывает к ВМ и завершает работу.
Аутентификация: username/password (Keystone v3) с авто-переавторизацией.
Поддерживает параллельную работу нескольких воркеров.
"""
import ipaddress
import os
import sys
import time
import threading
# Автоматическая загрузка переменных из .env файла
try:
from dotenv import load_dotenv
# Загружаем переменные из .env файла (если он существует)
load_dotenv()
except ImportError:
# Если python-dotenv не установлен, просто продолжаем без загрузки .env
pass
from openstack import connection
from openstack import exceptions as os_exc
from keystoneauth1 import exceptions as ks_exc
# Опциональная поддержка уведомлений через apprise
try:
from apprise import Apprise
APPRISE_AVAILABLE = True
except ImportError:
APPRISE_AVAILABLE = False
# ========= НАСТРОЙКИ ПОДКЛЮЧЕНИЯ (из переменных окружения) =========
def get_auth():
"""Получает параметры аутентификации из переменных окружения."""
auth = {
"auth_url": os.getenv("VKCLOUD_AUTH_URL", "https://infra.mail.ru:35357/v3/"),
"username": os.getenv("VKCLOUD_USERNAME"),
"password": os.getenv("VKCLOUD_PASSWORD"),
"project_id": os.getenv("VKCLOUD_PROJECT_ID"),
"user_domain_name": os.getenv("VKCLOUD_USER_DOMAIN_NAME", "users"),
"region_name": os.getenv("VKCLOUD_REGION_NAME", "RegionOne"),
"interface": os.getenv("VKCLOUD_INTERFACE", "public"),
}
# Опциональные параметры
verify = os.getenv("VKCLOUD_VERIFY")
if verify:
if verify.lower() == "false":
auth["verify"] = False
else:
auth["verify"] = verify
# Проверка обязательных параметров
required = ["username", "password", "project_id"]
missing = [k for k in required if not auth.get(k)]
if missing:
raise SystemExit(f"❌ Отсутствуют обязательные переменные окружения: {', '.join(f'VKCLOUD_{k.upper()}' for k in missing)}")
return auth
# ========= ПАРАМЕТРЫ РАБОТЫ (из переменных окружения) =========
SERVER_ID_OR_NAME = os.getenv("VKCLOUD_SERVER_ID_OR_NAME")
EXT_NET_NAME = os.getenv("VKCLOUD_EXT_NET_NAME") # None если не задано
PORT_ID = os.getenv("VKCLOUD_PORT_ID") # None если не задано
SLEEP_BETWEEN_ATTEMPTS = float(os.getenv("VKCLOUD_SLEEP_BETWEEN_ATTEMPTS", "0.6"))
ASSOC_WAIT = float(os.getenv("VKCLOUD_ASSOC_WAIT", "8.0"))
TARGET_NET_STR = os.getenv("VKCLOUD_TARGET_NET", "95.163.248.0/22")
# Поддержка нескольких подсетей через запятую
TARGET_NETS_STR_LIST = [net.strip() for net in TARGET_NET_STR.split(",") if net.strip()]
TARGET_NETS = [ipaddress.ip_network(net) for net in TARGET_NETS_STR_LIST]
WORKERS_COUNT = int(os.getenv("VKCLOUD_WORKERS_COUNT", "1"))
# Режим последовательного перебора IP внутри диапазона
SEQUENTIAL_IP_SCAN = os.getenv("VKCLOUD_SEQUENTIAL_IP_SCAN", "false").lower() == "true"
# Режим работы по расписанию (опционально)
WORK_DURATION_MINUTES = os.getenv("VKCLOUD_WORK_DURATION_MINUTES") # Время работы в минутах (None = без ограничений)
PAUSE_DURATION_MINUTES = os.getenv("VKCLOUD_PAUSE_DURATION_MINUTES") # Время паузы в минутах (None = без паузы)
# Уведомления через apprise (опционально)
APPRISE_URL = os.getenv("VKCLOUD_APPRISE_URL") # URL для уведомлений через apprise
# Глобальная переменная для остановки всех воркеров
stop_event = threading.Event()
success_lock = threading.Lock()
success_achieved = False
success_ip = None
success_worker_id = None
# Глобальные переменные для режима работы по расписанию
pause_event = threading.Event()
work_start_time = None
work_start_lock = threading.Lock()
# Глобальные переменные для последовательного перебора IP
ip_iterators = {} # Словарь итераторов для каждой подсети
ip_iterators_lock = threading.Lock()
# ========= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =========
def get_conn(auth_config=None) -> connection.Connection:
"""Создает новое соединение с OpenStack."""
if auth_config is None:
auth_config = get_auth()
conn = connection.Connection(**auth_config)
# поднимет исключение, если авторизация не удалась
conn.authorize()
return conn
def ensure_conn_alive(conn: connection.Connection) -> connection.Connection:
"""Проверяет, что токен жив; при невалидности — пересоздаёт соединение."""
try:
conn.authorize()
return conn
except (ks_exc.Unauthorized, ks_exc.NotFound):
# токен протух/невалиден — пересобираем коннект
return get_conn()
def in_target_range(ip: str) -> bool:
"""Проверяет, принадлежит ли IP одной из целевых подсетей."""
try:
ip_addr = ipaddress.ip_address(ip)
return any(ip_addr in net for net in TARGET_NETS)
except ValueError:
return False
def find_server(conn: connection.Connection, server_id_or_name: str):
srv = conn.compute.find_server(server_id_or_name, ignore_missing=True)
if not srv:
raise SystemExit(f"❌ ВМ '{server_id_or_name}' не найдена")
return conn.compute.get_server(srv.id)
def pick_port(conn: connection.Connection, server, explicit_port_id=None):
if explicit_port_id:
port = conn.network.get_port(explicit_port_id)
if not port:
raise SystemExit(f"❌ Порт '{explicit_port_id}' не найден")
if port.device_id != server.id:
raise SystemExit("❌ Указанный порт не принадлежит этой ВМ")
return port
ports = list(conn.network.ports(device_id=server.id))
if not ports:
raise SystemExit("❌ У ВМ нет сетевых портов")
# активные первыми
ports.sort(key=lambda p: (p.status != "ACTIVE", getattr(p, "created_at", "")))
return ports[0]
def find_external_network(conn: connection.Connection, name_or_id: str | None):
if name_or_id:
return conn.network.find_network(name_or_id, ignore_missing=False)
# авто-поиск первой внешней сети (router:external)
for net in conn.network.networks():
if getattr(net, "is_router_external", False):
return net
raise SystemExit("❌ Внешняя сеть не найдена. Укажите EXT_NET_NAME явно.")
def allocate_fip(conn: connection.Connection, ext_net_id: str, specific_ip: str = None):
"""Выделяет floating IP. Если указан specific_ip, пытается выделить именно этот IP."""
if specific_ip:
try:
# Пытаемся выделить конкретный IP адрес
return conn.network.create_ip(
floating_network_id=ext_net_id,
floating_ip_address=specific_ip
)
except Exception:
# Если не удалось выделить конкретный IP, возвращаем None
# Вызывающий код должен обработать это и попробовать без указания IP
return None
else:
# Обычное выделение случайного IP
return conn.network.create_ip(floating_network_id=ext_net_id)
def get_next_ip_from_networks():
"""Генерирует следующий IP адрес для перебора из целевых подсетей."""
global ip_iterators
with ip_iterators_lock:
if not ip_iterators:
# Инициализируем итераторы для каждой подсети
for net in TARGET_NETS:
# Пропускаем сетевой адрес и broadcast адрес
hosts = list(net.hosts())
if hosts:
ip_iterators[str(net)] = iter(hosts)
# Пробуем получить IP из каждой подсети по очереди
for net in TARGET_NETS:
net_str = str(net)
if net_str in ip_iterators:
try:
return str(next(ip_iterators[net_str]))
except StopIteration:
# Подсеть закончилась, перезапускаем итератор
hosts = list(net.hosts())
if hosts:
ip_iterators[net_str] = iter(hosts)
return str(next(ip_iterators[net_str]))
else:
# Подсеть пустая, удаляем итератор
del ip_iterators[net_str]
# Если все подсети закончились, перезапускаем все итераторы
ip_iterators.clear()
for net in TARGET_NETS:
hosts = list(net.hosts())
if hosts:
ip_iterators[str(net)] = iter(hosts)
# Пробуем снова
for net in TARGET_NETS:
net_str = str(net)
if net_str in ip_iterators:
try:
return str(next(ip_iterators[net_str]))
except StopIteration:
continue
return None
def associate_fip(conn: connection.Connection, fip, port):
return conn.network.update_ip(fip, port_id=port.id)
def release_fip(conn: connection.Connection, fip):
try:
conn.network.delete_ip(fip, ignore_missing=True)
except Exception as e:
print(f"⚠️ Ошибка удаления IP {getattr(fip, 'floating_ip_address', '?')}: {e}", file=sys.stderr)
def wait_for_association(conn: connection.Connection, fip_id: str, port_id: str,
timeout: float = ASSOC_WAIT, poll: float = 0.5) -> bool:
waited = 0.0
while waited < timeout:
# Проверяем, не нужно ли остановиться
if stop_event.is_set():
return False
f = conn.network.get_ip(fip_id)
if getattr(f, "port_id", None) == port_id:
return True
time.sleep(poll)
waited += poll
# Проверка после паузы
if stop_event.is_set():
return False
return False
def send_notification(title: str, body: str, notification_type: str = "info"):
"""Отправляет уведомление через apprise, если настроено."""
if not APPRISE_AVAILABLE or not APPRISE_URL:
return
try:
apobj = Apprise()
apobj.add(APPRISE_URL)
# Определяем приоритет и иконку в зависимости от типа
if notification_type == "success":
body = f"✅ {body}"
elif notification_type == "error":
body = f"❌ {body}"
else:
body = f"ℹ️ {body}"
apobj.notify(
body=body,
title=title,
)
except Exception as e:
print(f"⚠️ Ошибка отправки уведомления: {e}", file=sys.stderr)
# ========= ОСНОВНОЙ СЦЕНАРИЙ =========
def worker(worker_id: int, server_id_or_name: str, port_id: str, ext_net_id: str):
"""Функция воркера для параллельного поиска floating IP."""
global success_achieved, work_start_time
auth_config = get_auth()
conn = get_conn(auth_config)
print(f"[Воркер {worker_id}] 🔗 Подключен к VK Cloud")
while not stop_event.is_set():
# Проверка времени работы (если включен режим работы по расписанию)
if WORK_DURATION_MINUTES:
with work_start_lock:
if work_start_time is None:
work_start_time = time.time()
elapsed_minutes = (time.time() - work_start_time) / 60
if elapsed_minutes >= float(WORK_DURATION_MINUTES):
print(f"[Воркер {worker_id}] ⏸️ Время работы истекло ({WORK_DURATION_MINUTES} мин), ожидаю паузу...")
pause_event.set()
break
# Проверка паузы
if pause_event.is_set():
break
# Проверяем, не достиг ли успех другой воркер
with success_lock:
if success_achieved:
print(f"[Воркер {worker_id}] 🛑 Остановка: успех достигнут другим воркером")
break
# Дополнительная проверка перед началом итерации
if stop_event.is_set():
break
# Гарантируем живость токена перед каждой итерацией
conn = ensure_conn_alive(conn)
# Проверка после переавторизации
if stop_event.is_set():
break
fip = None
try:
# Проверка перед выделением IP
if stop_event.is_set():
break
# 1) выделяем floating IP
specific_ip = None
if SEQUENTIAL_IP_SCAN:
specific_ip = get_next_ip_from_networks()
if specific_ip:
# Пытаемся выделить конкретный IP
fip = allocate_fip(conn, ext_net_id, specific_ip)
if not fip:
# Не удалось выделить конкретный IP, пробуем обычным способом
fip = allocate_fip(conn, ext_net_id)
else:
# Итераторы закончились, используем обычный способ
fip = allocate_fip(conn, ext_net_id)
else:
# Обычный режим - случайный IP
fip = allocate_fip(conn, ext_net_id)
ip = getattr(fip, "floating_ip_address", None)
if not ip:
print(f"[Воркер {worker_id}] ⚠️ Получен FIP без адреса — освобождаю и повторяю…")
release_fip(conn, fip)
fip = None
time.sleep(SLEEP_BETWEEN_ATTEMPTS)
continue
if SEQUENTIAL_IP_SCAN and specific_ip:
print(f"[Воркер {worker_id}] 🔹 Выделен IP: {ip} (попытка выделить {specific_ip})")
else:
print(f"[Воркер {worker_id}] 🔹 Выделен IP: {ip}")
# Проверка после выделения IP
if stop_event.is_set():
release_fip(conn, fip)
break
# 2) проверяем диапазон
if in_target_range(ip):
# Проверка перед привязкой - может другой воркер уже успел
if stop_event.is_set():
release_fip(conn, fip)
break
target_nets_str = ", ".join(str(net) for net in TARGET_NETS)
print(f"[Воркер {worker_id}] ✅ IP {ip} принадлежит одной из подсетей ({target_nets_str}). Привязываю к порту {port_id}…")
port = conn.network.get_port(port_id)
associate_fip(conn, fip, port)
# Проверка после привязки
if stop_event.is_set():
release_fip(conn, fip)
break
# 3) ждём подтверждения привязки
if wait_for_association(conn, fip.id, port_id):
global success_ip, success_worker_id
with success_lock:
if not success_achieved:
success_achieved = True
success_ip = ip
success_worker_id = worker_id
stop_event.set()
print(f"[Воркер {worker_id}] 🎉 Привязка подтверждена. Готово!")
print(f"[Воркер {worker_id}] 📌 Итоговый IP: {ip}")
# Отправляем уведомление об успехе
send_notification(
"VK Cloud: Floating IP привязан",
f"IP {ip} успешно привязан к ВМ воркером {worker_id}",
"success"
)
else:
# Другой воркер уже успел
print(f"[Воркер {worker_id}] ⚠️ Другой воркер уже привязал IP, освобождаю…")
release_fip(conn, fip)
break
else:
print(f"[Воркер {worker_id}] ⚠️ Привязка не подтвердилась, освобождаю IP и продолжаю…")
release_fip(conn, fip)
fip = None
else:
target_nets_str = ", ".join(str(net) for net in TARGET_NETS)
print(f"[Воркер {worker_id}] ❌ IP {ip} не из целевых подсетей ({target_nets_str}), удаляю…")
release_fip(conn, fip)
fip = None
except KeyboardInterrupt:
# Пробрасываем KeyboardInterrupt наверх
raise
except (ks_exc.Unauthorized, ks_exc.NotFound) as e:
# На всякий случай, если токен «упал» посреди операций
print(f"[Воркер {worker_id}] 🔁 Токен невалиден: {e}. Переавторизация…")
try:
if fip:
release_fip(conn, fip)
finally:
conn = get_conn(auth_config)
except os_exc.HttpException as e:
print(f"[Воркер {worker_id}] ⚠️ Ошибка API (HTTP): {e}", file=sys.stderr)
if fip:
release_fip(conn, fip)
except Exception as e:
if not stop_event.is_set():
print(f"[Воркер {worker_id}] ⚠️ Неожиданная ошибка: {e}", file=sys.stderr)
if fip:
release_fip(conn, fip)
# Проверка перед паузой
if stop_event.is_set():
break
time.sleep(SLEEP_BETWEEN_ATTEMPTS)
# Проверка после паузы
if stop_event.is_set():
break
# Финальное сообщение при остановке
if stop_event.is_set() and not success_achieved:
print(f"[Воркер {worker_id}] 🛑 Остановлен")
def run_work_cycle(server_id_or_name: str, port_id: str, ext_net_id: str):
"""Запускает один цикл работы воркеров."""
global work_start_time, success_achieved, success_ip, success_worker_id, ip_iterators
# Сброс флагов для нового цикла
work_start_time = None
pause_event.clear()
stop_event.clear()
success_achieved = False
success_ip = None
success_worker_id = None
# Сброс итераторов IP при новом цикле (если включен последовательный перебор)
if SEQUENTIAL_IP_SCAN:
with ip_iterators_lock:
ip_iterators.clear()
print("🚀 Запускаю параллельный поиск подходящего floating IP…")
if SEQUENTIAL_IP_SCAN:
print(f"🔢 Режим последовательного перебора IP: включен")
if WORK_DURATION_MINUTES:
print(f"⏱️ Режим работы по расписанию: работа {WORK_DURATION_MINUTES} мин, пауза {PAUSE_DURATION_MINUTES or 0} мин")
# Запускаем воркеры
threads = []
for i in range(1, WORKERS_COUNT + 1):
t = threading.Thread(
target=worker,
args=(i, server_id_or_name, port_id, ext_net_id),
daemon=False
)
t.start()
threads.append(t)
time.sleep(0.1) # Небольшая задержка между запусками
# Ждем завершения всех воркеров
for t in threads:
t.join()
return success_achieved
def main():
global work_start_time
# Проверка обязательных параметров
if not SERVER_ID_OR_NAME:
raise SystemExit("❌ Отсутствует обязательная переменная окружения: VKCLOUD_SERVER_ID_OR_NAME")
if WORKERS_COUNT < 1:
raise SystemExit("❌ Количество воркеров должно быть >= 1")
# Проверка параметров режима работы по расписанию
if WORK_DURATION_MINUTES:
try:
work_duration = float(WORK_DURATION_MINUTES)
if work_duration <= 0:
raise SystemExit("❌ VKCLOUD_WORK_DURATION_MINUTES должно быть > 0")
except ValueError:
raise SystemExit("❌ VKCLOUD_WORK_DURATION_MINUTES должно быть числом")
if PAUSE_DURATION_MINUTES:
try:
pause_duration = float(PAUSE_DURATION_MINUTES)
if pause_duration < 0:
raise SystemExit("❌ VKCLOUD_PAUSE_DURATION_MINUTES должно быть >= 0")
except ValueError:
raise SystemExit("❌ VKCLOUD_PAUSE_DURATION_MINUTES должно быть числом")
else:
print("⚠️ Включен режим работы по расписанию, но не указана пауза. Будет бесконечный цикл работы.")
# Отправляем уведомление о старте
schedule_info = ""
if WORK_DURATION_MINUTES:
schedule_info = f" (режим работы: {WORK_DURATION_MINUTES} мин работа, {PAUSE_DURATION_MINUTES or 0} мин пауза)"
target_nets_str = ", ".join(str(net) for net in TARGET_NETS)
send_notification(
"VK Cloud: Запуск поиска Floating IP",
f"Запущено {WORKERS_COUNT} воркер(ов) для поиска IP в подсетях: {target_nets_str}{schedule_info}",
"info"
)
print("🔗 Подключаюсь к VK Cloud (password auth)…")
conn = get_conn()
# Ресурсы (получаем один раз для всех воркеров)
server = find_server(conn, SERVER_ID_OR_NAME)
port = pick_port(conn, server, PORT_ID)
ext_net = find_external_network(conn, EXT_NET_NAME)
print(f"🖥️ ВМ: {server.name} ({server.id})")
print(f"🔌 Порт: {port.id}")
print(f"🌐 Внешняя сеть: {ext_net.name} ({ext_net.id})")
if len(TARGET_NETS) == 1:
print(f"🎯 Целевая подсеть: {TARGET_NETS[0]}")
else:
print(f"🎯 Целевые подсети ({len(TARGET_NETS)}): {', '.join(str(net) for net in TARGET_NETS)}")
print(f"👷 Количество воркеров: {WORKERS_COUNT}")
try:
# Основной цикл работы
cycle_number = 1
while True:
if cycle_number > 1:
print(f"\n{'='*60}")
print(f"🔄 Цикл работы #{cycle_number}")
print(f"{'='*60}\n")
# Запускаем цикл работы
success = run_work_cycle(SERVER_ID_OR_NAME, port.id, ext_net.id)
if success:
print(f"\n✅ Успешно завершено! IP {success_ip} привязан воркером {success_worker_id}")
send_notification(
"VK Cloud: Floating IP найден и привязан",
f"IP {success_ip} успешно привязан к ВМ воркером {success_worker_id}",
"success"
)
return 0
# Если включен режим работы по расписанию и время работы истекло
if WORK_DURATION_MINUTES and pause_event.is_set():
if not PAUSE_DURATION_MINUTES or float(PAUSE_DURATION_MINUTES) == 0:
print("⚠️ Время работы истекло, но пауза не задана. Завершение работы.")
send_notification(
"VK Cloud: Время работы истекло",
f"Время работы ({WORK_DURATION_MINUTES} мин) истекло, пауза не задана. Завершение.",
"info"
)
return 1
pause_seconds = float(PAUSE_DURATION_MINUTES) * 60
print(f"\n⏸️ Пауза на {PAUSE_DURATION_MINUTES} минут...")
send_notification(
"VK Cloud: Пауза",
f"Время работы ({WORK_DURATION_MINUTES} мин) истекло. Пауза на {PAUSE_DURATION_MINUTES} мин.",
"info"
)
# Ожидание паузы с возможностью прерывания
pause_start = time.time()
while time.time() - pause_start < pause_seconds:
if stop_event.is_set():
break
time.sleep(1)
if stop_event.is_set():
break
# Сбрасываем pause_event перед новым циклом
pause_event.clear()
print(f"▶️ Пауза завершена, возобновляю работу...\n")
send_notification(
"VK Cloud: Возобновление работы",
f"Пауза завершена. Начинаю цикл работы #{cycle_number + 1}.",
"info"
)
cycle_number += 1
else:
# Если режим работы по расписанию не включен, завершаем после первого цикла
print("⚠️ Все воркеры завершились, но успех не достигнут")
send_notification(
"VK Cloud: Поиск завершен без результата",
"Все воркеры завершились, но подходящий IP не найден",
"error"
)
return 1
except KeyboardInterrupt:
print("\n🛑 Остановлено пользователем.")
stop_event.set()
pause_event.set()
send_notification(
"VK Cloud: Поиск остановлен",
"Поиск Floating IP остановлен пользователем",
"info"
)
return 2
if __name__ == "__main__":
sys.exit(main())