Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0189e1f
Adding a few features
YannRnd May 28, 2021
ad36ee7
Features and fixes
YannRnd Jun 9, 2021
7c24091
Features and fixes
YannRnd Jun 9, 2021
a590f1c
Modify url for new version of Django
YannRnd Jun 21, 2021
9f496b9
Update requirements.txt
YannRnd Jun 21, 2021
e2d3eda
Fixing API version in config & requirements, adding a delete config f…
YannRnd Jun 21, 2021
17b80a4
Optimizing code in spy_instance
YannRnd Jun 22, 2021
1696bc5
Optimizing code in spy_instance
YannRnd Jun 22, 2021
6b0da42
Fix delete + model save
YannRnd Jun 23, 2021
04f645f
Fix delete OS
YannRnd Jun 24, 2021
99f7cb0
Delete notification
YannRnd Jun 24, 2021
67e18c9
Adjustments in OpenstackConnection.py
YannRnd Jun 24, 2021
93571f8
Fix delete
YannRnd Jun 24, 2021
bafb0cd
Reset cache in delete
YannRnd Jun 24, 2021
28ab5f4
Optimizing VM deleting
YannRnd Jun 25, 2021
a8788c3
Fix delete optimizing
YannRnd Jun 25, 2021
50ceab4
Adapting delete in views
YannRnd Jun 25, 2021
e71e2f0
Fix exclusion projects
YannRnd Jun 25, 2021
07ef53b
Fix exclusion projects
YannRnd Jun 25, 2021
001851c
Changing exclusion rules
YannRnd Jun 25, 2021
5170164
Loading config during spy_instance
YannRnd Jun 29, 2021
4c9752b
Adding reset cache setting
YannRnd Jun 29, 2021
eb029fe
Inverting reset cache config: True = reset
YannRnd Jun 29, 2021
a82dac8
Inverting reset cache config: True = reset
YannRnd Jun 29, 2021
e462b46
Rearranging priority for special lease durations
YannRnd Jun 29, 2021
155e7c3
Adding special lease instance differentiation for new instances
YannRnd Jun 29, 2021
c21a511
Fix bool evaluation and heartbeat timeout
YannRnd Jun 29, 2021
a7ace5b
Test button on instance view
YannRnd Jun 30, 2021
b80b6dd
Test button on instance view
YannRnd Jun 30, 2021
d5c16cd
Test button on instance view
YannRnd Jul 1, 2021
f9d5e39
Test button on instance view
YannRnd Jul 1, 2021
d244ea5
Test button on instance view
YannRnd Jul 2, 2021
260e336
Test button on instance view
YannRnd Jul 6, 2021
134992f
Test button on instance view
YannRnd Jul 6, 2021
afb117f
Test button on instance view
YannRnd Jul 6, 2021
8e27d64
Test button on instance view
YannRnd Jul 6, 2021
ee93227
Test button on instance view
YannRnd Jul 8, 2021
3a506a8
Button on instance view
YannRnd Jul 30, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion openstack_lease_it/config/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,11 @@ password = secret
email_header = admin@example.com
subject=Cloud@VD notification
link=https://lease-it.lal.in2p3.fr
default_domain=default.example.com
default_domain=default.example.com

[project-01]
exclude = True

[user_id_2]
exclude = True
duration = 120
84 changes: 72 additions & 12 deletions openstack_lease_it/lease_it/backend/OpenstackConnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""
This module manage interaction between application and
OpenStack cloud infrastructure

"""
import math

Expand All @@ -20,6 +21,8 @@
from lease_it.datastore import InstancesAccess, LEASE_DURATION
from lease_it.backend.Exceptions import PermissionDenied

from lease_it.models import Instances

# Define nova client version as a constant
NOVA_VERSION = 2

Expand Down Expand Up @@ -62,7 +65,9 @@ def _instances(self):
List of instances actually launched
:return: dict()
"""
response = cache.get('instances')
response = None
if not eval(GLOBAL_CONFIG['RESET_CACHE']):
response = cache.get('instances')
if not response:
response = dict()
nova = nvclient.Client(NOVA_VERSION, session=self.session)
Expand Down Expand Up @@ -104,7 +109,9 @@ def _flavors(self):
List of flavors and their details
"""
# We retrieve information from memcached
response = cache.get('flavors')
response = None
if not eval(GLOBAL_CONFIG['RESET_CACHE']):
response = cache.get('flavors')
if not response:
response = dict()
nova = nvclient.Client(NOVA_VERSION, session=self.session)
Expand All @@ -124,7 +131,9 @@ def _domains(self):
List all domains available
:return: dict()
"""
response = cache.get('domains')
response = None
if not eval(GLOBAL_CONFIG['RESET_CACHE']):
response = cache.get('domains')
if not response:
response = dict()
keystone = ksclient.Client(session=self.session)
Expand All @@ -146,7 +155,9 @@ def _users(self):
so we return a None object
:return: dict()
"""
response = cache.get('users')
response = None
if not eval(GLOBAL_CONFIG['RESET_CACHE']):
response = cache.get('users')
if not response:
response = dict()
keystone = ksclient.Client(session=self.session)
Expand Down Expand Up @@ -288,44 +299,93 @@ def lease_instance(request, instance_id):
not request.user.is_superuser:
raise PermissionDenied(request.user.id, instance_id)
InstancesAccess.lease(data_instances[instance_id])
InstancesAccess.heartbeat(data_instances[instance_id])
return data_instances[instance_id]

def delete(self, instances_to_delete):
"""
Deletes the VM with the id given in parameter

