193 lines
7.0 KiB
Python
193 lines
7.0 KiB
Python
from collections import defaultdict
|
|
import datetime
|
|
import json
|
|
import os
|
|
from typing import Dict, List
|
|
|
|
from zope import component
|
|
from zope.interface import implementer
|
|
|
|
from ceo_common.errors import UserNotFoundError
|
|
from ceo_common.logger_factory import logger_factory
|
|
from ceo_common.interfaces import ICloudResourceManager, IUser, \
|
|
ILDAPService, IMailService, IKubernetesService, IVHostManager, \
|
|
ICloudStackService, IContainerRegistryService
|
|
from ceo_common.model import Term
|
|
import ceo_common.utils as utils
|
|
|
|
logger = logger_factory(__name__)
|
|
|
|
|
|
@implementer(ICloudResourceManager)
|
|
class CloudResourceManager:
|
|
def __init__(self):
|
|
state_dir = '/run/ceod'
|
|
if not os.path.isdir(state_dir):
|
|
os.mkdir(state_dir)
|
|
self.pending_deletions_file = \
|
|
os.path.join(state_dir, 'pending_account_deletions.json')
|
|
|
|
@staticmethod
|
|
def _should_not_have_resources_deleted(user: IUser) -> bool:
|
|
return not user.is_member() or user.membership_is_valid()
|
|
|
|
def _get_resources_for_each_user(self) -> Dict[str, Dict]:
|
|
"""
|
|
Get a list of cloud resources each user is using.
|
|
The returned dict looks like
|
|
{
|
|
"ctdalek": {
|
|
"resources": ["cloudstack", "k8s", ...],
|
|
"cloudstack_account_id": "3452345-2453245-23453..."
|
|
},
|
|
...
|
|
}
|
|
The "cloudstack_account_id" key will only be present if the user
|
|
has a CloudStack account.
|
|
"""
|
|
k8s_srv = component.getUtility(IKubernetesService)
|
|
vhost_mgr = component.getUtility(IVHostManager)
|
|
cloudstack_srv = component.getUtility(ICloudStackService)
|
|
reg_srv = component.getUtility(IContainerRegistryService)
|
|
|
|
accounts = defaultdict(lambda: {'resources': []})
|
|
|
|
cloudstack_accounts = cloudstack_srv.get_accounts()
|
|
# note that cloudstack_accounts is a dict, not a list
|
|
for username, account_id in cloudstack_accounts.items():
|
|
accounts[username]['resources'].append('cloudstack')
|
|
accounts[username]['cloudstack_account_id'] = account_id
|
|
|
|
vhost_accounts = vhost_mgr.get_accounts()
|
|
for username in vhost_accounts:
|
|
accounts[username]['resources'].append('vhost')
|
|
|
|
k8s_accounts = k8s_srv.get_accounts()
|
|
for username in k8s_accounts:
|
|
accounts[username]['resources'].append('k8s')
|
|
|
|
reg_accounts = reg_srv.get_accounts()
|
|
for username in reg_accounts:
|
|
accounts[username]['resources'].append('registry')
|
|
|
|
return accounts
|
|
|
|
def _perform_deletions(
|
|
self,
|
|
state: Dict,
|
|
accounts: Dict[str, Dict],
|
|
accounts_deleted: List[str],
|
|
):
|
|
ldap_srv = component.getUtility(ILDAPService)
|
|
mail_srv = component.getUtility(IMailService)
|
|
k8s_srv = component.getUtility(IKubernetesService)
|
|
vhost_mgr = component.getUtility(IVHostManager)
|
|
cloudstack_srv = component.getUtility(ICloudStackService)
|
|
reg_srv = component.getUtility(IContainerRegistryService)
|
|
|
|
for username in state['accounts_to_be_deleted']:
|
|
if username not in accounts:
|
|
continue
|
|
try:
|
|
user = ldap_srv.get_user(username)
|
|
except UserNotFoundError:
|
|
continue
|
|
if self._should_not_have_resources_deleted(user):
|
|
continue
|
|
resources = accounts[username]['resources']
|
|
if 'cloudstack' in resources:
|
|
account_id = accounts[username]['cloudstack_account_id']
|
|
cloudstack_srv.delete_account(account_id)
|
|
if 'vhost' in resources:
|
|
vhost_mgr.delete_all_vhosts_for_user(username)
|
|
if 'k8s' in resources:
|
|
k8s_srv.delete_account(username)
|
|
if 'registry' in resources:
|
|
reg_srv.delete_project_for_user(username)
|
|
accounts_deleted.append(username)
|
|
mail_srv.send_cloud_account_has_been_deleted_message(user)
|
|
logger.info(f'Deleted cloud resources for {username}')
|
|
|
|
def _perform_deletions_if_warning_period_passed(
|
|
self,
|
|
now: datetime.datetime,
|
|
accounts: Dict[str, Dict],
|
|
accounts_deleted: List[str],
|
|
accounts_to_be_deleted: List[str],
|
|
):
|
|
state = json.load(open(self.pending_deletions_file))
|
|
last_check = datetime.datetime.fromtimestamp(state['timestamp'])
|
|
delta = now - last_check
|
|
if delta.days < 7:
|
|
logger.debug(
|
|
'Skipping account purge because less than one week has '
|
|
'passed since the warning emails were sent out'
|
|
)
|
|
accounts_to_be_deleted.extend(state['accounts_to_be_deleted'])
|
|
return
|
|
self._perform_deletions(state, accounts, accounts_deleted)
|
|
os.unlink(self.pending_deletions_file)
|
|
|
|
def _in_grace_period(self, now: datetime.datetime) -> bool:
|
|
current_term = Term.current()
|
|
beginning_of_term = current_term.to_datetime()
|
|
delta = now - beginning_of_term
|
|
# one-month grace period
|
|
return delta.days < 30
|
|
|
|
def _send_out_warning_emails(
|
|
self,
|
|
now: datetime.datetime,
|
|
accounts: Dict[str, dict],
|
|
accounts_to_be_deleted: List[str],
|
|
):
|
|
ldap_srv = component.getUtility(ILDAPService)
|
|
mail_srv = component.getUtility(IMailService)
|
|
state = {
|
|
'timestamp': int(now.timestamp()),
|
|
'accounts_to_be_deleted': accounts_to_be_deleted,
|
|
}
|
|
for username in accounts:
|
|
try:
|
|
user = ldap_srv.get_user(username)
|
|
except UserNotFoundError:
|
|
logger.warning(f'User {username} not found')
|
|
continue
|
|
if self._should_not_have_resources_deleted(user):
|
|
continue
|
|
accounts_to_be_deleted.append(username)
|
|
mail_srv.send_cloud_account_will_be_deleted_message(user)
|
|
logger.info(
|
|
f'A warning email was sent to {username} because their '
|
|
'cloud account will be deleted'
|
|
)
|
|
if accounts_to_be_deleted:
|
|
json.dump(state, open(self.pending_deletions_file, 'w'))
|
|
|
|
def _warning_emails_were_sent_out(self) -> bool:
|
|
return os.path.isfile(self.pending_deletions_file)
|
|
|
|
def purge_accounts(self) -> Dict:
|
|
accounts_deleted = []
|
|
accounts_to_be_deleted = []
|
|
result = {
|
|
'accounts_deleted': accounts_deleted,
|
|
'accounts_to_be_deleted': accounts_to_be_deleted,
|
|
}
|
|
|
|
now = utils.get_current_datetime()
|
|
if self._in_grace_period(now):
|
|
return result
|
|
|
|
# get a list of all cloud services each user is using
|
|
accounts = self._get_resources_for_each_user()
|
|
|
|
if self._warning_emails_were_sent_out():
|
|
self._perform_deletions_if_warning_period_passed(
|
|
now, accounts, accounts_deleted, accounts_to_be_deleted)
|
|
return result
|
|
|
|
self._send_out_warning_emails(now, accounts, accounts_to_be_deleted)
|
|
|
|
return result
|