|
|
|
@ -2,15 +2,16 @@ from collections import defaultdict |
|
|
|
|
import datetime |
|
|
|
|
import json |
|
|
|
|
import os |
|
|
|
|
from typing import Dict |
|
|
|
|
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, \ |
|
|
|
|
from ceo_common.interfaces import ICloudResourceManager, IUser, \ |
|
|
|
|
ILDAPService, IMailService, IKubernetesService, IVHostManager, \ |
|
|
|
|
ICloudStackService |
|
|
|
|
ICloudStackService, IContainerRegistryService |
|
|
|
|
from ceo_common.model import Term |
|
|
|
|
import ceo_common.utils as utils |
|
|
|
|
|
|
|
|
@ -26,79 +27,133 @@ class CloudResourceManager: |
|
|
|
|
self.pending_deletions_file = \ |
|
|
|
|
os.path.join(state_dir, 'pending_account_deletions.json') |
|
|
|
|
|
|
|
|
|
def purge_accounts(self) -> Dict: |
|
|
|
|
accounts_deleted = [] |
|
|
|
|
accounts_to_be_deleted = [] |
|
|
|
|
result = { |
|
|
|
|
'accounts_deleted': accounts_deleted, |
|
|
|
|
'accounts_to_be_deleted': accounts_to_be_deleted, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
current_term = Term.current() |
|
|
|
|
beginning_of_term = current_term.to_datetime() |
|
|
|
|
now = utils.get_current_datetime() |
|
|
|
|
delta = now - beginning_of_term |
|
|
|
|
if delta.days < 30: |
|
|
|
|
# one-month grace period |
|
|
|
|
return result |
|
|
|
|
@staticmethod |
|
|
|
|
def _should_not_have_resources_deleted(user: IUser) -> bool: |
|
|
|
|
return not user.is_member() or user.membership_is_valid() |
|
|
|
|
|
|
|
|
|
ldap_srv = component.getUtility(ILDAPService) |
|
|
|
|
mail_srv = component.getUtility(IMailService) |
|
|
|
|
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': []}) |
|
|
|
|
|
|
|
|
|
# get a list of all cloud services each user is using |
|
|
|
|
accounts = defaultdict(list) |
|
|
|
|
cloudstack_accounts = cloudstack_srv.get_accounts() |
|
|
|
|
# note that cloudstack_accounts is a dict, not a list |
|
|
|
|
for username in cloudstack_accounts: |
|
|
|
|
accounts[username].append('cloudstack') |
|
|
|
|
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].append('vhost') |
|
|
|
|
accounts[username]['resources'].append('vhost') |
|
|
|
|
|
|
|
|
|
k8s_accounts = k8s_srv.get_accounts() |
|
|
|
|
for username in k8s_accounts: |
|
|
|
|
accounts[username].append('k8s') |
|
|
|
|
|
|
|
|
|
if os.path.isfile(self.pending_deletions_file): |
|
|
|
|
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 result |
|
|
|
|
for username in state['accounts_to_be_deleted']: |
|
|
|
|
if username not in accounts: |
|
|
|
|
continue |
|
|
|
|
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) |
|
|
|
|
if user.membership_is_valid(): |
|
|
|
|
continue |
|
|
|
|
services = accounts[username] |
|
|
|
|
if 'cloudstack' in services: |
|
|
|
|
account_id = cloudstack_accounts[username] |
|
|
|
|
cloudstack_srv.delete_account(account_id) |
|
|
|
|
if 'vhost' in services: |
|
|
|
|
vhost_mgr.delete_all_vhosts_for_user(username) |
|
|
|
|
if 'k8s' in services: |
|
|
|
|
k8s_srv.delete_account(username) |
|
|
|
|
accounts_deleted.append(username) |
|
|
|
|
mail_srv.send_cloud_account_has_been_deleted_message(user) |
|
|
|
|
logger.info(f'Deleted cloud resources for {username}') |
|
|
|
|
os.unlink(self.pending_deletions_file) |
|
|
|
|
return result |
|
|
|
|
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: |
|
|
|
|
user = ldap_srv.get_user(username) |
|
|
|
|
if user.membership_is_valid(): |
|
|
|
|
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) |
|
|
|
@ -108,4 +163,30 @@ class CloudResourceManager: |
|
|
|
|
) |
|
|
|
|
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 |
|
|
|
|