Fixed multy phisical machines service to add a "custom" maximum duration for assignation

This commit is contained in:
Adolfo Gómez García 2021-07-21 13:59:12 +02:00
parent f4e953c9c9
commit 91d2398ade
3 changed files with 77 additions and 21 deletions

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2012-2019 Virtual Cable S.L. # Copyright (c) 2012-2021 Virtual Cable S.L.U.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -12,7 +12,7 @@
# * Redistributions in binary form must reproduce the above copyright notice, # * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors # * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software # may be used to endorse or promote products derived from this software
# without specific prior written permission. # without specific prior written permission.
# #
@ -38,7 +38,10 @@ from uds.core.ui import gui
from uds.core import auths from uds.core import auths
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from django.http import HttpRequest, HttpResponse # pylint: disable=ungrouped-imports from django.http import (
HttpRequest,
HttpResponse,
) # pylint: disable=ungrouped-imports
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -125,7 +128,9 @@ class SampleAuth(auths.Authenticator):
# unserialization, and at this point all will be default values # unserialization, and at this point all will be default values
# so self.groups.value will be [] # so self.groups.value will be []
if values and len(self.groups.value) < 2: if values and len(self.groups.value) < 2:
raise auths.Authenticator.ValidationException(_('We need more than two groups!')) raise auths.Authenticator.ValidationException(
_('We need more than two groups!')
)
def searchUsers(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]: def searchUsers(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]:
""" """
@ -137,7 +142,13 @@ class SampleAuth(auths.Authenticator):
facility for users. In our case, we will simply return a list of users facility for users. In our case, we will simply return a list of users
(array of dictionaries with ids and names) with the pattern plus 1..10 (array of dictionaries with ids and names) with the pattern plus 1..10
""" """
return [{'id': '{0}-{1}'.format(pattern, a), 'name': '{0} number {1}'.format(pattern, a)} for a in range(1, 10)] return [
{
'id': '{0}-{1}'.format(pattern, a),
'name': '{0} number {1}'.format(pattern, a),
}
for a in range(1, 10)
]
def searchGroups(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]: def searchGroups(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]:
""" """
@ -154,7 +165,9 @@ class SampleAuth(auths.Authenticator):
res.append({'id': g, 'name': ''}) res.append({'id': g, 'name': ''})
return res return res
def authenticate(self, username: str, credentials: str, groupsManager: 'auths.GroupsManager') -> bool: def authenticate(
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
) -> bool:
""" """
This method is invoked by UDS whenever it needs an user to be authenticated. This method is invoked by UDS whenever it needs an user to be authenticated.
It is used from web interface, but also from administration interface to It is used from web interface, but also from administration interface to
@ -196,7 +209,9 @@ class SampleAuth(auths.Authenticator):
:note: groupsManager is an in/out parameter :note: groupsManager is an in/out parameter
""" """
if username != credentials: # All users with same username and password are allowed if (
username != credentials
): # All users with same username and password are allowed
return False return False
# Now the tricky part. We will make this user belong to groups that contains at leat # Now the tricky part. We will make this user belong to groups that contains at leat
@ -247,11 +262,17 @@ class SampleAuth(auths.Authenticator):
# I know, this is a bit ugly, but this is just a sample :-) # I know, this is a bit ugly, but this is just a sample :-)
res = '<p>Login name: <input id="logname" type="text"/></p>' res = '<p>Login name: <input id="logname" type="text"/></p>'
res += '<p><a href="" onclick="window.location.replace(\'' + self.callbackUrl() + '?user=' res += (
'<p><a href="" onclick="window.location.replace(\''
+ self.callbackUrl()
+ '?user='
)
res += '\' + $(\'#logname\').val()); return false;">Login</a></p>' res += '\' + $(\'#logname\').val()); return false;">Login</a></p>'
return res return res
def authCallback(self, parameters: typing.Dict[str, typing.Any], gm: 'auths.GroupsManager') -> typing.Optional[str]: def authCallback(
self, parameters: typing.Dict[str, typing.Any], gm: 'auths.GroupsManager'
) -> typing.Optional[str]:
""" """
We provide this as a sample of callback for an user. We provide this as a sample of callback for an user.
We will accept all petitions that has "user" parameter We will accept all petitions that has "user" parameter
@ -286,6 +307,7 @@ class SampleAuth(auths.Authenticator):
Here, we will set the state to "Inactive" and realName to the same as username, but twice :-) Here, we will set the state to "Inactive" and realName to the same as username, but twice :-)
""" """
from uds.core.util.state import State from uds.core.util.state import State
usrData['real_name'] = usrData['name'] + ' ' + usrData['name'] usrData['real_name'] = usrData['name'] + ' ' + usrData['name']
usrData['state'] = State.INACTIVE usrData['state'] = State.INACTIVE