:param instances_to_delete: list of instances to delete
:return: void
"""
if eval(GLOBAL_CONFIG['OS_DELETE']):
nova = nvclient.Client(NOVA_VERSION, session=self.session)
instance_list = nova.servers.list(search_opts={'all_tenants': 'true'})
for instance in instance_list:
for to_delete in instances_to_delete:
if instance.id == to_delete['id']:
instance.delete()
else:
print("Deleted the instances from Openstack")
cache.delete('instances')

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
if a VM is close to its lease time + update lease duration according to the lease duration settings

:return: dict()
"""
now = date.today()
data_instances = InstancesAccess.show(self._instances())
users = self._users()
projects = self.projects()
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
# We mark the VM as shown
InstancesAccess.heartbeat(data_instances[instance])
user_name = users[data_instances[instance]['user_id']]['name']
instance_name = data_instances[instance]['name']
project_name = projects[data_instances[instance]['project_id']]['name']
leased_at = data_instances[instance]['leased_at']
lease_end = data_instances[instance]['lease_end']
lease_duration = LEASE_DURATION
# If the instance benefits from a special lease (from its user_name, instance_id or project_id),
# we update the lease_duration (used to determine whether to delete it or not)
# and the instance's lease duration
# Special lease are ordered in the following priority order :
# instance_id > instance_name > user_name > project_id > project_name
if data_instances[instance]['id'] in GLOBAL_CONFIG['SPECIAL_LEASE_DURATION']:
lease_duration = GLOBAL_CONFIG['SPECIAL_LEASE_DURATION'][data_instances[instance]['id']]
elif instance_name in GLOBAL_CONFIG['SPECIAL_LEASE_DURATION']:
lease_duration = GLOBAL_CONFIG['SPECIAL_LEASE_DURATION'][instance_name]
elif user_name in GLOBAL_CONFIG['SPECIAL_LEASE_DURATION']:
lease_duration = GLOBAL_CONFIG['SPECIAL_LEASE_DURATION'][user_name]
elif data_instances[instance]['project_id'] in GLOBAL_CONFIG['SPECIAL_LEASE_DURATION']:
lease_duration = GLOBAL_CONFIG['SPECIAL_LEASE_DURATION'][data_instances[instance]['project_id']]
elif project_name in GLOBAL_CONFIG['SPECIAL_LEASE_DURATION']:
lease_duration = GLOBAL_CONFIG['SPECIAL_LEASE_DURATION'][project_name]

model = Instances.objects.get(id=data_instances[instance]['id'])
model.lease_duration = lease_duration
model.save()

# 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)
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
second_notification_date,
)
# If lease as expire we tag it as delete
if lease_end < now:
# If lease has expired and it's not in the excluded projects, we tag it as delete
if lease_end < now and project_name not in GLOBAL_CONFIG["EXCLUDE"] and \
user_name not in GLOBAL_CONFIG["EXCLUDE"] and \
instance_name not in GLOBAL_CONFIG["EXCLUDE"]:
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])
cache.delete("instances")
return response

