vpn-btcpay-provisioner/scripts/subscription_checker.py

224 lines
8.7 KiB
Python
Raw Normal View History

2024-12-13 09:57:12 +00:00
import logging
import subprocess
import os
import tempfile
from pathlib import Path
2024-12-30 06:03:07 +00:00
from datetime import datetime, timedelta
from utils.db.operations import DatabaseManager
from utils.db.models import SubscriptionStatus
from app.handlers.payment_handler import BTCPayHandler
2024-12-13 09:57:12 +00:00
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Path setup
SCRIPT_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = SCRIPT_DIR.parent
CLEANUP_PLAYBOOK = PROJECT_ROOT / 'ansible' / 'playbooks' / 'vpn_cleanup.yml'
INVENTORY_FILE = PROJECT_ROOT / 'inventory.ini'
# Notification thresholds configuration
NOTIFICATION_THRESHOLDS = {
2024-12-30 06:03:07 +00:00
'minimum_duration': timedelta(hours=1), # Minimum subscription duration
2024-12-13 09:57:12 +00:00
'short_term': {
2024-12-30 06:03:07 +00:00
'max_duration': timedelta(days=1),
2024-12-13 09:57:12 +00:00
'warning_fraction': 0.5, # Warn when 50% of time remains
'grace_fraction': 0.1 # Grace period of 10% of subscription length
},
'medium_term': {
2024-12-30 06:03:07 +00:00
'max_duration': timedelta(days=7),
2024-12-13 09:57:12 +00:00
'warning_fraction': 0.25, # Warn when 25% of time remains
'grace_hours': 12 # Fixed 12-hour grace period
},
'long_term': {
'warning_days': 7, # 7 days warning for longer subscriptions
'grace_days': 2 # 2 days grace period
}
}
2024-12-30 06:03:07 +00:00
def run_cleanup_playbook(subscription_id):
2024-12-13 09:57:12 +00:00
"""Run the VPN cleanup playbook"""
2024-12-30 06:03:07 +00:00
logger.info(f"Running cleanup playbook for subscription {subscription_id}")
2024-12-13 09:57:12 +00:00
vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD', '')
if not vault_pass:
raise Exception("Vault password not found in environment variables")
with tempfile.NamedTemporaryFile(mode='w', delete=False) as vault_pass_file:
vault_pass_file.write(vault_pass)
vault_pass_file.flush()
cmd = [
'ansible-playbook',
str(CLEANUP_PLAYBOOK),
'-i', str(INVENTORY_FILE),
2024-12-30 06:03:07 +00:00
'-e', f'subscription_id={subscription_id}',
2024-12-13 09:57:12 +00:00
'--vault-password-file', vault_pass_file.name,
'-vvv'
]
logger.debug(f"Running command: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True
)
if result.returncode != 0:
logger.error(f"Cleanup playbook failed: {result.stderr}")
else:
logger.debug(f"Cleanup playbook output: {result.stdout}")
os.unlink(vault_pass_file.name)
return result
def calculate_notification_times(start_time, end_time):
"""
Calculate warning and grace period times based on subscription duration
Returns:
tuple: (warning_time, grace_end_time)
"""
duration = end_time - start_time
# Handle extremely short subscriptions
if duration < NOTIFICATION_THRESHOLDS['minimum_duration']:
warning_time = start_time # Immediate warning
grace_end_time = end_time # No grace period
return warning_time, grace_end_time
# Short-term subscriptions (less than 1 day)
if duration <= NOTIFICATION_THRESHOLDS['short_term']['max_duration']:
warning_delta = duration * NOTIFICATION_THRESHOLDS['short_term']['warning_fraction']
grace_delta = duration * NOTIFICATION_THRESHOLDS['short_term']['grace_fraction']
warning_time = end_time - warning_delta
grace_end_time = end_time + grace_delta
# Medium-term subscriptions (1-7 days)
elif duration <= NOTIFICATION_THRESHOLDS['medium_term']['max_duration']:
warning_delta = duration * NOTIFICATION_THRESHOLDS['medium_term']['warning_fraction']
grace_hours = NOTIFICATION_THRESHOLDS['medium_term']['grace_hours']
warning_time = end_time - warning_delta
2024-12-30 06:03:07 +00:00
grace_end_time = end_time + timedelta(hours=grace_hours)
2024-12-13 09:57:12 +00:00
# Long-term subscriptions (> 7 days)
else:
warning_days = NOTIFICATION_THRESHOLDS['long_term']['warning_days']
grace_days = NOTIFICATION_THRESHOLDS['long_term']['grace_days']
2024-12-30 06:03:07 +00:00
warning_time = end_time - timedelta(days=warning_days)
grace_end_time = end_time + timedelta(days=grace_days)
2024-12-13 09:57:12 +00:00
return warning_time, grace_end_time
2024-12-30 06:03:07 +00:00
def get_notification_message(subscription, remaining_time):
2024-12-13 09:57:12 +00:00
"""Generate appropriate notification message based on subscription duration"""
2024-12-30 06:03:07 +00:00
if remaining_time < timedelta(hours=1):
2024-12-13 09:57:12 +00:00
return f"Your VPN subscription expires in {int(remaining_time.total_seconds() / 60)} minutes!"
2024-12-30 06:03:07 +00:00
elif remaining_time < timedelta(days=1):
2024-12-13 09:57:12 +00:00
return f"Your VPN subscription expires in {int(remaining_time.total_seconds() / 3600)} hours!"
else:
return f"Your VPN subscription expires in {remaining_time.days} days!"
2024-12-30 06:03:07 +00:00
def notify_user(subscription, message):
2024-12-13 09:57:12 +00:00
"""Send notification to user about subscription status"""
try:
2024-12-30 06:03:07 +00:00
if not subscription.user or not subscription.user.email:
logger.error(f"No email found for subscription {subscription.id}")
2024-12-13 09:57:12 +00:00
return False
btcpay_handler = BTCPayHandler()
email_sent = btcpay_handler.send_confirmation_email(
2024-12-30 06:03:07 +00:00
subscription.user.email,
2024-12-13 09:57:12 +00:00
f"""
VPN Subscription Update
{message}
If you wish to continue using the VPN service, please renew your subscription.
"""
)
if email_sent:
2024-12-30 06:03:07 +00:00
logger.info(f"Sent notification to {subscription.user.email}: {message}")
2024-12-13 09:57:12 +00:00
else:
2024-12-30 06:03:07 +00:00
logger.warning(f"Failed to send notification to {subscription.user.email}")
2024-12-13 09:57:12 +00:00
return email_sent
except Exception as e:
logger.error(f"Error sending notification: {str(e)}")
return False
def check_subscriptions():
"""Check subscription status and clean up expired ones"""
logger.info("Starting subscription check")
2024-12-30 06:03:07 +00:00
try:
active_subscriptions = DatabaseManager.get_active_subscriptions()
logger.info(f"Checking {len(active_subscriptions)} active subscriptions")
now = datetime.utcnow()
for subscription in active_subscriptions:
try:
logger.debug(f"Processing subscription {subscription.id}")
2024-12-13 09:57:12 +00:00
2024-12-30 06:03:07 +00:00
warning_time, grace_end_time = calculate_notification_times(
subscription.start_time,
subscription.expiry_time
)
2024-12-13 09:57:12 +00:00
2024-12-30 06:03:07 +00:00
# Calculate remaining time
remaining_time = subscription.expiry_time - now
logger.debug(f"Subscription {subscription.id} has {remaining_time} remaining")
2024-12-13 09:57:12 +00:00
2024-12-30 06:03:07 +00:00
# Handle warnings
if now >= warning_time and not subscription.warning_sent:
message = get_notification_message(subscription, remaining_time)
logger.info(f"Sending notification for subscription {subscription.id}: {message}")
if notify_user(subscription, message):
DatabaseManager.update_warning_sent(subscription.id)
2024-12-13 09:57:12 +00:00
2024-12-30 06:03:07 +00:00
# Handle expiration
if now >= grace_end_time:
logger.info(f"Processing expiration for subscription {subscription.id}")
2024-12-13 09:57:12 +00:00
2024-12-30 06:03:07 +00:00
try:
result = run_cleanup_playbook(subscription.invoice_id)
2024-12-13 09:57:12 +00:00
2024-12-30 06:03:07 +00:00
if result.returncode == 0:
DatabaseManager.expire_subscription(subscription.id)
logger.info(f"Successfully cleaned up subscription {subscription.id}")
# Send final notification
notify_user(subscription, "Your VPN subscription has expired and been deactivated.")
else:
logger.error(f"Cleanup failed: {result.stderr}")
except Exception as e:
logger.error(f"Error during cleanup: {str(e)}")
logger.error(traceback.format_exc())
except Exception as e:
logger.error(f"Error processing subscription {subscription.id}: {str(e)}")
logger.error(traceback.format_exc())
continue
logger.info("Subscription check completed")
except Exception as e:
logger.error(f"Subscription checker failed: {str(e)}")
logger.error(traceback.format_exc())
raise
2024-12-13 09:57:12 +00:00
if __name__ == '__main__':
try:
check_subscriptions()
except Exception as e:
logger.error(f"Subscription checker failed: {str(e)}")
raise