diff --git a/openstack_lease_it/config/config.ini b/openstack_lease_it/config/config.ini
index 6077c12..ead0ad6 100644
--- a/openstack_lease_it/config/config.ini
+++ b/openstack_lease_it/config/config.ini
@@ -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
\ No newline at end of file
+default_domain=default.example.com
+
+[project-01]
+exclude = True
+
+[user_id_2]
+exclude = True
+duration = 120
\ No newline at end of file
diff --git a/openstack_lease_it/lease_it/backend/OpenstackConnection.py b/openstack_lease_it/lease_it/backend/OpenstackConnection.py
index 9934a8a..ef28a8f 100644
--- a/openstack_lease_it/lease_it/backend/OpenstackConnection.py
+++ b/openstack_lease_it/lease_it/backend/OpenstackConnection.py
@@ -2,6 +2,7 @@
"""
This module manage interaction between application and
OpenStack cloud infrastructure
+
"""
import math
@@ -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
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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
+
diff --git a/openstack_lease_it/lease_it/backend/TestConnection.py b/openstack_lease_it/lease_it/backend/TestConnection.py
index ebdfd47..9e3dee2 100644
--- a/openstack_lease_it/lease_it/backend/TestConnection.py
+++ b/openstack_lease_it/lease_it/backend/TestConnection.py
@@ -13,6 +13,7 @@
- _users()
- _projects()
"""
+
from django.utils.dateparse import parse_datetime
from django.core.cache import cache
@@ -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
@@ -36,7 +40,9 @@ 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': {
@@ -44,18 +50,18 @@ def _instances(self):
'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()
@@ -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': {
@@ -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': {
@@ -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: {
@@ -186,4 +198,12 @@ def _projects(self):
:return: dict()
"""
- return dict()
\ No newline at end of file
+ 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")
diff --git a/openstack_lease_it/lease_it/backend/__init__.py b/openstack_lease_it/lease_it/backend/__init__.py
index be8b218..30baa92 100644
--- a/openstack_lease_it/lease_it/backend/__init__.py
+++ b/openstack_lease_it/lease_it/backend/__init__.py
@@ -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 *
diff --git a/openstack_lease_it/lease_it/client/run.py b/openstack_lease_it/lease_it/client/run.py
index 3adb54f..9baa3d5 100644
--- a/openstack_lease_it/lease_it/client/run.py
+++ b/openstack_lease_it/lease_it/client/run.py
@@ -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
@@ -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)
diff --git a/openstack_lease_it/lease_it/datastore/ModelAccess.py b/openstack_lease_it/lease_it/datastore/ModelAccess.py
index 88b618e..968de2e 100644
--- a/openstack_lease_it/lease_it/datastore/ModelAccess.py
+++ b/openstack_lease_it/lease_it/datastore/ModelAccess.py
@@ -2,7 +2,6 @@
"""
ModelAccess module is a interface between Django model and view
"""
-
from dateutil.relativedelta import relativedelta
from django.utils import timezone
@@ -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):
@@ -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
@@ -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)
diff --git a/openstack_lease_it/lease_it/datastore/__init__.py b/openstack_lease_it/lease_it/datastore/__init__.py
index 946a895..217a58c 100644
--- a/openstack_lease_it/lease_it/datastore/__init__.py
+++ b/openstack_lease_it/lease_it/datastore/__init__.py
@@ -1,4 +1,4 @@
# pylint: skip-file
# -*- coding: utf-8 -*-
-from ModelAccess import *
+from lease_it.datastore.ModelAccess import *
diff --git a/openstack_lease_it/lease_it/fixtures/load_test_models.py b/openstack_lease_it/lease_it/fixtures/load_test_models.py
new file mode 100644
index 0000000..0da66d1
--- /dev/null
+++ b/openstack_lease_it/lease_it/fixtures/load_test_models.py
@@ -0,0 +1,340 @@
+"""
+Executing this file updates the test_models.json with the data provided by test_models_setup.json.
+This simplifies the instances' management during the tests.
+"""
+import json
+
+# Basic database dictionary
+test_models_base = [
+ {
+ "model": "contenttypes.contenttype",
+ "fields": {
+ "app_label": "openstack_auth",
+ "model": "user"},
+ "pk": 1
+ },
+ {
+ "model": "contenttypes.contenttype",
+ "fields": {
+ "app_label": "admin", "model": "logentry"
+ },
+ "pk": 2
+ },
+ {
+ "model": "contenttypes.contenttype",
+ "fields": {
+ "app_label": "auth", "model": "permission"
+ },
+ "pk": 3
+ },
+ {
+ "model": "contenttypes.contenttype",
+ "fields": {
+ "app_label": "auth", "model": "group"
+ },
+ "pk": 4
+ },
+ {
+ "model": "contenttypes.contenttype",
+ "fields": {
+ "app_label": "auth", "model": "user"
+ },
+ "pk": 5
+ },
+ {
+ "model": "contenttypes.contenttype",
+ "fields":
+ {
+ "app_label": "contenttypes",
+ "model": "contenttype"
+ },
+ "pk": 6
+ },
+ {
+ "model": "contenttypes.contenttype",
+ "fields":
+ {
+ "app_label": "sessions", "model": "session"
+ },
+ "pk": 7
+ },
+ {
+ "model": "contenttypes.contenttype",
+ "fields":
+ {
+ "app_label": "lease_it",
+ "model": "instances"
+ },
+ "pk": 8
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can add user",
+ "content_type": 1,
+ "codename": "add_user"
+ },
+ "pk": 1
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can change user",
+ "content_type": 1,
+ "codename": "change_user"
+ },
+ "pk": 2
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can delete user",
+ "content_type": 1,
+ "codename": "delete_user"
+ },
+ "pk": 3
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can add log entry",
+ "content_type": 2,
+ "codename": "add_logentry"
+ },
+ "pk": 4
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can change log entry",
+ "content_type": 2,
+ "codename": "change_logentry"
+ },
+ "pk": 5
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can delete log entry",
+ "content_type": 2,
+ "codename": "delete_logentry"
+ },
+ "pk": 6
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can add permission",
+ "content_type": 3,
+ "codename": "add_permission"
+ },
+ "pk": 7
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can change permission",
+ "content_type": 3,
+ "codename": "change_permission"
+ },
+ "pk": 8
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can delete permission",
+ "content_type": 3,
+ "codename": "delete_permission"
+ },
+ "pk": 9
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can add group",
+ "content_type": 4,
+ "codename": "add_group"
+ },
+ "pk": 10
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can change group",
+ "content_type": 4,
+ "codename": "change_group"
+ },
+ "pk": 11
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can delete group",
+ "content_type": 4,
+ "codename": "delete_group"
+ },
+ "pk": 12
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can add user",
+ "content_type": 5,
+ "codename": "add_user"
+ },
+ "pk": 13
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can change user",
+ "content_type": 5,
+ "codename": "change_user"
+ },
+ "pk": 14
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can delete user",
+ "content_type": 5,
+ "codename": "delete_user"
+ },
+ "pk": 15
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can add content type",
+ "content_type": 6,
+ "codename": "add_contenttype"
+ },
+ "pk": 16
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can change content type",
+ "content_type": 6,
+ "codename": "change_contenttype"
+ },
+ "pk": 17
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can delete content type",
+ "content_type": 6,
+ "codename": "delete_contenttype"
+ },
+ "pk": 18
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can add session",
+ "content_type": 7,
+ "codename": "add_session"
+ },
+ "pk": 19
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can change session",
+ "content_type": 7,
+ "codename": "change_session"
+ },
+ "pk": 20
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can delete session",
+ "content_type": 7,
+ "codename": "delete_session"
+ },
+ "pk": 21
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can add instances",
+ "content_type": 8,
+ "codename": "add_instances"
+ },
+ "pk": 22
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can change instances",
+ "content_type": 8,
+ "codename": "change_instances"
+ },
+ "pk": 23
+ },
+ {
+ "model": "auth.permission",
+ "fields":
+ {
+ "name": "Can delete instances",
+ "content_type": 8,
+ "codename": "delete_instances"
+ },
+ "pk": 24
+ },
+ {
+ "model": "auth.user",
+ "fields":
+ {
+ "password": "pbkdf2_sha256$20000$f0kZKtEf9D78$XvUaXte8o9kP5Nu5stefJUXCWltiQRFl7iZD1mB5onI=",
+ "last_login": "2021-05-26T11:28:57.063Z",
+ "is_superuser": True,
+ "username": "yann",
+ "first_name": "",
+ "last_name": "",
+ "email": "test@test.fr",
+ "is_staff": True,
+ "is_active": True,
+ "date_joined": "2021-05-25T10:37:21.225Z",
+ "groups": [],
+ "user_permissions": []
+ },
+ "pk": 1
+ }
+]
+
+# Adding the instances to the basic database dictionary
+test_models_setup_file = open("./lease_it/fixtures/test_models_setup.json")
+test_models = open("./lease_it/fixtures/test_models.json", "w")
+test_models_setup = json.loads(test_models_setup_file.read())
+
+for test_model in test_models_setup:
+ test_models_base.append(test_model)
+
+json.dump(test_models_base, test_models)
+
diff --git a/openstack_lease_it/lease_it/fixtures/test_models explanations b/openstack_lease_it/lease_it/fixtures/test_models explanations
new file mode 100644
index 0000000..5d45602
--- /dev/null
+++ b/openstack_lease_it/lease_it/fixtures/test_models explanations
@@ -0,0 +1,8 @@
+
+File used to load a database locally corresponding to the data from TestConnection
+
+To load this database, use the django command :
+ manage.py loaddata test_models.json
+
+To manage instances loaded parameters, set them in test_models_setup.json,
+then execute the Python file load_test_models.py
diff --git a/openstack_lease_it/lease_it/fixtures/test_models.json b/openstack_lease_it/lease_it/fixtures/test_models.json
new file mode 100644
index 0000000..c940c6a
--- /dev/null
+++ b/openstack_lease_it/lease_it/fixtures/test_models.json
@@ -0,0 +1 @@
+[{"model": "contenttypes.contenttype", "fields": {"app_label": "openstack_auth", "model": "user"}, "pk": 1}, {"model": "contenttypes.contenttype", "fields": {"app_label": "admin", "model": "logentry"}, "pk": 2}, {"model": "contenttypes.contenttype", "fields": {"app_label": "auth", "model": "permission"}, "pk": 3}, {"model": "contenttypes.contenttype", "fields": {"app_label": "auth", "model": "group"}, "pk": 4}, {"model": "contenttypes.contenttype", "fields": {"app_label": "auth", "model": "user"}, "pk": 5}, {"model": "contenttypes.contenttype", "fields": {"app_label": "contenttypes", "model": "contenttype"}, "pk": 6}, {"model": "contenttypes.contenttype", "fields": {"app_label": "sessions", "model": "session"}, "pk": 7}, {"model": "contenttypes.contenttype", "fields": {"app_label": "lease_it", "model": "instances"}, "pk": 8}, {"model": "auth.permission", "fields": {"name": "Can add user", "content_type": 1, "codename": "add_user"}, "pk": 1}, {"model": "auth.permission", "fields": {"name": "Can change user", "content_type": 1, "codename": "change_user"}, "pk": 2}, {"model": "auth.permission", "fields": {"name": "Can delete user", "content_type": 1, "codename": "delete_user"}, "pk": 3}, {"model": "auth.permission", "fields": {"name": "Can add log entry", "content_type": 2, "codename": "add_logentry"}, "pk": 4}, {"model": "auth.permission", "fields": {"name": "Can change log entry", "content_type": 2, "codename": "change_logentry"}, "pk": 5}, {"model": "auth.permission", "fields": {"name": "Can delete log entry", "content_type": 2, "codename": "delete_logentry"}, "pk": 6}, {"model": "auth.permission", "fields": {"name": "Can add permission", "content_type": 3, "codename": "add_permission"}, "pk": 7}, {"model": "auth.permission", "fields": {"name": "Can change permission", "content_type": 3, "codename": "change_permission"}, "pk": 8}, {"model": "auth.permission", "fields": {"name": "Can delete permission", "content_type": 3, "codename": "delete_permission"}, "pk": 9}, {"model": "auth.permission", "fields": {"name": "Can add group", "content_type": 4, "codename": "add_group"}, "pk": 10}, {"model": "auth.permission", "fields": {"name": "Can change group", "content_type": 4, "codename": "change_group"}, "pk": 11}, {"model": "auth.permission", "fields": {"name": "Can delete group", "content_type": 4, "codename": "delete_group"}, "pk": 12}, {"model": "auth.permission", "fields": {"name": "Can add user", "content_type": 5, "codename": "add_user"}, "pk": 13}, {"model": "auth.permission", "fields": {"name": "Can change user", "content_type": 5, "codename": "change_user"}, "pk": 14}, {"model": "auth.permission", "fields": {"name": "Can delete user", "content_type": 5, "codename": "delete_user"}, "pk": 15}, {"model": "auth.permission", "fields": {"name": "Can add content type", "content_type": 6, "codename": "add_contenttype"}, "pk": 16}, {"model": "auth.permission", "fields": {"name": "Can change content type", "content_type": 6, "codename": "change_contenttype"}, "pk": 17}, {"model": "auth.permission", "fields": {"name": "Can delete content type", "content_type": 6, "codename": "delete_contenttype"}, "pk": 18}, {"model": "auth.permission", "fields": {"name": "Can add session", "content_type": 7, "codename": "add_session"}, "pk": 19}, {"model": "auth.permission", "fields": {"name": "Can change session", "content_type": 7, "codename": "change_session"}, "pk": 20}, {"model": "auth.permission", "fields": {"name": "Can delete session", "content_type": 7, "codename": "delete_session"}, "pk": 21}, {"model": "auth.permission", "fields": {"name": "Can add instances", "content_type": 8, "codename": "add_instances"}, "pk": 22}, {"model": "auth.permission", "fields": {"name": "Can change instances", "content_type": 8, "codename": "change_instances"}, "pk": 23}, {"model": "auth.permission", "fields": {"name": "Can delete instances", "content_type": 8, "codename": "delete_instances"}, "pk": 24}, {"model": "auth.user", "fields": {"password": "pbkdf2_sha256$20000$f0kZKtEf9D78$XvUaXte8o9kP5Nu5stefJUXCWltiQRFl7iZD1mB5onI=", "last_login": "2021-05-26T11:28:57.063Z", "is_superuser": true, "username": "yann", "first_name": "", "last_name": "", "email": "test@test.fr", "is_staff": true, "is_active": true, "date_joined": "2021-05-25T10:37:21.225Z", "groups": [], "user_permissions": []}, "pk": 1}, {"model": "lease_it.instances", "fields": {"heartbeat_at": "2021-05-24", "leased_at": "2021-05-24", "lease_duration": 120}, "pk": "instance-01"}, {"model": "lease_it.instances", "fields": {"heartbeat_at": "2020-08-24", "leased_at": "2020-05-24", "lease_duration": 90}, "pk": "instance-02"}, {"model": "lease_it.instances", "fields": {"heartbeat_at": "2020-08-24", "leased_at": "2020-05-24", "lease_duration": 120}, "pk": "instance-03"}, {"model": "lease_it.instances", "fields": {"heartbeat_at": "2020-08-24", "leased_at": "2020-05-24", "lease_duration": 120}, "pk": "instance-04"}, {"model": "lease_it.instances", "fields": {"heartbeat_at": "2021-08-24", "leased_at": "2021-05-24", "lease_duration": 90}, "pk": "instance-05"}]
\ No newline at end of file
diff --git a/openstack_lease_it/lease_it/fixtures/test_models_setup.json b/openstack_lease_it/lease_it/fixtures/test_models_setup.json
new file mode 100644
index 0000000..e914aa8
--- /dev/null
+++ b/openstack_lease_it/lease_it/fixtures/test_models_setup.json
@@ -0,0 +1,53 @@
+
+[
+ {
+ "model": "lease_it.instances",
+ "fields":
+ {
+ "heartbeat_at": "2021-05-24",
+ "leased_at": "2021-05-24",
+ "lease_duration": 120
+ },
+ "pk": "instance-01"
+ },
+ {
+ "model": "lease_it.instances",
+ "fields":
+ {
+ "heartbeat_at": "2020-08-24",
+ "leased_at": "2020-05-24",
+ "lease_duration": 90
+ },
+ "pk": "instance-02"
+ },
+ {
+ "model": "lease_it.instances",
+ "fields":
+ {
+ "heartbeat_at": "2020-08-24",
+ "leased_at": "2020-05-24",
+ "lease_duration": 120
+ },
+ "pk": "instance-03"
+ },
+ {
+ "model": "lease_it.instances",
+ "fields":
+ {
+ "heartbeat_at": "2020-08-24",
+ "leased_at": "2020-05-24",
+ "lease_duration": 120
+ },
+ "pk": "instance-04"
+ },
+ {
+ "model": "lease_it.instances",
+ "fields":
+ {
+ "heartbeat_at": "2021-08-24",
+ "leased_at": "2021-05-24",
+ "lease_duration": 90
+ },
+ "pk": "instance-05"
+ }
+]
diff --git a/openstack_lease_it/lease_it/notification/MailNotification.py b/openstack_lease_it/lease_it/notification/MailNotification.py
index 5dc5c09..29a04ba 100644
--- a/openstack_lease_it/lease_it/notification/MailNotification.py
+++ b/openstack_lease_it/lease_it/notification/MailNotification.py
@@ -8,24 +8,33 @@
from email.mime.text import MIMEText
from openstack_lease_it.settings import GLOBAL_CONFIG, EMAIL_REGEXP, LOGGER_NOTIFICATION
+
class MailNotification(object): # pylint: disable=too-few-public-methods
"""
A class to abstract e-mail notification
"""
def __init__(self, users):
"""
- Not yet implemented
+ Not implemented yet
"""
self.users = users
- self.smtp = smtplib.SMTP_SSL(GLOBAL_CONFIG['NOTIFICATION_SMTP'])
- self.smtp.login(GLOBAL_CONFIG['NOTIFICATION_USERNAME'],
- GLOBAL_CONFIG['NOTIFICATION_PASSWORD'])
- delete_content = open(GLOBAL_CONFIG['NOTIFICATION_DELETE_CONTENT'], 'r')
- lease_content = open(GLOBAL_CONFIG['NOTIFICATION_LEASE_CONTENT'], 'r')
- self.notification = {
- 'delete': delete_content.read(),
- 'notify': lease_content.read()
- }
+ try:
+ self.smtp = smtplib.SMTP_SSL(GLOBAL_CONFIG['NOTIFICATION_SMTP'])
+ self.smtp.login(GLOBAL_CONFIG['NOTIFICATION_USERNAME'],
+ GLOBAL_CONFIG['NOTIFICATION_PASSWORD'])
+ delete_content = open(GLOBAL_CONFIG['NOTIFICATION_DELETE_CONTENT'], 'r')
+ lease_content = open(GLOBAL_CONFIG['NOTIFICATION_LEASE_CONTENT'], 'r')
+ self.notification = {
+ 'delete': delete_content.read(),
+ 'notify': lease_content.read()
+ }
+ except:
+ self.notification = {
+ 'delete': open("/dev/null").read(),
+ 'notify': open("/dev/null").read()
+ }
+
+ pass
@staticmethod
def format_user_instances(user):
@@ -54,9 +63,13 @@ def format_mail(self, user, notification_type, instances):
LOGGER_NOTIFICATION.info("User %s as not be found", user)
core_text = self.notification[notification_type]
instances_text = self.format_user_instances(instances)
- return core_text.format(username=user_name,
+ try:
+ return core_text.format(username=user_name,
link=GLOBAL_CONFIG['NOTIFICATION_LINK'],
instances=instances_text)
+ except KeyError:
+ LOGGER_NOTIFICATION.info("Notification link not found")
+ pass
def send(self, notifications):
"""
@@ -68,13 +81,24 @@ def send(self, notifications):
for notification in notifications:
for user in notifications[notification]:
mail_text = self.format_mail(user, notification, notifications[notification][user])
- mail = MIMEText(mail_text)
- mail['Subject'] = GLOBAL_CONFIG['NOTIFICATION_SUBJECT']
- mail['From'] = GLOBAL_CONFIG['NOTIFICATION_EMAIL_HEADER']
+ try:
+ mail = MIMEText(mail_text)
+ except AttributeError:
+ LOGGER_NOTIFICATION.info("Mail text None type")
+ mail = dict()
+ try:
+ mail['Subject'] = GLOBAL_CONFIG['NOTIFICATION_SUBJECT']
+ except KeyError:
+ LOGGER_NOTIFICATION.info("Email subject not found")
+ mail['Subject'] = "/"
+ try:
+ mail['From'] = GLOBAL_CONFIG['NOTIFICATION_EMAIL_HEADER']
+ except KeyError:
+ LOGGER_NOTIFICATION.info("Email header not found")
+ mail['From'] = "/"
try:
email = self.users[user]['email']
- if not re.match(EMAIL_REGEXP, email) and \
- GLOBAL_CONFIG['NOTIFICATION_DOMAIN'] != "":
+ if not re.match(EMAIL_REGEXP, email) and GLOBAL_CONFIG['NOTIFICATION_DOMAIN'] != "":
LOGGER_NOTIFICATION.info("email %s not match a email format"
" (name@domain.com). Add @%s",
email, GLOBAL_CONFIG['NOTIFICATION_DOMAIN'])
@@ -88,9 +112,16 @@ def send(self, notifications):
email]
except KeyError:
LOGGER_NOTIFICATION.info("email field of %s as not be found", user)
- mail['To'] = GLOBAL_CONFIG['NOTIFICATION_EMAIL_HEADER']
- recipient = [GLOBAL_CONFIG['NOTIFICATION_EMAIL_HEADER']]
+ try:
+ mail['To'] = GLOBAL_CONFIG['NOTIFICATION_EMAIL_HEADER']
+ recipient = [GLOBAL_CONFIG['NOTIFICATION_EMAIL_HEADER']]
+ except KeyError:
+ LOGGER_NOTIFICATION.info("Email header not found")
+ recipient = "/"
LOGGER_NOTIFICATION.info("Notification %s to %s", notification, ''.join(recipient))
- self.smtp.sendmail(GLOBAL_CONFIG['NOTIFICATION_EMAIL_HEADER'],
+ try:
+ self.smtp.sendmail(GLOBAL_CONFIG['NOTIFICATION_EMAIL_HEADER'],
recipient,
mail.as_string())
+ except AttributeError:
+ LOGGER_NOTIFICATION.info("Smtp not found")
diff --git a/openstack_lease_it/lease_it/static/js/database.js b/openstack_lease_it/lease_it/static/js/database.js
index 4155ca4..b21aba3 100644
--- a/openstack_lease_it/lease_it/static/js/database.js
+++ b/openstack_lease_it/lease_it/static/js/database.js
@@ -26,9 +26,9 @@ function buildDatabaseView(div_name) {
targets: [0],
render: function ( data, type, row ) {
var now = new Date();
- var heartbeat_date = new Date(row.heartbeat_at);
- // If a VM as not been seen since 1 week, we can delete it
- if (heartbeat_date < now - HEARTBEAT_TIMEOUT) {
+ var lease_end = new Date(row.lease_end);
+ // If a VM has a lease_end date before today, we can delete it
+ if (lease_end < now - HEARTBEAT_TIMEOUT) {
return buildDatabaseRowMenu(data) +
formatText(data, MAX_STRING_LENGTH);
} else {
@@ -49,7 +49,7 @@ function buildDatabaseRowMenu(data) {
var menu = '' +
'chevron_right ' +
'' +
- '' +
'delete ';
return menu;
@@ -70,9 +70,12 @@ function swapDatabaseRowMenu(button) {
/*
deleteDatabase delete entry in database
*/
-function deleteDatabase(instance) {
- return $.getJSON("/database/" + instance, function(data){
+function deleteDatabase(id) {
+ confirm("Instance of id :" + id + " will be deleted")
+ return $.getJSON("/database/" + id, function(data){
}).success(function(data){
- notify(data);
+ var notification = data;
+ notification["instance"]["id"] = "Instance of id : " + notification["instance"]["id"] + " has been deleted";
+ notify(notification);
});
}
\ No newline at end of file
diff --git a/openstack_lease_it/lease_it/static/js/instances.js b/openstack_lease_it/lease_it/static/js/instances.js
index 85a59b0..020a07c 100644
--- a/openstack_lease_it/lease_it/static/js/instances.js
+++ b/openstack_lease_it/lease_it/static/js/instances.js
@@ -16,14 +16,14 @@ const MAX_STRING_LENGTH = 30;
/*
buildInstancesView create a full display of Instance on div_name
*/
-function buildInstancesView(div_name, get_option, show_user){
+function buildInstancesView(div_name, get_option, is_admin){
var table_columns = [
{ data: 'name' },
{ data: 'project' },
{ data: 'created_at' },
{ data: 'lease_end' }
];
- if (show_user) {
+ if (is_admin) {
table_columns.unshift({data: 'user'});
}
$('#table-' + div_name).DataTable({
@@ -44,9 +44,16 @@ function buildInstancesView(div_name, get_option, show_user){
pageLength: 25,
columnDefs: [
{
- targets: [0, 1, 2],
- render: function ( data, type, row ) {
- return formatText(data, MAX_STRING_LENGTH);
+ targets: [0, 1, 2],
+ render: function ( data, type, row, meta ) {
+ var now = new Date();
+ var lease_end = new Date(row.lease_end.slice(0,10));
+ if (meta.col == 0 && (div_name == "admin-instances" || div_name == "instances")) {
+ return buildInstanceRowMenu(data, row, is_admin) + formatText(data, MAX_STRING_LENGTH);
+ }
+ else {
+ return formatText(data, MAX_STRING_LENGTH);
+ };
}
}],
drawCallback: function(settings, json) {
@@ -86,4 +93,48 @@ function formatLeaseBtn(date, instance) {
' new badge hoverable"' +
' data-badge-caption="new lease" onClick="updateLease(\''+
instance + '\')">';
-}
\ No newline at end of file
+}
+
+/*
+ buildInstanceRowMenu build a menu for each row of Instance Table
+*/
+function buildInstanceRowMenu(data, row, is_admin) {
+ if (is_admin) {
+ var menu = '' +
+ 'chevron_right ' +
+ '' +
+ '' +
+ 'delete ';
+ }
+ else {
+ var menu = '' +
+ 'chevron_right ' +
+ '' +
+ '' +
+ 'delete ';
+
+ };
+ return menu;
+}
+
+/*
+ swapInstanceRowMenu swap on/off the delete button
+*/
+function swapInstanceRowMenu(button) {
+ if ($('#instance-delete-' + button).css('display') == 'none') {
+ $('#instance-icon-' + button).text('chevron_left');
+ }
+ if ($('#instance-admin-delete-' + button).css('display') == 'none') {
+ $('#instance-admin-icon-' + button).text('chevron_left');
+ }
+ else {
+ $('#instance-icon-' + button).text('chevron_right');
+ $('#instance-admin-icon-' + button).text('chevron_right');
+
+ }
+ $('#instance-delete-' + button).toggle();
+ $('#instance-admin-delete-' + button).toggle();
+}
+
diff --git a/openstack_lease_it/lease_it/tests.py b/openstack_lease_it/lease_it/tests.py
index 98e7d3d..1c5b41f 100644
--- a/openstack_lease_it/lease_it/tests.py
+++ b/openstack_lease_it/lease_it/tests.py
@@ -1,4 +1,87 @@
+"""
+This file tests the instances model with some plausible values.
+The database created for the test is destroyed right after.
+To test further, test_models creates a database that doesn't get destroyed.
+To proceed to the tests, run the django command :
+ manage.py test
+"""
+
# pylint: skip-file
+import django.core.exceptions
from django.test import TestCase
+from datetime import date
+from dateutil.relativedelta import relativedelta
+from lease_it.models import Instances
+from lease_it.client.run import instance_spy
+from openstack_lease_it.settings import GLOBAL_CONFIG, load_config
# Create your tests here.
+
+now = date.today()
+
+
+class InstancesTest(TestCase):
+ def setUp(self):
+ """
+ Filling the database with plausible values
+
+ :return: void
+ """
+ Instances.objects.create(id="instance-01",
+ heartbeat_at=now, leased_at=now - relativedelta(days=+119), lease_duration=120)
+ Instances.objects.create(id="instance-02",
+ heartbeat_at="2020-08-28", leased_at="2020-05-28", lease_duration=90)
+ Instances.objects.create(id="instance-03",
+ heartbeat_at="2020-09-25", leased_at="2020-05-28", lease_duration=120)
+ Instances.objects.create(id="instance-04",
+ heartbeat_at="2020-09-25", leased_at="2020-05-28", lease_duration=120)
+ Instances.objects.create(id="instance-05",
+ heartbeat_at="2021-08-28", leased_at=now - relativedelta(days=+89), lease_duration=90)
+
+ def test_instance_spy(self):
+ """
+ Testing the instance_spy function
+
+ :return: void
+ """
+
+ instance_spy()
+ # Only the instance with id "instance-02" should be deleted, as it's not excluded nor leased
+ self.assertIsInstance(Instances.objects.get(id="instance-01"), Instances)
+ self.assertRaisesMessage(django.core.exceptions.ObjectDoesNotExist, "Instances matching query does not exist.",
+ Instances.objects.get, id="instance-02")
+ self.assertIsInstance(Instances.objects.get(id="instance-03"), Instances)
+ self.assertIsInstance(Instances.objects.get(id="instance-04"), Instances)
+ self.assertIsInstance(Instances.objects.get(id="instance-05"), Instances)
+
+ load_config()
+ GLOBAL_CONFIG['EXCLUDE'].remove("Jane Smith")
+ instance_spy()
+ # Now instance_03 should be deleted, because it no longer benefits from the user exclusion
+ self.assertIsInstance(Instances.objects.get(id="instance-01"), Instances)
+ self.assertRaisesMessage(django.core.exceptions.ObjectDoesNotExist, "Instances matching query does not exist.",
+ Instances.objects.get, id="instance-03")
+ self.assertIsInstance(Instances.objects.get(id="instance-04"), Instances)
+ self.assertIsInstance(Instances.objects.get(id="instance-05"), Instances)
+
+ GLOBAL_CONFIG['EXCLUDE'].remove("project-01")
+ instance_spy()
+ # Now that all the exclusions are disabled, instance 04 should be deleted
+ self.assertIsInstance(Instances.objects.get(id="instance-01"), Instances)
+ self.assertRaisesMessage(django.core.exceptions.ObjectDoesNotExist, "Instances matching query does not exist.",
+ Instances.objects.get, id="instance-04")
+ self.assertIsInstance(Instances.objects.get(id="instance-05"), Instances)
+
+ Instances.objects.filter(id="instance-01").update(leased_at=now - relativedelta(days=+91))
+ Instances.objects.filter(id="instance-05").update(leased_at=now - relativedelta(days=+91))
+ instance_spy()
+ # Only instance-05 should be deleted, as its lease duration is 90 days and instance-01's is 120
+ self.assertIsInstance(Instances.objects.get(id="instance-01"), Instances)
+ self.assertRaisesMessage(django.core.exceptions.ObjectDoesNotExist, "Instances matching query does not exist.",
+ Instances.objects.get, id="instance-05")
+
+ Instances.objects.filter(id="instance-01").update(leased_at=now - relativedelta(days=+121))
+ instance_spy()
+ # Now that we are over instance-01's lease duration, it should get deleted
+ self.assertRaisesMessage(django.core.exceptions.ObjectDoesNotExist, "Instances matching query does not exist.",
+ Instances.objects.get, id="instance-01")
diff --git a/openstack_lease_it/lease_it/urls.py b/openstack_lease_it/lease_it/urls.py
index 2d57481..56fdfc1 100644
--- a/openstack_lease_it/lease_it/urls.py
+++ b/openstack_lease_it/lease_it/urls.py
@@ -14,21 +14,23 @@
"""
from django.conf.urls import url
+from lease_it import views
+
urlpatterns = [ # pylint: disable=invalid-name
# Default dashboard view
- url(r'^$', 'lease_it.views.dashboard', name='dashboard'),
+ url(r'^$', views.dashboard, name='dashboard'),
# Flavors view
- url(r'^flavors', 'lease_it.views.flavors', name='flavors'),
+ url(r'^flavors', views.flavors, name='flavors'),
# Instances view
- url(r'^instances[/]?$', 'lease_it.views.instances', name='instances'),
- url(r'^instances/(?P[\w-]+)$', 'lease_it.views.instance', name='instance'),
+ url(r'^instances[/]?$', views.instances, name='instances'),
+ url(r'^instances/(?P[\w-]+)$', views.instance, name='instance'),
# Database view
- url(r'^database[/]?$', 'lease_it.views.databases', name='databases'),
- url(r'^database/(?P[\w-]+)$', 'lease_it.views.database', name='database'),
+ url(r'^database[/]?$', views.databases, name='databases'),
+ url(r'^database/(?P[\w-]+)$', views.database, name='database'),
# Users view
- url(r'^users', 'lease_it.views.users', name='users')
+ url(r'^users', views.users, name='users')
]
diff --git a/openstack_lease_it/lease_it/views.py b/openstack_lease_it/lease_it/views.py
index 61e46cf..ddeaded 100644
--- a/openstack_lease_it/lease_it/views.py
+++ b/openstack_lease_it/lease_it/views.py
@@ -9,6 +9,7 @@
from django.shortcuts import render
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
+from django.core.cache import cache
from lease_it import backend
from lease_it.backend import Exceptions as bckExceptions # pylint: disable=ungrouped-imports
@@ -149,7 +150,7 @@ def databases(request): # pylint: disable=unused-argument
return JsonResponse(response, safe=False)
-@superuser_required
+@login_required
def database(request, instance_id): # pylint: disable=unused-argument
"""
This view is used to delete instance from database
@@ -163,7 +164,18 @@ def database(request, instance_id): # pylint: disable=unused-argument
'instance': {'id': instance_id}
}
try:
- InstancesAccess.delete(instance_id)
+ # We retrieve data from backend
+ user_instances = BACKEND.instances(request, True)
+ if request.user.is_superuser or instance_id in [k for k in user_instances]:
+ InstancesAccess.delete(instance_id)
+ BACKEND.delete([{'id': instance_id}])
+ cache.delete('instances')
+ else :
+ response = {
+ 'status': 'not allowed',
+ 'instance': {'id': instance_id}
+ }
+
except StillRunning as error:
response = {
'status': 'failed',
diff --git a/openstack_lease_it/openstack_lease_it/config.py b/openstack_lease_it/openstack_lease_it/config.py
index baeacdc..c792f41 100644
--- a/openstack_lease_it/openstack_lease_it/config.py
+++ b/openstack_lease_it/openstack_lease_it/config.py
@@ -7,7 +7,7 @@
This module also provide **GLOBAL_CONFIG** variable used to share configuration across module /
django apps.
"""
-import ConfigParser
+import configparser
import os
BASE_CONFIG_DIR = '/etc/openstack-lease-it'
@@ -30,6 +30,7 @@
'DJANGO_LOGDIR': '/var/log/openstack-lease-it/',
'DJANGO_LOGLEVEL': 'INFO',
'DJANGO_SECRET_KEY': 'Must_be_defined', # Must be defined to allow sphinx to run
+ 'RESET_CACHE': False,
# OpenStack parameters
'OS_USERNAME': 'admin',
@@ -37,10 +38,11 @@
'OS_PASSWORD': 'admin_password', # Must be defined to allow sphinx to run
'OS_PROJECT_NAME': 'admin',
'OS_AUTH_URL': 'https://keystone.example.com', # Must be defined to allow sphinx to run
- 'OS_IDENTITY_API_VERSION': '3',
+ 'OS_IDENTITY_API_VERSION': 3,
'OS_USER_DOMAIN_NAME': 'default',
'OS_PROJECT_DOMAIN_NAME': 'default',
'OS_CACERT': None, # If certificate is signed by a legit CA, we don't need to define it
+ 'OS_DELETE': True, # Actually deletes the VMs from Openstack (turn False for testing)
# memcached parameter
'MEMCACHED_HOST': '127.0.0.1',
@@ -53,7 +55,14 @@
'NOTIFICATION_DEBUG': 'False',
'NOTIFICATION_DOMAIN': '',
'NOTIFICATION_DELETE_CONTENT': BASE_CONFIG_DIR + '/delete-notification.txt',
- 'NOTIFICATION_LEASE_CONTENT': BASE_CONFIG_DIR + '/lease-notification.txt'
+ 'NOTIFICATION_LEASE_CONTENT': BASE_CONFIG_DIR + '/lease-notification.txt',
+
+ # Exclude initialisation : list of exclusions (projects, users,...)
+ 'EXCLUDE': [],
+
+ # Lease durations dictionary initialisation
+ # It takes the form {"user_id_" + user_id: lease_duration}
+ 'SPECIAL_LEASE_DURATION': dict()
}
"""
We use the global variable GLOBAL_CONFIG to share openstack-lease-it configuration to all user. Some
@@ -64,13 +73,15 @@
'DJANGO_SECRET_KEY': 'secret_key',
'DJANGO_DEBUG': 'debug',
'DJANGO_LOGDIR': 'log_dir',
- 'DJANGO_LOGLEVEL': 'log_level'
+ 'DJANGO_LOGLEVEL': 'log_level',
+ 'RESET_CACHE': 'reset_cache'
}
"""
- **DJANGO_SECRET_KEY**: The secret key used by django (file option: *secret_key*)
- **DJANGO_DEBUG**: The DEBUG value for django (file option: *debug*)
- **DJANGO_LOGDIR**: Directory where log file will be write (file option: *log_dir*)
- **DJANGO_LOGLEVEL**: The log level used for django (file option: *log_level*)
+ - **RESET_CACHE**: Enable / Disable reset of the cache when loading instances
"""
@@ -90,7 +101,8 @@
'OS_CACERT': 'OS_CACERT',
'OS_IDENTITY_API_VERSION': 'OS_IDENTITY_API_VERSION',
'OS_PROJECT_DOMAIN_NAME': 'OS_PROJECT_DOMAIN_NAME',
- 'OS_USER_DOMAIN_NAME': 'OS_USER_DOMAIN_NAME'
+ 'OS_USER_DOMAIN_NAME': 'OS_USER_DOMAIN_NAME',
+ 'OS_DELETE': 'OS_DELETE'
}
"""
- **OS_USERNAME**: Openstack admin username (file option: *OS_USERNAME*)
@@ -102,6 +114,7 @@
- **OS_IDENTITY_API_VERSION**: Keystone version (file option: *OS_IDENTITY_API_VERSION*)
- **OS_PROJECT_DOMAIN_NAME**: project domain name (file option: *OS_PROJECT_DOMAIN_NAME*)
- **OS_USER_DOMAIN_NAME**: user domain name (file option: *OS_USER_DOMAIN_NAME*)
+ - **OS_DELETE**: Deletes the VMs (turn False for testing)
"""
@@ -147,12 +160,19 @@
"""
+LISTS_OPTIONS = {
+}
+"""
+
+"""
+
SECTIONS = {
'django': DJANGO_OPTIONS,
'openstack': OPENSTACK_OPTIONS,
'memcached': MEMCACHED_OPTIONS,
'plugins': PLUGINS_OPTIONS,
- 'notification': NOTIFICATION_OPTIONS
+ 'notification': NOTIFICATION_OPTIONS,
+ 'lists': LISTS_OPTIONS
}
"""
@@ -161,6 +181,7 @@
- **memcached**: section [memcached]
- **plugins**: section [plugins]
- **notification**: section [notification]
+ - **lists**: lists [lists]
"""
@@ -174,14 +195,36 @@ def load_config_option(config, section):
:param section: The section of configuration file we compute
:return: void
"""
- options = SECTIONS[section]
+ section_exists = True
+ try:
+ options = SECTIONS[section]
+ except KeyError:
+ # The section is unknown, we must create the options dictionary to go through
+ section_exists = False
+ options = config.options(section)
+ options = dict(zip(options, options))
for option in options:
try:
- GLOBAL_CONFIG[option] = config.get(section,
- options[option])
- except ConfigParser.NoSectionError:
+ config_to_add = config.get(section, options[option])
+ if option == 'OS_IDENTITY_API_VERSION':
+ GLOBAL_CONFIG[option] = int(config_to_add)
+ else:
+ if section == "lists":
+ config_to_add = list(filter(None, [x.strip() for x in config_to_add.splitlines()]))
+ # If the first element is detected to be a tuple, we assume the whole list is a list of tuples
+ if config_to_add[0][0] == "(" and config_to_add[0][-1] == ")":
+ config_to_add = [tuple(x[1:-1].strip().split(',')) for x in config_to_add]
+ if section_exists:
+ GLOBAL_CONFIG[option] = config_to_add
+ else:
+ if "exclude" == option and bool(config_to_add) and section not in GLOBAL_CONFIG["EXCLUDE"]:
+ GLOBAL_CONFIG["EXCLUDE"].append(section)
+ if "duration" == option:
+ GLOBAL_CONFIG["SPECIAL_LEASE_DURATION"][section] = int(config_to_add)
+
+ except configparser.NoSectionError:
pass
- except ConfigParser.NoOptionError:
+ except configparser.NoOptionError:
pass
@@ -191,9 +234,10 @@ def load_config():
:return: void
"""
- config = ConfigParser.RawConfigParser()
+ config = configparser.RawConfigParser()
for config_file in CONFIG_FILES:
config.read(config_file)
- for section in SECTIONS:
- load_config_option(config, section)
+ sections = config.sections()
+ for section in sections:
+ load_config_option(config, section)
\ No newline at end of file
diff --git a/openstack_lease_it/openstack_lease_it/settings.py b/openstack_lease_it/openstack_lease_it/settings.py
index 533191e..11a6c95 100644
--- a/openstack_lease_it/openstack_lease_it/settings.py
+++ b/openstack_lease_it/openstack_lease_it/settings.py
@@ -14,9 +14,11 @@
import os
import ast
import logging
+import django
from openstack_lease_it.config import GLOBAL_CONFIG, load_config
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Load configuration
@@ -25,6 +27,7 @@
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = GLOBAL_CONFIG['DJANGO_SECRET_KEY']
+
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = ast.literal_eval(GLOBAL_CONFIG['DJANGO_DEBUG'])
@@ -211,3 +214,5 @@
LOGGER = logging.getLogger('main')
LOGGER_NOTIFICATION = logging.getLogger('notification')
LOGGER_INSTANCES = logging.getLogger('instances')
+
+django.setup()
diff --git a/openstack_lease_it/openstack_lease_it/urls.py b/openstack_lease_it/openstack_lease_it/urls.py
index c96b314..734cf32 100644
--- a/openstack_lease_it/openstack_lease_it/urls.py
+++ b/openstack_lease_it/openstack_lease_it/urls.py
@@ -17,6 +17,7 @@
from django.contrib import admin
from openstack_lease_it.settings import GLOBAL_CONFIG
+from openstack_lease_it import views
if GLOBAL_CONFIG['BACKEND_PLUGIN'] == 'Openstack':
urlpatterns = [ # pylint: disable=invalid-name
@@ -30,8 +31,8 @@
]
else:
urlpatterns = [ # pylint: disable=invalid-name
- url(r'^login', 'openstack_lease_it.views.login', name='login'),
- url(r'^logout', 'openstack_lease_it.views.logout', name='logout'),
+ url(r'^login', views.login, name='login'),
+ url(r'^logout', views.logout, name='logout'),
# We add default django admin view in case of Test backend to allow easiest user management
url(r'^admin', include(admin.site.urls)),
url(r'^', include('lease_it.urls'))
diff --git a/requirements.txt b/requirements.txt
index 4be24e9..c36d4f9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,26 +1,34 @@
alabaster==0.7.10
appdirs==1.4.3
+asgiref==3.3.4
Babel==2.4.0
+certifi==2020.12.5
+chardet==4.0.0
+configparser==5.0.2
debtcollector==1.13.0
-Django==1.8
-django-openstack-auth==3.1.1
+Django==1.11.29
+django-openstack-auth==3.6.1
docutils==0.14
funcsigs==1.0.2
+idna==2.10
imagesize==0.7.1
iso8601==0.1.11
Jinja2==2.9.6
-keystoneauth1==2.19.0
-MarkupSafe==1.0
+keystoneauth1==4.3.1
+MarkupSafe==2.0.1
monotonic==1.3
+msgpack==1.0.2
msgpack-python==0.4.8
netaddr==0.7.19
netifaces==0.10.5
-oslo.config==3.24.0
-oslo.i18n==3.15.0
-oslo.policy==1.22.0
+os-service-types==1.7.0
+oslo.config==8.7.0
+oslo.context==3.2.0
+oslo.i18n==5.0.1
+oslo.policy==3.8.0
oslo.serialization==2.18.0
-oslo.utils==3.25.0
-packaging==16.8
+oslo.utils==4.9.0
+packaging==20.9
pbr==2.0.0
positional==1.1.1
prettytable==0.7.2
@@ -31,14 +39,17 @@ python-keystoneclient==3.10.0
python-memcached==1.58
python-novaclient==8.0.0
pytz==2017.2
-PyYAML==3.12
-requests==2.12.5
-rfc3986==0.4.1
+PyYAML==5.4.1
+requests==2.25.1
+rfc3986==1.5.0
simplejson==3.10.0
six==1.10.0
snowballstemmer==1.2.1
Sphinx==1.6.5
+sphinxcontrib-serializinghtml==1.1.5
sphinxcontrib-websupport==1.0.1
+sqlparse==0.4.1
stevedore==1.21.0
typing==3.6.2
+urllib3==1.26.4
wrapt==1.10.10