40 changes: 30 additions & 10 deletions openstack_lease_it/lease_it/backend/TestConnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- _users()
- _projects()
"""

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

Expand All @@ -21,10 +22,13 @@
from lease_it.backend.Exceptions import PermissionDenied


from openstack_lease_it.settings import GLOBAL_CONFIG


class TestConnection(OpenstackConnection):
"""
This class is only used for developement. This will
return false value formated as expected by views.
This class is only used for development. This will
return plausible values formatted as expected by views.
"""
def __init__(self):
self.session = None
Expand All @@ -36,26 +40,28 @@ def _instances(self):

:return: dict()
"""
response = cache.get('instances')
response = None
if not eval(GLOBAL_CONFIG['RESET_CACHE']):
response = cache.get('instances')
if not response:
response = {
'instance-01': {
'user_id': 1,
'project_id': 'project-01',
'id': 'instance-01',
'name': 'instance-name-01',
'created_at': parse_datetime('2017-04-29T17:40:26Z').date()
'created_at': parse_datetime('2016-04-29T17:40:26Z').date()
},
'instance-02': {
'user_id': 1,
'project_id': 'project-01',
'project_id': 'project-02',
'id': 'instance-02',
'name': 'instance-name-02',
'created_at': parse_datetime('2017-10-29T17:40:26Z').date()
},
'instance-03': {
'user_id': 2,
'project_id': 'project-01',
'project_id': 'project-02',
'id': 'instance-03',
'name': 'instance-name-03',
'created_at': parse_datetime('2016-04-29T17:40:26Z').date()
Expand Down Expand Up @@ -85,7 +91,9 @@ def _flavors(self):

:return: dict()
"""
response = cache.get('flavors')
response = None
if not eval(GLOBAL_CONFIG['RESET_CACHE']):
response = cache.get('flavors')
if not response:
response = {
'flavor.01': {
Expand Down Expand Up @@ -138,7 +146,9 @@ def _domains(self):

:return: dict()
"""
response = cache.get('domains')
response = None
if not eval(GLOBAL_CONFIG['RESET_CACHE']):
response = cache.get('domains')
if not response:
response = {
'domain-01': {
Expand All @@ -160,7 +170,9 @@ def _users(self):

:return: dict()
"""
response = cache.get('users')
response = None
if not eval(GLOBAL_CONFIG['RESET_CACHE']):
response = cache.get('users')
if not response:
response = {
1: {
Expand All @@ -186,4 +198,12 @@ def _projects(self):

:return: dict()
"""
return dict()
return dict()

def delete(self, instance_id):
"""
Notifies in the terminal the instance that tried to be deleted, as it can't because it's a test connection

:return: void
"""
print("Deleted the instance " + instance_id + " from Openstack")
6 changes: 3 additions & 3 deletions openstack_lease_it/lease_it/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pylint: skip-file
# -*- coding: utf-8 -*-

from OpenstackConnection import *
from TestConnection import *
from Exceptions import *
from lease_it.backend.OpenstackConnection import *
from lease_it.backend.TestConnection import *
from lease_it.backend.Exceptions import *
9 changes: 8 additions & 1 deletion openstack_lease_it/lease_it/client/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
"""
import os
from collections import defaultdict
from lease_it.notification.MailNotification import MailNotification


os.environ['DJANGO_SETTINGS_MODULE'] = "openstack_lease_it.settings"

from lease_it.notification.MailNotification import MailNotification
from lease_it.datastore.ModelAccess import InstancesAccess


from lease_it import backend # pylint: disable=wrong-import-position
from openstack_lease_it.settings import GLOBAL_CONFIG # pylint: disable=wrong-import-position

Expand All @@ -30,6 +34,9 @@ def instance_spy():
notification[notification_type] = defaultdict(list)
for instance in instances[notification_type]:
notification[notification_type][instance['user_id']].append(instance)
if notification_type == "delete":
InstancesAccess.delete(instance['id'])
BACKEND.delete(instances['delete'])
notify = MailNotification(users)
notify.send(notification)

Expand Down
18 changes: 12 additions & 6 deletions openstack_lease_it/lease_it/datastore/ModelAccess.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"""
ModelAccess module is a interface between Django model and view
"""

from dateutil.relativedelta import relativedelta

from django.utils import timezone
Expand All @@ -13,17 +12,16 @@
from lease_it.datastore.Exceptions import StillRunning

from openstack_lease_it.settings import LOGGER_INSTANCES
from openstack_lease_it.settings import GLOBAL_CONFIG

# Default lease duration in day
LEASE_DURATION = 90
# Number of day we keep instance in database
HEARTBEAT_TIMEOUT = 7


class InstancesAccess(object): # pylint: disable=too-few-public-methods
"""
ModelAccess is a class will abstract model access for application. It
will get / save / ... informations in a format expected by views
will get / save / ... information in a format expected by views
"""
@staticmethod
def get(instance):
Expand All @@ -42,6 +40,12 @@ def get(instance):
model.leased_at = timezone.now()
model.heartbeat_at = timezone.now()
model.lease_duration = LEASE_DURATION
if instance['id'] in GLOBAL_CONFIG['SPECIAL_LEASE_DURATION']:
model.lease_duration = GLOBAL_CONFIG['SPECIAL_LEASE_DURATION'][instance['id']]
elif instance['name'] in GLOBAL_CONFIG['SPECIAL_LEASE_DURATION']:
model.lease_duration = GLOBAL_CONFIG['SPECIAL_LEASE_DURATION'][instance['name']]
# The different lease durations according to users and projects
# will be adapted once spy_instance has run once
return model

@staticmethod
Expand Down Expand Up @@ -133,8 +137,10 @@ def delete(instance_id):
"""
try:
model = Instances.objects.get(id=instance_id) # pylint: disable=no-member
if model.heartbeat_at + relativedelta(days=+HEARTBEAT_TIMEOUT) > timezone.now().date():
raise StillRunning(model.id, model.heartbeat_at)
# To let the users delete their own instances, we have to disable the StillRunning error
#if model.leased_at + relativedelta(days=+model.lease_duration) >\
# timezone.now().date():
# raise StillRunning(model.id, model.heartbeat_at)
model.delete()
except ObjectDoesNotExist:
LOGGER_INSTANCES.info('Instance %s does not exist', instance_id)
2 changes: 1 addition & 1 deletion openstack_lease_it/lease_it/datastore/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pylint: skip-file
# -*- coding: utf-8 -*-

from ModelAccess import *
from lease_it.datastore.ModelAccess import *
Loading