Source code for lease_it.backend.OpenstackConnection

#  -*- coding: utf-8 -*-
"""
This module manage interaction between application and
OpenStack cloud infrastructure
"""
import math

from datetime import date
from dateutil.relativedelta import relativedelta

from django.core.cache import cache
from django.utils.dateparse import parse_datetime

from keystoneauth1.identity import v3
from keystoneauth1 import session, exceptions as ksexceptions
from keystoneclient.v3 import client as ksclient
from novaclient import client as nvclient

from openstack_lease_it.settings import GLOBAL_CONFIG, LOGGER_INSTANCES
from lease_it.datastore import InstancesAccess, LEASE_DURATION
from lease_it.backend.Exceptions import PermissionDenied

# Define nova client version as a constant
NOVA_VERSION = 2

# Default cache timeout (in sec)
FLAVOR_CACHE_TIMEOUT = 86400
USERS_CACHE_TIMEOUT = 86400
PROJECTS_CACHE_TIMEOUT = 86400
INSTANCES_CACHE_TIMEOUT = 86400


[docs]class OpenstackConnection(object): # pylint: disable=too-few-public-methods """ This class manage interface between OpenStack Cloud infrastructure and views. """ def __init__(self): """ During class initialization, we create a connection to OpenStack Cloud """ super(OpenstackConnection, self).__init__() # We need to be admin to have access to hypervisor list credentials = dict() credentials['username'] = GLOBAL_CONFIG['OS_USERNAME'] credentials['password'] = GLOBAL_CONFIG['OS_PASSWORD'] credentials['auth_url'] = GLOBAL_CONFIG['OS_AUTH_URL'] credentials['project_name'] = GLOBAL_CONFIG['OS_PROJECT_NAME'] credentials['project_domain_name'] = GLOBAL_CONFIG['OS_PROJECT_DOMAIN_NAME'] credentials['user_domain_name'] = GLOBAL_CONFIG['OS_USER_DOMAIN_NAME'] try: auth = v3.Password(**credentials) self.session = session.Session(auth=auth, verify=GLOBAL_CONFIG['OS_CACERT']) except: # pylint: disable=bare-except pass def _instances(self): """ List of instances actually launched :return: dict() """ response = cache.get('instances') if not response: response = dict() nova = nvclient.Client(NOVA_VERSION, session=self.session) data_instances = nova.servers.list(search_opts={'all_tenants': 'true'}) for instance in data_instances: response[instance.id] = { 'user_id': instance.user_id, 'project_id': instance.tenant_id, 'id': instance.id, 'name': instance.name, 'created_at': parse_datetime(instance.created).date() } cache.set('instances', response, INSTANCES_CACHE_TIMEOUT) return response def _hypervisors(self): """ List of hypervisors and their details :return: dict() """ nova = nvclient.Client(NOVA_VERSION, session=self.session) hypervisors = nova.hypervisors.list() response = list() for hypervisor in hypervisors: response.append({ 'status': hypervisor.status, 'state': hypervisor.state, 'vcpus': hypervisor.vcpus, 'vcpus_used': hypervisor.vcpus_used, 'free_ram': hypervisor.free_ram_mb, 'memory': hypervisor.memory_mb, 'free_disk': hypervisor.free_disk_gb, 'local_disk': hypervisor.local_gb }) return response def _flavors(self): """ List of flavors and their details """ # We retrieve information from memcached response = cache.get('flavors') if not response: response = dict() nova = nvclient.Client(NOVA_VERSION, session=self.session) flavors = nova.flavors.list() for flavor in flavors: response[flavor.name] = { 'name': flavor.name, 'disk': int(flavor.disk), 'ram': int(flavor.ram), 'cpu': int(flavor.vcpus) } cache.set('flavors', response, FLAVOR_CACHE_TIMEOUT) return response def _domains(self): """ List all domains available :return: dict() """ response = cache.get('domains') if not response: response = dict() keystone = ksclient.Client(session=self.session) try: data_domains = keystone.domains.list() except ksexceptions.ConnectFailure: data_domains = list() for domain in data_domains: response[domain.id] = { 'id': domain.id, 'name': domain.name } cache.set('domain', response, USERS_CACHE_TIMEOUT) return response def _users(self): """ List of users. If not on admin network, we can't retrieve information, so we return a None object :return: dict() """ response = cache.get('users') if not response: response = dict() keystone = ksclient.Client(session=self.session) data_domain = self._domains() for domain in data_domain: try: data_users = keystone.users.list(domain=domain) except ksexceptions.ConnectFailure: data_users = list(domain) for user in data_users: try: user_email = user.email except AttributeError: user_email = "" response[user.id] = { 'id': user.id, 'domain_id': domain, 'name': user.name, 'email': user_email } cache.set('users', response, USERS_CACHE_TIMEOUT) return response def _projects(self): """ List of projects on OpenStack. :return: dict() """ keystone = ksclient.Client(session=self.session) try: projects = keystone.projects.list() except ksexceptions.ConnectFailure: projects = None return projects
[docs] def flavors(self): """ Return a list of flavor and a detail about - Their properties (CPU / Disk / RAM) - The actual Cloud state (number of VM we can start, maximum VM we can start if empty) :return: dict() """ flavors = self._flavors() # Retrieve hypervisor status to populate response hypervisors = self._hypervisors() # For each flavor, we look @ each hypervisor how many of # it can be launch @ the current state and the maximum value # based on flavor # * disk # * CPU # * RAM for flavor in flavors: free_flavor = 0 max_flavor = 0 for hypervisor in hypervisors: # If hypervisor is disable or down we don't care of it if hypervisor['status'] == "enabled" and\ hypervisor['state'] == "up": # We round down the number of flavor free_cpu = math.floor((hypervisor['vcpus'] - hypervisor['vcpus_used']) / flavors[flavor]['cpu']) max_cpu = math.floor(hypervisor['vcpus'] / flavors[flavor]['cpu']) free_ram = math.floor(hypervisor['free_ram'] / flavors[flavor]['ram']) max_ram = math.floor(hypervisor['memory'] / flavors[flavor]['ram']) free_disk = math.floor(hypervisor['free_disk'] / flavors[flavor]['disk']) max_disk = math.floor(hypervisor['local_disk'] / flavors[flavor]['disk']) # We keep the lowest value of ram / cpu / disk as it s # the weak link of the hypervisor if min(free_cpu, free_ram, free_disk) > 0: free_flavor += min(free_cpu, free_ram, free_disk) if min(max_cpu, max_ram, max_disk) > 0: max_flavor += min(max_cpu, max_ram, max_disk) flavors[flavor]['free'] = free_flavor flavors[flavor]['max'] = max_flavor return flavors
[docs] def instances(self, request, filtered=False): """ List all instances started on cluster and owned by user :param request: Web request, used to retrieve user id :param filtered: True if we only return user_id instances :return: dict() """ response = dict() data_instances = self._instances() # We only display instances that are owned by logged user for instance in data_instances: if data_instances[instance]['user_id'] == request.user.id or not filtered: response[data_instances[instance]['id']] = data_instances[instance] return InstancesAccess.show(response)
[docs] def users(self): """ Return a list of users w/ attributes id, first_name, last_name and email :return: dict of users """ return self._users()
[docs] def projects(self): """ Return a list of project w/ there id and name :return: dict() """ # We retrieve information from memcached response = cache.get('projects') if not response: # If not on memcached, we ask OpenStack response = dict() data_projects = self._projects() if data_projects is not None: for project in data_projects: response[project.id] = { 'id': project.id, 'name': project.name } cache.set('projects', response, PROJECTS_CACHE_TIMEOUT) return response
@staticmethod
[docs] def lease_instance(request, instance_id): """ If instance_id is owned by user_id, then update lease information, if not, raise PermissionDenied exception. A Openstack administrator can also update a lease for a user :param instance_id: id of instance :param request: Web request :return: void """ data_instances = cache.get('instances') if data_instances[instance_id]['user_id'] != request.user.id and \ not request.user.is_superuser: raise PermissionDenied(request.user.id, instance_id) InstancesAccess.lease(data_instances[instance_id]) return data_instances[instance_id]
[docs] def spy_instances(self): """ spy_instances is started by instance_spy module and check all running VM + notify user if a VM is close to its lease time :return: dict() """ now = date.today() data_instances = InstancesAccess.show(self._instances()) response = { 'delete': list(), # List of instance we must delete 'notify': list() # List of instance we must notify user to renew the lease } for instance in data_instances: # We mark the VM as showed InstancesAccess.heartbeat(data_instances[instance]) leased_at = data_instances[instance]['leased_at'] lease_end = data_instances[instance]['lease_end'] # If it's a new instance, we put lease value as today # it's not necessary to lease on model as heartbeat should have create and # lease the virtual machine if leased_at is None: lease_end = now + relativedelta(days=+LEASE_DURATION) first_notification_date = lease_end - relativedelta(days=+LEASE_DURATION/3) second_notification_date = lease_end - relativedelta(days=+LEASE_DURATION/6) LOGGER_INSTANCES.info( "Instance: %s will be notify %s and %s", data_instances[instance]['id'], first_notification_date, second_notification_date ) # If lease as expire we tag it as delete if lease_end < now: response['delete'].append(data_instances[instance]) elif first_notification_date == now or \ second_notification_date == now or \ lease_end < now - relativedelta(days=-6): response['notify'].append(data_instances[instance]) return response