# -*- 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