View File

@ -97,6 +97,19 @@ class IPMachinesService(IPServiceBase):
tab=gui.ADVANCED_TAB, tab=gui.ADVANCED_TAB,
) )
maxSessionForMachine = gui.NumericField(
length=3,
label=_('Max session per machine'),
defvalue='0',
order=3,
tooltip=_('Maximum session duration before UDS thinks this machine got locked and releases it (hours). 0 means "never".'),
minValue=0,
required=True,
tab=gui.ADVANCED_TAB,
)
# Description of service # Description of service
typeName = _('Static Multiple IP') typeName = _('Static Multiple IP')
typeType = 'IPMachinesService' typeType = 'IPMachinesService'
@ -120,6 +133,7 @@ class IPMachinesService(IPServiceBase):
_token: str = '' _token: str = ''
_port: int = 0 _port: int = 0
_skipTimeOnFailure: int = 0 _skipTimeOnFailure: int = 0
_maxSessionForMachine: int = 0
def initialize(self, values: 'Module.ValuesType') -> None: def initialize(self, values: 'Module.ValuesType') -> None:
if values is None: if values is None:
@ -141,7 +155,7 @@ class IPMachinesService(IPServiceBase):
d = self.storage.readData('ips') d = self.storage.readData('ips')
old_ips = pickle.loads(d) if d and isinstance(d, bytes) else [] old_ips = pickle.loads(d) if d and isinstance(d, bytes) else []
# dissapeared ones # dissapeared ones
dissapeared = set(i.split('~')[0] for i in old_ips) - set(i.split('~')[0] for i in self._ips) dissapeared = set(IPServiceBase.getIp(i.split('~')[0]) for i in old_ips) - set(i.split('~')[0] for i in self._ips)
with transaction.atomic(): with transaction.atomic():
for removable in dissapeared: for removable in dissapeared:
self.storage.remove(removable) self.storage.remove(removable)
@ -149,6 +163,7 @@ class IPMachinesService(IPServiceBase):
self._token = self.token.value.strip() self._token = self.token.value.strip()
self._port = self.port.value self._port = self.port.value
self._skipTimeOnFailure = self.skipTimeOnFailure.num() self._skipTimeOnFailure = self.skipTimeOnFailure.num()
self._maxSessionForMachine = self.maxSessionForMachine.num()
def getToken(self): def getToken(self):
return self._token or None return self._token or None
@ -161,16 +176,18 @@ class IPMachinesService(IPServiceBase):
'token': self._token, 'token': self._token,
'port': str(self._port), 'port': str(self._port),
'skipTimeOnFailure': str(self._skipTimeOnFailure), 'skipTimeOnFailure': str(self._skipTimeOnFailure),
'maxSessionForMachine': str(self._maxSessionForMachine),
} }
def marshal(self) -> bytes: def marshal(self) -> bytes:
self.storage.saveData('ips', pickle.dumps(self._ips)) self.storage.saveData('ips', pickle.dumps(self._ips))
return b'\0'.join( return b'\0'.join(
[ [
b'v4', b'v5',
self._token.encode(), self._token.encode(),
str(self._port).encode(), str(self._port).encode(),
str(self._skipTimeOnFailure).encode(), str(self._skipTimeOnFailure).encode(),
str(self._maxSessionForMachine).encode(),
] ]
) )
@ -186,24 +203,41 @@ class IPMachinesService(IPServiceBase):
self._ips = [] self._ips = []
if values[0] != b'v1': if values[0] != b'v1':
self._token = values[1].decode() self._token = values[1].decode()
if values[0] in (b'v3', b'v4'): if values[0] in (b'v3', b'v4', b'v5'):
self._port = int(values[2].decode()) self._port = int(values[2].decode())
if values[0] == b'v4': if values[0] in (b'v4', b'v5'):
self._skipTimeOnFailure = int(values[3].decode()) self._skipTimeOnFailure = int(values[3].decode())
if values[0] == b'v5':
self._maxSessionForMachine = int(values[4].decode())
# Sets maximum services for this # Sets maximum services for this
self.maxDeployed = len(self._ips) self.maxDeployed = len(self._ips)
def canBeUsed(self, locked: typing.Optional[int], now: int) -> int:
# If _maxSessionForMachine is 0, it can be used only if not locked
# (that is locked is None)
if self._maxSessionForMachine <= 0:
return bool(locked)
if not isinstance(locked, int): # May have "old" data, that was the IP repeated
return False
if not locked or locked < now - self._maxSessionForMachine * 3600:
return True
return False
def getUnassignedMachine(self) -> typing.Optional[str]: def getUnassignedMachine(self) -> typing.Optional[str]:
# Search first unassigned machine # Search first unassigned machine
try: try:
now = getSqlDatetimeAsUnix() now = getSqlDatetimeAsUnix()
consideredFreeTime = now - config.GlobalConfig.SESSION_EXPIRE_TIME.getInt(force=False) * 3600
for ip in self._ips: for ip in self._ips:
theIP = IPServiceBase.getIp(ip) theIP = IPServiceBase.getIp(ip)
theMAC = IPServiceBase.getMac(ip) theMAC = IPServiceBase.getMac(ip)
locked = self.storage.getPickle(theIP) locked = self.storage.getPickle(theIP)
if not locked or locked < consideredFreeTime: if self.canBeUsed(locked, now):
if self._port > 0 and self._skipTimeOnFailure > 0 and self.cache.get('port{}'.format(theIP)): if self._port > 0 and self._skipTimeOnFailure > 0 and self.cache.get('port{}'.format(theIP)):
continue # The check failed not so long ago, skip it... continue # The check failed not so long ago, skip it...
self.storage.putPickle(theIP, now) self.storage.putPickle(theIP, now)
@ -246,9 +280,7 @@ class IPMachinesService(IPServiceBase):
def unassignMachine(self, ip: str) -> None: def unassignMachine(self, ip: str) -> None:
try: try:
if ';' in ip: self.storage.remove(IPServiceBase.getIp(ip))
ip = ip.split(';')[0] # ; means that HAS an attached MAC
self.storage.remove(ip)
except Exception: except Exception:
logger.exception("Exception at getUnassignedMachine") logger.exception("Exception at getUnassignedMachine")
@ -271,8 +303,10 @@ class IPMachinesService(IPServiceBase):
theIP = IPServiceBase.getIp(assignableId) theIP = IPServiceBase.getIp(assignableId)
theMAC = IPServiceBase.getMac(assignableId) theMAC = IPServiceBase.getMac(assignableId)
if self.storage.readData(theIP) is None: now = getSqlDatetimeAsUnix()
self.storage.saveData(theIP, theIP) locked = self.storage.getPickle(theIP)
if self.canBeUsed(locked, now):
self.storage.saveData(theIP, now)
if theMAC: if theMAC:
theIP += ';' + theMAC theIP += ';' + theMAC
return userServiceInstance.assign(theIP) return userServiceInstance.assign(theIP)

View File

@ -86,7 +86,7 @@ def authCallback(request: HttpRequest, authName: str) -> HttpResponse:
return errors.exceptionView(request, e) return errors.exceptionView(request, e)
def authCallback_stage2(request: ExtendedHttpRequestWithUser, ticketId: str) -> HttpResponse: def authCallback_stage2(request: 'ExtendedHttpRequestWithUser', ticketId: str) -> HttpResponse:
try: try:
ticket = TicketStore.get(ticketId) ticket = TicketStore.get(ticketId)
params: typing.Dict[str, typing.Any] = ticket['params'] params: typing.Dict[str, typing.Any] = ticket['params']