pyceo/ceod/model/CloudResourceManager.py

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(username, 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