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 ' + ' '; 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 ' + + ' '; + } + else { + var menu = '' + + 'chevron_right ' + + ' '; + + }; + 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