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
|