""" Copyright 2016 Platform9 Systems Inc.(http://www.platform9.com) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from datetime import datetime, timedelta from leasehandler import get_lease_handler from persistence import DbPersistence from eventlet.greenthread import spawn_after import logging from leasehandler.constants import SUCCESS_OK, ERR_UNKNOWN, ERR_NOT_FOUND logger = logging.getLogger(__name__) def get_tenant_lease_data(data): """ Simple function to transform tenant database proxy object into an externally consumable dictionary. :param data: database row object """ return {'vm_lease_policy': {'tenant_uuid': data['tenant_uuid'], 'expiry_mins': data['expiry_mins'], 'created_at': data['created_at'], 'created_by': data['created_by'], 'updated_at': data['updated_at'], 'updated_by': data['updated_by']}} def get_vm_lease_data(data): """ Simple function to transform instance database proxy object into an externally consumable dictionary. :param data: database row object """ return {'instance_uuid': data['instance_uuid'], 'tenant_uuid': data['tenant_uuid'], 'expiry': data['expiry'], 'created_at': data['created_at'], 'created_by': data['created_by'], 'updated_at': data['updated_at'], 'updated_by': data['updated_by']} class LeaseManager: """ Lease Manager is the main class for mors dealing with CRUD operations for the REST API as well as the actual deletion of the Instances. Instance deletion and discovery is achieved through an object 'leasehandler'. """ def __init__(self, conf): self.domain_mgr = DbPersistence(conf.get("DEFAULT", "db_conn")) self.lease_handler = get_lease_handler(conf) self.sleep_seconds = conf.getint("DEFAULT", "sleep_seconds") def add_tenant_lease(self, context, tenant_obj): logger.info("Adding tenant lease %s", tenant_obj) self.domain_mgr.add_tenant_lease( tenant_obj['tenant_uuid'], tenant_obj['expiry_mins'], context.user_id, datetime.utcnow()) def update_tenant_lease(self, context, tenant_obj): logger.info("Update tenant lease %s", tenant_obj) self.domain_mgr.update_tenant_lease( tenant_obj['tenant_uuid'], tenant_obj['expiry_mins'], context.user_id, datetime.utcnow()) def delete_tenant_lease(self, context, tenant_id): logger.info("Delete tenant lease %s", tenant_id) return self.domain_mgr.delete_tenant_lease(tenant_id) def get_tenant_leases(self, context): logger.debug("Getting all tenant lease") all_tenants = self.domain_mgr.get_all_tenant_leases() all_tenants = map(lambda x: get_tenant_lease_data(x), all_tenants) logger.debug("Getting all tenant lease %s", all_tenants) return all_tenants def get_tenant_lease(self, context, tenant_id): data = self.domain_mgr.get_tenant_lease(tenant_id) logger.debug("Getting tenant lease %s", data) if data: return get_tenant_lease_data(data) return {} def get_tenant_and_associated_instance_leases(self, context, tenant_uuid): logger.debug("Getting tenant and instances leases %s", tenant_uuid) return { 'tenant_lease': self.get_tenant_lease(context, tenant_uuid), 'all_vms': map(lambda x: get_vm_lease_data(x), self.domain_mgr.get_instance_leases_by_tenant(tenant_uuid)) } def check_instance_lease_violation(self, instance_lease, tenant_lease): violation = False expiry = instance_lease['expiry'] if (datetime.utcnow() + timedelta(seconds=tenant_lease['expiry_mins']*60)) < expiry: violation = True return violation def get_instance_lease(self, context, instance_id): data = self.domain_mgr.get_instance_lease(instance_id) if data: data = get_vm_lease_data(data) logger.debug("Get instance lease %s %s", instance_id, data) return data def add_instance_lease(self, context, tenant_uuid, instance_lease_obj): logger.info("Add instance lease %s", instance_lease_obj) tenant_lease = self.domain_mgr.get_tenant_lease(tenant_uuid) if not self.check_instance_lease_violation(instance_lease_obj, tenant_lease): self.domain_mgr.add_instance_lease(instance_lease_obj['instance_uuid'], tenant_uuid, instance_lease_obj['expiry'], context.user_id, datetime.utcnow()) else: raise ValueError("Instance lease exceeds tenant lease") def update_instance_lease(self, context, tenant_uuid, instance_lease_obj): logger.info("Update instance lease %s", instance_lease_obj) tenant_lease = self.domain_mgr.get_tenant_lease(tenant_uuid) if not self.check_instance_lease_violation(instance_lease_obj, tenant_lease): self.domain_mgr.update_instance_lease(instance_lease_obj['instance_uuid'], tenant_uuid, instance_lease_obj['expiry'], context.user_id, datetime.utcnow()) else: raise ValueError("Instance lease exceeds tenant lease") def delete_instance_lease(self, context, instance_uuid): logger.info("Delete instance lease %s", instance_uuid) self.domain_mgr.delete_instance_leases([instance_uuid]) def start(self): spawn_after(self.sleep_seconds, self.run) # Could have used a generator here, would save memory but wonder if it is a good idea given the error conditions # This is a simple implementation which goes and deletes VMs one by one def _get_vms_to_delete_for_tenant(self, tenant_uuid, expiry_mins): vms_to_delete = [] vm_ids_to_delete = set() do_not_delete = set() now = datetime.utcnow() add_seconds = timedelta(seconds=expiry_mins*60) instance_leases = self.get_tenant_and_associated_instance_leases(None, tenant_uuid)['all_vms'] for i_lease in instance_leases: if now > i_lease['expiry']: logger.info("Explicit lease for %s queueing for deletion", i_lease['instance_uuid']) vms_to_delete.append(i_lease) vm_ids_to_delete.add(i_lease['instance_uuid']) else: do_not_delete.add(i_lease['instance_uuid']) logger.debug("Ignoring vm, vm not expired yet %s", i_lease['instance_uuid']) tenant_vms = self.lease_handler.get_all_vms(tenant_uuid) for vm in tenant_vms: expiry_date = vm['created_at'] + add_seconds if now > expiry_date and vm['instance_uuid'] not in vm_ids_to_delete \ and vm['instance_uuid'] not in do_not_delete: logger.info("Instance %s queued up for deletion creation date %s", vm['instance_uuid'], vm['created_at']) vms_to_delete.append(vm) else: logger.debug("Ignoring vm, vm not expired yet or already deleted %s, %s", vm['instance_uuid'], vm['created_at']) return vms_to_delete def _delete_vms_for_tenant(self, t_lease): tenant_vms_to_delete = self._get_vms_to_delete_for_tenant(t_lease['tenant_uuid'], t_lease['expiry_mins']) # Keep it simple and delete them serially result = self.lease_handler.delete_vms(tenant_vms_to_delete) remove_from_db = [] for vm_result in result.items(): # If either the VM has been successfully deleted or has already been deleted # Remove from our database if vm_result[1] == SUCCESS_OK or vm_result[1] == ERR_NOT_FOUND: remove_from_db.append(vm_result[0]) if len(remove_from_db) > 0: logger.info("Removing vms %s from db", remove_from_db) self.domain_mgr.delete_instance_leases(remove_from_db) def run(self): # Delete the cleanup tenant_leases = self.domain_mgr.get_all_tenant_leases() for t_lease in tenant_leases: self._delete_vms_for_tenant(t_lease) # Sleep again for sleep_seconds spawn_after(self.sleep_seconds, self.run)