pyceo/ceod/model/CloudResourceManager.py

193 lines
7.0 KiB
Python
Raw Normal View History

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