diff --git a/server/requirements.txt b/server/requirements.txt index def9f25d8..b10a2f151 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -13,7 +13,6 @@ pyOpenSSL==16.2.0 mysqlclient==1.3.12 python-ldap==3.0.0b4 paramiko==2.3.1 -python-dateutil defusedxml==0.5.0 python-dateutil==2.6.1 requests==2.18.4 diff --git a/server/src/server/urls.py b/server/src/server/urls.py index 574d0271a..5b13368b5 100644 --- a/server/src/server/urls.py +++ b/server/src/server/urls.py @@ -2,7 +2,9 @@ """ Url patterns for UDS project (Django) """ -from django.conf.urls import include, url +from django.conf.urls import include +from django.urls import path + # Uncomment the next two lines to enable the admin: # from django.contrib import admin @@ -10,5 +12,5 @@ from django.conf.urls import include, url urlpatterns = [ - url(r'^', include('uds.urls')), + path('', include('uds.urls')), ] diff --git a/server/src/uds/core/auths/auth.py b/server/src/uds/core/auths/auth.py index 2a7c7bb3d..3dac5ff2b 100644 --- a/server/src/uds/core/auths/auth.py +++ b/server/src/uds/core/auths/auth.py @@ -54,7 +54,7 @@ from uds.models import User import logging import six -__updated__ = '2018-01-05' +__updated__ = '2018-02-26' logger = logging.getLogger(__name__) authLogger = logging.getLogger('authLog') diff --git a/server/src/uds/core/managers/PublicationManager.py b/server/src/uds/core/managers/PublicationManager.py index 6050d19ad..b15d21c13 100644 --- a/server/src/uds/core/managers/PublicationManager.py +++ b/server/src/uds/core/managers/PublicationManager.py @@ -132,8 +132,14 @@ class PublicationFinishChecker(DelayedTask): for old in servicePoolPub.deployed_service.publications.filter(state=State.USABLE): old.state = State.REMOVABLE old.save() - pc = PublicationOldMachinesCleaner(old.id) - pc.register(GlobalConfig.SESSION_EXPIRE_TIME.getInt(True) * 3600, 'pclean-' + str(old.id), True) + + osm = servicePoolPub.deployed_service.osmanager + # If os manager says "machine is persistent", do not tray to delete "previous version" assigned machines + doPublicationCleanup = True if osm is None else not osm.getInstance().isPersistent() + + if doPublicationCleanup: + pc = PublicationOldMachinesCleaner(old.id) + pc.register(GlobalConfig.SESSION_EXPIRE_TIME.getInt(True) * 3600, 'pclean-' + str(old.id), True) servicePoolPub.setState(State.USABLE) servicePoolPub.deployed_service.markOldUserServicesAsRemovables(servicePoolPub) @@ -205,7 +211,7 @@ class PublicationManager(object): """ Initiates the publication of a service pool, or raises an exception if this cannot be done :param servicePool: Service pool object (db object) - :param changeLog: if not Noe, store change log string on "change log" table + :param changeLog: if not None, store change log string on "change log" table """ if servicePool.publications.filter(state__in=State.PUBLISH_STATES).count() > 0: raise PublishException(_('Already publishing. Wait for previous publication to finish and try again')) diff --git a/server/src/uds/core/managers/UserServiceManager.py b/server/src/uds/core/managers/UserServiceManager.py index c9f89ffd4..7fd613f38 100644 --- a/server/src/uds/core/managers/UserServiceManager.py +++ b/server/src/uds/core/managers/UserServiceManager.py @@ -51,8 +51,6 @@ import requests import json import logging -__updated__ = '2018-02-16' - logger = logging.getLogger(__name__) traceLogger = logging.getLogger('traceLog') @@ -488,18 +486,20 @@ class UserServiceManager(object): This method is used by UserService when a request for setInUse(False) is made This checks that the service can continue existing or not """ - remove = False - # uService = UserService.objects.get(id=uService.id) - with transaction.atomic(): - uService = UserService.objects.select_for_update().get(id=uService.id) - if uService.publication is None: - return - if uService.publication.id != uService.deployed_service.activePublication().id: - logger.debug('Old revision of user service, marking as removable: {0}'.format(uService)) - remove = True + osm = uService.deployed_service.osmanager + # If os manager says "machine is persistent", do not tray to delete "previous version" assigned machines + doPublicationCleanup = True if osm is None else not osm.getInstance().isPersistent() - if remove: - uService.remove() + if doPublicationCleanup: + remove = False + with transaction.atomic(): + uService = UserService.objects.select_for_update().get(id=uService.id) + if uService.publication not is None and uService.publication.id != uService.deployed_service.activePublication().id: + logger.debug('Old revision of user service, marking as removable: {0}'.format(uService)) + remove = True + + if remove: + uService.remove() def notifyReadyFromOsManager(self, uService, data): try: diff --git a/server/src/uds/core/osmanagers/BaseOsManager.py b/server/src/uds/core/osmanagers/BaseOsManager.py index 96755a493..4b56a5fe4 100644 --- a/server/src/uds/core/osmanagers/BaseOsManager.py +++ b/server/src/uds/core/osmanagers/BaseOsManager.py @@ -42,7 +42,7 @@ from uds.core import Module import six -__updated__ = '2017-10-02' +__updated__ = '2018-03-02' STORAGE_KEY = 'osmk' @@ -260,6 +260,13 @@ class OSManager(Module): if save: userService.save() + def isPersistent(self): + ''' + When a publication if finished, old assigned machines will be removed if this value is True. + Defaults to False + ''' + return False + def __str__(self): return "Base OS Manager" diff --git a/server/src/uds/models/Authenticator.py b/server/src/uds/models/Authenticator.py index c16713f02..6a7cba2ef 100644 --- a/server/src/uds/models/Authenticator.py +++ b/server/src/uds/models/Authenticator.py @@ -48,7 +48,7 @@ import logging logger = logging.getLogger(__name__) -__updated__ = '2016-02-26' +__updated__ = '2018-02-26' @python_2_unicode_compatible @@ -176,6 +176,28 @@ class Authenticator(ManagedObjectModel, TaggingMixin): """ return Authenticator.objects.all().order_by('priority') + @staticmethod + def getByTag(tag=None): + ''' + Gets authenticator by tag name. + Special tag name "disabled" is used to exclude customAuth + ''' + from uds.core.util.Config import GlobalConfig + + auths = [] + if tag is not None: + auths = Authenticator.objects.filter(small_name=tag).order_by('priority', 'name') + if auths.count() == 0: + auths = Authenticator.objects.all().order_by('priority', 'name') + # If disallow global login (use all auths), get just the first by priority/name + if GlobalConfig.DISALLOW_GLOBAL_LOGIN.getBool(False) is True: + auths = auths[0:1] + logger.debug(auths) + else: + auths = Authenticator.objects.all().order_by('priority', 'name') + + return [auth for auth in auths if auth.getType().isCustom() is False or tag != 'disabled'] + @staticmethod def beforeDelete(sender, **kwargs): """ @@ -206,5 +228,6 @@ class Authenticator(ManagedObjectModel, TaggingMixin): def __str__(self): return u"{0} of type {1} (id:{2})".format(self.name, self.data_type, self.id) + # Connects a pre deletion signal to Authenticator signals.pre_delete.connect(Authenticator.beforeDelete, sender=Authenticator) diff --git a/server/src/uds/osmanagers/LinuxOsManager/LinuxOsManager.py b/server/src/uds/osmanagers/LinuxOsManager/LinuxOsManager.py index 2d3dc8898..d71777631 100644 --- a/server/src/uds/osmanagers/LinuxOsManager/LinuxOsManager.py +++ b/server/src/uds/osmanagers/LinuxOsManager/LinuxOsManager.py @@ -201,6 +201,9 @@ class LinuxOsManager(osmanagers.OSManager): if self._onLogout == 'remove': userService.release() + def isPersistent(self): + return not self._onLogout == 'remove' + def checkState(self, service): logger.debug('Checking state for service {0}'.format(service)) return State.RUNNING diff --git a/server/src/uds/osmanagers/WindowsOsManager/WindowsOsManager.py b/server/src/uds/osmanagers/WindowsOsManager/WindowsOsManager.py index b193005d7..39ab04a90 100644 --- a/server/src/uds/osmanagers/WindowsOsManager/WindowsOsManager.py +++ b/server/src/uds/osmanagers/WindowsOsManager/WindowsOsManager.py @@ -244,6 +244,9 @@ class WindowsOsManager(osmanagers.OSManager): if self._onLogout == 'remove': userService.release() + def isPersistent(self): + return not self._onLogout == 'remove' + def checkState(self, service): logger.debug('Checking state for service {0}'.format(service)) return State.RUNNING diff --git a/server/src/uds/services/OpenStack/openStack/UDSOpenStackClient.py b/server/src/uds/services/OpenStack/openStack/UDSOpenStackClient.py index 63ea2099e..1bc56b923 100644 --- a/server/src/uds/services/OpenStack/openStack/UDSOpenStackClient.py +++ b/server/src/uds/services/OpenStack/openStack/UDSOpenStackClient.py @@ -33,33 +33,26 @@ # pylint: disable=maybe-no-member,protected-access from django.utils.translation import ugettext as _ -from uds.core.util.Cache import Cache - import logging import requests import json import dateutil.parser -import hashlib import six - -__updated__ = '2017-11-13' +__updated__ = '2018-03-02' logger = logging.getLogger(__name__) # Required: Authentication v3 - -# This is a vary basic implementation for what we need from openstack +# This is an implementation for what we need from openstack # This does not includes (nor it is intention) full API implementation, just the parts we need -# Theese are related to auth, compute & network basically - -# In case we Cache time for endpoints. This is more likely to not change never, so we will tray to keep it as long as we can (1 hour for example?) -# ENDPOINTS_TIMEOUT = 1 * 3600 +# These are related to auth, compute & network basically # Do not verify SSL conections right now VERIFY_SSL = False + # Helpers def ensureResponseIsValid(response, errMsg=None): if response.ok is False: @@ -96,28 +89,30 @@ def getRecurringUrlJson(url, headers, key, params=None, errMsg=None, timeout=10) # Decorators def authRequired(func): + def ensurer(obj, *args, **kwargs): obj.ensureAuthenticated() try: return func(obj, *args, **kwargs) except Exception as e: logger.error('Got error {} for openstack'.format(e)) - obj._cleanCache() # On any request error, force next time auth raise + return ensurer + def authProjectRequired(func): + def ensurer(obj, *args, **kwargs): if obj._projectId is None: raise Exception('Need a project for method {}'.format(func)) obj.ensureAuthenticated() return func(obj, *args, **kwargs) + return ensurer class Client(object): - cache = Cache('uds-openstack') - PUBLIC = 'public' PRIVATE = 'private' INTERNAL = 'url' @@ -138,18 +133,6 @@ class Client(object): self._authUrl = 'http{}://{}:{}/'.format('s' if useSSL else '', host, port) - # Generates a hash for auth + credentials - h = hashlib.md5() - h.update(six.binary_type(host)) - h.update(six.binary_type(port)) - h.update(six.binary_type(domain)) - h.update(six.binary_type(username)) - h.update(six.binary_type(password)) - h.update(six.binary_type(useSSL)) - h.update(six.binary_type(projectId)) - h.update(six.binary_type(region)) - self._cacheKey = h.hexdigest() - def _getEndpointFor(self, type_): # If no region is indicatad, first endpoint is returned for i in self._catalog: if i['type'] == type_: @@ -164,38 +147,7 @@ class Client(object): return headers - def _getFromCache(self): - cached = self.cache.get(self._cacheKey) - if cached is not None: - self._authenticated = True - self._tokenId = cached['tokenId'] - # Extract the token id - self._userId = cached['userId'] - self._projectId = cached['projectId'] - self._catalog = cached['catalog'] - - return True - - return False - - def _saveToCache(self, validity=600): - self.cache.put(self._cacheKey, - { - 'tokenId': self._tokenId, - 'userId': self._userId, - 'projectId': self._projectId, - 'catalog': self._catalog - }, - validity - 60) # We substract some seconds to allow some time desynchronization - - def _clearCache(self): - self.cache.remove(self._cacheKey) - def authPassword(self): - # If cached data exists, use it as auth - if self._getFromCache() is True: - return - data = { 'auth': { 'identity': { @@ -239,20 +191,16 @@ class Client(object): self._userId = token['user']['id'] validity = (dateutil.parser.parse(token['expires_at']).replace(tzinfo=None) - dateutil.parser.parse(token['issued_at']).replace(tzinfo=None)).seconds - 60 - logger.debug('The token {} will be valid for {}'.format(self._tokenId, validity)) - # Now, if endpoints are present (only if tenant was specified), store & cache them + # Now, if endpoints are present (only if tenant was specified), store them if self._projectId is not None: self._catalog = token['catalog'] - self._saveToCache(validity) - def ensureAuthenticated(self): if self._authenticated is False: self.authPassword() - @authRequired def listProjects(self): return getRecurringUrlJson(self._authUrl + 'v3/users/{user_id}/projects'.format(user_id=self._userId), @@ -261,7 +209,6 @@ class Client(object): errMsg='List Projects', timeout=self._timeout) - @authRequired def listRegions(self): return getRecurringUrlJson(self._authUrl + 'v3/regions/', @@ -270,7 +217,6 @@ class Client(object): errMsg='List Regions', timeout=self._timeout) - @authProjectRequired def listServers(self, detail=False, params=None): path = '/servers/' + 'detail' if detail is True else '' @@ -281,7 +227,6 @@ class Client(object): errMsg='List Vms', timeout=self._timeout) - @authProjectRequired def listImages(self): return getRecurringUrlJson(self._getEndpointFor('image') + '/v2/images?status=active', @@ -290,7 +235,6 @@ class Client(object): errMsg='List Images', timeout=self._timeout) - @authProjectRequired def listVolumeTypes(self): return getRecurringUrlJson(self._getEndpointFor('volumev2') + '/types', @@ -299,7 +243,6 @@ class Client(object): errMsg='List Volume Types', timeout=self._timeout) - @authProjectRequired def listVolumes(self): # self._getEndpointFor('volumev2') + '/volumes' @@ -309,7 +252,6 @@ class Client(object): errMsg='List Volumes', timeout=self._timeout) - @authProjectRequired def listVolumeSnapshots(self, volumeId=None): for s in getRecurringUrlJson(self._getEndpointFor('volumev2') + '/snapshots', @@ -320,7 +262,6 @@ class Client(object): if volumeId is None or s['volume_id'] == volumeId: yield s - @authProjectRequired def listAvailabilityZones(self): for az in getRecurringUrlJson(self._getEndpointFor('compute') + '/os-availability-zone', @@ -331,7 +272,6 @@ class Client(object): if az['zoneState']['available'] is True: yield az['zoneName'] - @authProjectRequired def listFlavors(self): return getRecurringUrlJson(self._getEndpointFor('compute') + '/flavors', @@ -340,7 +280,6 @@ class Client(object): errMsg='List Flavors', timeout=self._timeout) - @authProjectRequired def listNetworks(self): return getRecurringUrlJson(self._getEndpointFor('network') + '/v2.0/networks', @@ -372,7 +311,6 @@ class Client(object): errMsg='List security groups', timeout=self._timeout) - @authProjectRequired def getServer(self, serverId): r = requests.get(self._getEndpointFor('compute') + '/servers/{server_id}'.format(server_id=serverId), @@ -396,7 +334,6 @@ class Client(object): return v - @authProjectRequired def getSnapshot(self, snapshotId): """ @@ -414,7 +351,6 @@ class Client(object): return v - @authProjectRequired def updateSnapshot(self, snapshotId, name=None, description=None): data = { 'snapshot': {} } @@ -436,7 +372,6 @@ class Client(object): return v - @authProjectRequired def createVolumeSnapshot(self, volumeId, name, description=None): description = 'UDS Snapshot' if description is None else description @@ -461,7 +396,6 @@ class Client(object): return r.json()['snapshot'] - @authProjectRequired def createVolumeFromSnapshot(self, snapshotId, name, description=None): description = 'UDS Volume' if description is None else description @@ -520,7 +454,6 @@ class Client(object): return r.json()['server'] - @authProjectRequired def deleteServer(self, serverId): r = requests.post(self._getEndpointFor('compute') + '/servers/{server_id}/action'.format(server_id=serverId), @@ -533,7 +466,6 @@ class Client(object): # This does not returns anything - @authProjectRequired def deleteSnapshot(self, snapshotId): r = requests.delete(self._getEndpointFor('volumev2') + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshotId), @@ -545,7 +477,6 @@ class Client(object): # Does not returns a message body - @authProjectRequired def startServer(self, serverId): r = requests.post(self._getEndpointFor('compute') + '/servers/{server_id}/action'.format(server_id=serverId), @@ -558,7 +489,6 @@ class Client(object): # This does not returns anything - @authProjectRequired def stopServer(self, serverId): r = requests.post(self._getEndpointFor('compute') + '/servers/{server_id}/action'.format(server_id=serverId), @@ -589,7 +519,6 @@ class Client(object): ensureResponseIsValid(r, 'Resuming server') - def testConnection(self): # First, ensure requested api is supported # We need api version 3.2 or greater diff --git a/server/src/uds/templates/uds/html5/login.html b/server/src/uds/templates/uds/html5/login.html index 86b04eae6..77febe2c7 100644 --- a/server/src/uds/templates/uds/html5/login.html +++ b/server/src/uds/templates/uds/html5/login.html @@ -1,9 +1,11 @@ {% extends "uds/html5/templates/base.html" %} -{% load i18n static html5 %} +{% load i18n static html5 uds %} {% block title %}{% trans 'Welcome to UDS' %}{% endblock %} {% block js %} +{% javascript_auths authenticators %} + +