nordabiz/scripts/ovh_vps_monitor.py
Maciej Pienczyn 110d971dca
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
feat: migrate prod docs to OVH VPS + UTC→Warsaw timezone in all templates
Production moved from on-prem VM 249 (10.22.68.249) to OVH VPS
(57.128.200.27, inpi-vps-waw01). Updated ALL documentation, slash
commands, memory files, architecture docs, and deploy procedures.

Added |local_time Jinja filter (UTC→Europe/Warsaw) and converted
155 .strftime() calls across 71 templates so timestamps display
in Polish timezone regardless of server timezone.

Also includes: created_by_id tracking, abort import fix, ICS
calendar fix for missing end times, Pros Poland data cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:41:53 +02:00

341 lines
12 KiB
Python

#!/usr/bin/env python3
"""
OVH VPS Availability Monitor for Warsaw (WAW)
Sprawdza dostępność VPS-1, VPS-2, VPS-3, VPS-4 w Warszawie.
Powiadamia przez macOS notification + opcjonalnie Telegram.
Użycie:
python3 scripts/ovh_vps_monitor.py # jednorazowe sprawdzenie
python3 scripts/ovh_vps_monitor.py --daemon # co 10 minut
Konfiguracja OVH API (jednorazowo):
1. Wejdź na https://eu.api.ovh.com/createToken/
2. Zaloguj się kontem pm861830-ovh
3. Ustaw:
- GET /order/cart/*
- POST /order/cart/*
- DELETE /order/cart/*
4. Wpisz klucze do ~/.ovh.conf (format poniżej)
~/.ovh.conf:
[default]
endpoint=ovh-eu
application_key=TWÓJ_APP_KEY
application_secret=TWÓJ_APP_SECRET
consumer_key=TWÓJ_CONSUMER_KEY
"""
import json
import os
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
# --- Configuration ---
MODELS = {
'vps-2025-model1': 'VPS-1 (4 vCores, 8 GB, 75 GB SSD) — 23,50 PLN/m',
'vps-2025-model2': 'VPS-2 (6 vCores, 12 GB, 100 GB NVMe) — 36,21 PLN/m',
'vps-2025-model3': 'VPS-3 (8 vCores, 24 GB, 200 GB NVMe) — 72,42 PLN/m',
'vps-2025-model4': 'VPS-4 (12 vCores, 48 GB, 300 GB NVMe) — 133,96 PLN/m',
}
TARGET_DC = 'WAW'
CHECK_INTERVAL = 600 # 10 minut
STATE_FILE = Path(__file__).parent / '.ovh_vps_monitor_state.json'
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
TELEGRAM_CHAT_ID = os.environ.get('TELEGRAM_CHAT_ID', '')
def check_availability_curl():
"""Sprawdza dostępność VPS w WAW przez publiczne API OVH (cart flow)."""
import urllib.request
import urllib.error
results = {}
# 1. Utwórz koszyk
req = urllib.request.Request(
'https://eu.api.ovh.com/1.0/order/cart',
data=json.dumps({'ovhSubsidiary': 'PL'}).encode(),
headers={'Content-Type': 'application/json'},
method='POST'
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
cart = json.loads(resp.read())
cart_id = cart['cartId']
except Exception as e:
print(f'[ERROR] Nie mogę utworzyć koszyka: {e}')
return {}
# 2. Dla każdego modelu — dodaj do koszyka i sprawdź DC
for plan_code, plan_name in MODELS.items():
try:
# Dodaj VPS do koszyka
add_req = urllib.request.Request(
f'https://eu.api.ovh.com/1.0/order/cart/{cart_id}/vps',
data=json.dumps({
'planCode': plan_code,
'duration': 'P1M',
'pricingMode': 'default',
'quantity': 1
}).encode(),
headers={'Content-Type': 'application/json'},
method='POST'
)
with urllib.request.urlopen(add_req, timeout=15) as resp:
item = json.loads(resp.read())
item_id = item['itemId']
# Skonfiguruj WAW
dc_req = urllib.request.Request(
f'https://eu.api.ovh.com/1.0/order/cart/{cart_id}/item/{item_id}/configuration',
data=json.dumps({'label': 'vps_datacenter', 'value': TARGET_DC}).encode(),
headers={'Content-Type': 'application/json'},
method='POST'
)
with urllib.request.urlopen(dc_req, timeout=15) as resp:
dc_result = json.loads(resp.read())
# Skonfiguruj OS i region
for cfg in [
{'label': 'vps_os', 'value': 'Ubuntu 24.04'},
{'label': 'region', 'value': 'europe'},
]:
cfg_req = urllib.request.Request(
f'https://eu.api.ovh.com/1.0/order/cart/{cart_id}/item/{item_id}/configuration',
data=json.dumps(cfg).encode(),
headers={'Content-Type': 'application/json'},
method='POST'
)
with urllib.request.urlopen(cfg_req, timeout=15) as resp:
pass
# Sprawdź podsumowanie koszyka (publiczne, nie wymaga auth)
summary_req = urllib.request.Request(
f'https://eu.api.ovh.com/1.0/order/cart/{cart_id}/summary',
method='GET'
)
try:
with urllib.request.urlopen(summary_req, timeout=15) as resp:
summary = json.loads(resp.read())
results[plan_code] = 'available'
except urllib.error.HTTPError as e:
error_body = e.read().decode() if e.fp else ''
if 'stock' in error_body.lower() or 'unavailable' in error_body.lower():
results[plan_code] = 'out_of_stock'
elif e.code == 401:
# Auth wymagane — nie wiemy na pewno, spróbuj auth flow
results[plan_code] = 'needs_auth'
else:
results[plan_code] = 'unknown'
# Usuń item z koszyka
del_req = urllib.request.Request(
f'https://eu.api.ovh.com/1.0/order/cart/{cart_id}/item/{item_id}',
method='DELETE'
)
try:
with urllib.request.urlopen(del_req, timeout=15) as resp:
pass
except Exception:
pass
except urllib.error.HTTPError as e:
error_body = e.read().decode() if e.fp else ''
if 'stock' in error_body.lower() or 'not available' in error_body.lower():
results[plan_code] = 'out_of_stock'
else:
results[plan_code] = f'error: {e.code}'
except Exception as e:
results[plan_code] = f'error: {e}'
# Usuń koszyk
try:
del_cart = urllib.request.Request(
f'https://eu.api.ovh.com/1.0/order/cart/{cart_id}',
method='DELETE'
)
urllib.request.urlopen(del_cart, timeout=15)
except Exception:
pass
return results
def check_availability_ovh_lib():
"""Sprawdza dostępność VPS w WAW przez ovh Python library (z autoryzacją)."""
try:
import ovh
except ImportError:
print('[WARN] Brak biblioteki ovh — pip3 install ovh')
return {}
conf_path = Path.home() / '.ovh.conf'
if not conf_path.exists():
print(f'[WARN] Brak {conf_path} — użyj trybu bez autoryzacji')
return {}
try:
client = ovh.Client()
except Exception as e:
print(f'[ERROR] Nie mogę połączyć z OVH API: {e}')
return {}
results = {}
for plan_code, plan_name in MODELS.items():
try:
# Utwórz koszyk
cart = client.post('/order/cart', ovhSubsidiary='PL')
cart_id = cart['cartId']
client.post(f'/order/cart/{cart_id}/assign')
# Dodaj VPS
item = client.post(f'/order/cart/{cart_id}/vps',
planCode=plan_code, duration='P1M',
pricingMode='default', quantity=1)
item_id = item['itemId']
# Skonfiguruj WAW + OS + region
client.post(f'/order/cart/{cart_id}/item/{item_id}/configuration',
label='vps_datacenter', value=TARGET_DC)
client.post(f'/order/cart/{cart_id}/item/{item_id}/configuration',
label='vps_os', value='Ubuntu 24.04')
client.post(f'/order/cart/{cart_id}/item/{item_id}/configuration',
label='region', value='europe')
# Validate checkout (GET = validate, POST = place order)
checkout = client.get(f'/order/cart/{cart_id}/checkout')
# Jeśli doszliśmy tutaj — VPS jest dostępny w WAW!
results[plan_code] = 'available'
# Cleanup
client.delete(f'/order/cart/{cart_id}')
except Exception as e:
error_msg = str(e).lower()
if 'stock' in error_msg or 'not available' in error_msg or 'unavailable' in error_msg:
results[plan_code] = 'out_of_stock'
elif 'expired' in error_msg:
results[plan_code] = 'error_expired'
else:
results[plan_code] = f'error: {e}'
# Cleanup
try:
client.delete(f'/order/cart/{cart_id}')
except Exception:
pass
return results
def notify_macos(title, message):
"""Powiadomienie macOS."""
subprocess.run([
'osascript', '-e',
f'display notification "{message}" with title "{title}" sound name "Glass"'
], check=False)
def notify_telegram(message):
"""Powiadomienie Telegram."""
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
return
import urllib.request
url = f'https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage'
data = json.dumps({
'chat_id': TELEGRAM_CHAT_ID,
'text': message,
'parse_mode': 'Markdown'
}).encode()
req = urllib.request.Request(url, data=data,
headers={'Content-Type': 'application/json'})
try:
urllib.request.urlopen(req, timeout=10)
except Exception as e:
print(f'[WARN] Telegram notification failed: {e}')
def load_state():
"""Wczytaj poprzedni stan."""
if STATE_FILE.exists():
return json.loads(STATE_FILE.read_text())
return {}
def save_state(state):
"""Zapisz stan."""
STATE_FILE.write_text(json.dumps(state, indent=2))
def run_check():
"""Główna funkcja sprawdzająca."""
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(f'\n[{now}] Sprawdzam dostępność VPS w {TARGET_DC}...')
# Preferuj auth flow (dokładniejszy), fallback na public API
conf_path = Path.home() / '.ovh.conf'
if conf_path.exists():
print(' Tryb: OVH API (z autoryzacją)')
results = check_availability_ovh_lib()
else:
print(' Tryb: Public API (bez autoryzacji — mniej dokładny)')
results = check_availability_curl()
if not results:
print(' [WARN] Brak wyników — problem z API?')
return
prev_state = load_state()
new_available = []
for plan_code, status in results.items():
plan_name = MODELS.get(plan_code, plan_code)
icon = '' if status == 'available' else '' if 'stock' in str(status) else ''
print(f' {icon} {plan_name}: {status}')
# Czy to nowa dostępność?
prev = prev_state.get(plan_code, '')
if status == 'available' and prev != 'available':
new_available.append(plan_name)
# Powiadom o nowych dostępnościach
if new_available:
msg_lines = ['🟢 VPS dostępny w Warszawie!'] + [f'{n}' for n in new_available]
msg_lines.append(f'\n🔗 https://www.ovhcloud.com/pl/vps/')
message = '\n'.join(msg_lines)
print(f'\n 🔔 POWIADOMIENIE: {message}')
notify_macos('OVH VPS Warszawa!', '\n'.join(new_available))
notify_telegram(message)
else:
print(' Brak nowych dostępności.')
save_state(results)
def main():
daemon = '--daemon' in sys.argv
if daemon:
print(f'Uruchamiam monitoring VPS w {TARGET_DC} (co {CHECK_INTERVAL}s)...')
print(f'Stan zapisywany w: {STATE_FILE}')
print('Ctrl+C aby zatrzymać\n')
while True:
try:
run_check()
time.sleep(CHECK_INTERVAL)
except KeyboardInterrupt:
print('\nZatrzymano.')
break
else:
run_check()
if __name__ == '__main__':
main()