forked from shaba/openuds
Fixed multy phisical machines service to add a "custom" maximum duration for assignation
This commit is contained in:
parent
f4e953c9c9
commit
91d2398ade
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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']
|
||||||
|
Loading…
Reference in New Issue
Block a user