mirror of
https://github.com/dkmstr/openuds.git
synced 2025-01-27 14:03:53 +03:00
Fixing up new servers model
This commit is contained in:
parent
f3ddb68381
commit
8def04ccd7
@ -91,7 +91,7 @@ def logOperation(
|
|||||||
|
|
||||||
username = handler.request.user.pretty_name if handler.request.user else 'Unknown'
|
username = handler.request.user.pretty_name if handler.request.user else 'Unknown'
|
||||||
doLog(
|
doLog(
|
||||||
None,
|
None, # > None Objects goes to SYSLOG (global log)
|
||||||
level=level,
|
level=level,
|
||||||
message=f'{handler.request.ip} [{username}]: [{handler.request.method}/{response_code}] {path}'[
|
message=f'{handler.request.ip} [{username}]: [{handler.request.method}/{response_code}] {path}'[
|
||||||
:4096
|
:4096
|
||||||
|
@ -35,6 +35,7 @@ import typing
|
|||||||
|
|
||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from traitlets import default
|
||||||
|
|
||||||
from uds.core import messaging, types
|
from uds.core import messaging, types
|
||||||
from uds.core.environment import Environment
|
from uds.core.environment import Environment
|
||||||
@ -60,6 +61,7 @@ class Notifiers(ModelHandler):
|
|||||||
'comments',
|
'comments',
|
||||||
'level',
|
'level',
|
||||||
'tags',
|
'tags',
|
||||||
|
'enabled',
|
||||||
]
|
]
|
||||||
|
|
||||||
table_title = typing.cast(str, _('Notifiers'))
|
table_title = typing.cast(str, _('Notifiers'))
|
||||||
@ -67,6 +69,7 @@ class Notifiers(ModelHandler):
|
|||||||
{'name': {'title': _('Name'), 'visible': True, 'type': 'iconType'}},
|
{'name': {'title': _('Name'), 'visible': True, 'type': 'iconType'}},
|
||||||
{'type_name': {'title': _('Type')}},
|
{'type_name': {'title': _('Type')}},
|
||||||
{'level': {'title': _('Level')}},
|
{'level': {'title': _('Level')}},
|
||||||
|
{'enabled': {'title': _('Enabled')}},
|
||||||
{'comments': {'title': _('Comments')}},
|
{'comments': {'title': _('Comments')}},
|
||||||
{'tags': {'title': _('tags'), 'visible': False}},
|
{'tags': {'title': _('tags'), 'visible': False}},
|
||||||
]
|
]
|
||||||
@ -94,6 +97,14 @@ class Notifiers(ModelHandler):
|
|||||||
'tooltip': gettext('Level of notifications'),
|
'tooltip': gettext('Level of notifications'),
|
||||||
'type': types.ui.FieldType.CHOICE,
|
'type': types.ui.FieldType.CHOICE,
|
||||||
'order': 102,
|
'order': 102,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'label': gettext('Enabled'),
|
||||||
|
'tooltip': gettext('If checked, this notifier will be used'),
|
||||||
|
'type': types.ui.FieldType.CHECKBOX,
|
||||||
|
'order': 103,
|
||||||
|
'default': True,
|
||||||
}
|
}
|
||||||
]:
|
]:
|
||||||
self.addField(localGui, field)
|
self.addField(localGui, field)
|
||||||
@ -106,7 +117,8 @@ class Notifiers(ModelHandler):
|
|||||||
return {
|
return {
|
||||||
'id': item.uuid,
|
'id': item.uuid,
|
||||||
'name': item.name,
|
'name': item.name,
|
||||||
'level': item.level,
|
'level': str(item.level),
|
||||||
|
'enabled': item.enabled,
|
||||||
'tags': [tag.tag for tag in item.tags.all()],
|
'tags': [tag.tag for tag in item.tags.all()],
|
||||||
'comments': item.comments,
|
'comments': item.comments,
|
||||||
'type': type_.type(),
|
'type': type_.type(),
|
||||||
|
@ -37,7 +37,7 @@ import typing
|
|||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from uds.core import consts, transports, types
|
from uds.core import consts, transports, types, ui
|
||||||
from uds.core.environment import Environment
|
from uds.core.environment import Environment
|
||||||
from uds.core.util import ensure, permissions
|
from uds.core.util import ensure, permissions
|
||||||
from uds.models import Network, ServicePool, Transport
|
from uds.models import Network, ServicePool, Transport
|
||||||
@ -100,17 +100,14 @@ class Transports(ModelHandler):
|
|||||||
'name': 'allowed_oss',
|
'name': 'allowed_oss',
|
||||||
'value': [],
|
'value': [],
|
||||||
'choices': sorted(
|
'choices': sorted(
|
||||||
[
|
[ui.gui.choiceItem(x.name, x.name) for x in consts.os.KNOWN_OS_LIST],
|
||||||
{'id': x.name, 'text': x.name}
|
|
||||||
for x in consts.os.KNOWN_OS_LIST
|
|
||||||
],
|
|
||||||
key=lambda x: x['text'].lower(),
|
key=lambda x: x['text'].lower(),
|
||||||
),
|
),
|
||||||
'label': gettext('Allowed Devices'),
|
'label': gettext('Allowed Devices'),
|
||||||
'tooltip': gettext(
|
'tooltip': gettext(
|
||||||
'If empty, any kind of device compatible with this transport will be allowed. Else, only devices compatible with selected values will be allowed'
|
'If empty, any kind of device compatible with this transport will be allowed. Else, only devices compatible with selected values will be allowed'
|
||||||
),
|
),
|
||||||
'type': 'multichoice',
|
'type': types.ui.FieldType.MULTICHOICE,
|
||||||
'tab': types.ui.Tab.ADVANCED,
|
'tab': types.ui.Tab.ADVANCED,
|
||||||
'order': 102,
|
'order': 102,
|
||||||
},
|
},
|
||||||
@ -121,13 +118,15 @@ class Transports(ModelHandler):
|
|||||||
'name': 'pools',
|
'name': 'pools',
|
||||||
'value': [],
|
'value': [],
|
||||||
'choices': [
|
'choices': [
|
||||||
{'id': x.uuid, 'text': x.name}
|
ui.gui.choiceItem(x.uuid, x.name)
|
||||||
for x in ServicePool.objects.filter(service__isnull=False).order_by('name').prefetch_related('service')
|
for x in ServicePool.objects.filter(service__isnull=False)
|
||||||
if transportType.protocol in x.service.getType().allowedProtocols
|
.order_by('name')
|
||||||
|
.prefetch_related('service')
|
||||||
|
if transportType.protocol in x.service.getType().allowedProtocols
|
||||||
],
|
],
|
||||||
'label': gettext('Service Pools'),
|
'label': gettext('Service Pools'),
|
||||||
'tooltip': gettext('Currently assigned services pools'),
|
'tooltip': gettext('Currently assigned services pools'),
|
||||||
'type': 'multichoice',
|
'type': types.ui.FieldType.MULTICHOICE,
|
||||||
'order': 103,
|
'order': 103,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -138,10 +137,8 @@ class Transports(ModelHandler):
|
|||||||
'length': 32,
|
'length': 32,
|
||||||
'value': '',
|
'value': '',
|
||||||
'label': gettext('Label'),
|
'label': gettext('Label'),
|
||||||
'tooltip': gettext(
|
'tooltip': gettext('Metapool transport label (only used on metapool transports grouping)'),
|
||||||
'Metapool transport label (only used on metapool transports grouping)'
|
'type': types.ui.FieldType.TEXT,
|
||||||
),
|
|
||||||
'type': 'text',
|
|
||||||
'order': 201,
|
'order': 201,
|
||||||
'tab': types.ui.Tab.ADVANCED,
|
'tab': types.ui.Tab.ADVANCED,
|
||||||
},
|
},
|
||||||
@ -152,7 +149,7 @@ class Transports(ModelHandler):
|
|||||||
def item_as_dict(self, item: 'Model') -> typing.Dict[str, typing.Any]:
|
def item_as_dict(self, item: 'Model') -> typing.Dict[str, typing.Any]:
|
||||||
item = ensure.is_instance(item, Transport)
|
item = ensure.is_instance(item, Transport)
|
||||||
type_ = item.getType()
|
type_ = item.getType()
|
||||||
pools = [{'id': x.uuid} for x in item.deployedServices.all()]
|
pools = list(item.deployedServices.all().values_list('uuid', flat=True))
|
||||||
return {
|
return {
|
||||||
'id': item.uuid,
|
'id': item.uuid,
|
||||||
'name': item.name,
|
'name': item.name,
|
||||||
@ -161,10 +158,8 @@ class Transports(ModelHandler):
|
|||||||
'priority': item.priority,
|
'priority': item.priority,
|
||||||
'label': item.label,
|
'label': item.label,
|
||||||
'net_filtering': item.net_filtering,
|
'net_filtering': item.net_filtering,
|
||||||
'networks': [{'id': n.uuid} for n in item.networks.all()],
|
'networks': list(item.networks.all().values_list('uuid', flat=True)),
|
||||||
'allowed_oss': [{'id': x} for x in item.allowed_oss.split(',')]
|
'allowed_oss': [x for x in item.allowed_oss.split(',')] if item.allowed_oss != '' else [],
|
||||||
if item.allowed_oss != ''
|
|
||||||
else [],
|
|
||||||
'pools': pools,
|
'pools': pools,
|
||||||
'pools_count': len(pools),
|
'pools_count': len(pools),
|
||||||
'deployed_count': item.deployedServices.count(),
|
'deployed_count': item.deployedServices.count(),
|
||||||
@ -180,9 +175,7 @@ class Transports(ModelHandler):
|
|||||||
fields['label'] = fields['label'].strip().replace(' ', '-')
|
fields['label'] = fields['label'].strip().replace(' ', '-')
|
||||||
# And ensure small_name chars are valid [ a-zA-Z0-9:-]+
|
# And ensure small_name chars are valid [ a-zA-Z0-9:-]+
|
||||||
if fields['label'] and not re.match(r'^[a-zA-Z0-9:-]+$', fields['label']):
|
if fields['label'] and not re.match(r'^[a-zA-Z0-9:-]+$', fields['label']):
|
||||||
raise self.invalidRequestException(
|
raise self.invalidRequestException(gettext('Label must contain only letters, numbers, ":" and "-"'))
|
||||||
gettext('Label must contain only letters, numbers, ":" and "-"')
|
|
||||||
)
|
|
||||||
|
|
||||||
def afterSave(self, item: 'Model') -> None:
|
def afterSave(self, item: 'Model') -> None:
|
||||||
item = ensure.is_instance(item, Transport)
|
item = ensure.is_instance(item, Transport)
|
||||||
|
@ -457,14 +457,15 @@ class ServerManager(metaclass=singleton.Singleton):
|
|||||||
Returns:
|
Returns:
|
||||||
List of servers sorted by usage
|
List of servers sorted by usage
|
||||||
"""
|
"""
|
||||||
now = model_utils.getSqlDatetime()
|
with transaction.atomic():
|
||||||
fltrs = serverGroup.servers.filter(maintenance_mode=False)
|
now = model_utils.getSqlDatetime()
|
||||||
fltrs = fltrs.filter(Q(locked_until=None) | Q(locked_until__lte=now)) # Only unlocked servers
|
fltrs = serverGroup.servers.filter(maintenance_mode=False)
|
||||||
if excludeServersUUids:
|
fltrs = fltrs.filter(Q(locked_until=None) | Q(locked_until__lte=now)) # Only unlocked servers
|
||||||
fltrs = fltrs.exclude(uuid__in=excludeServersUUids)
|
if excludeServersUUids:
|
||||||
|
fltrs = fltrs.exclude(uuid__in=excludeServersUUids)
|
||||||
# Get the stats for all servers, but in parallel
|
|
||||||
serverStats = self.getServerStats(fltrs)
|
# Get the stats for all servers, but in parallel
|
||||||
|
serverStats = self.getServerStats(fltrs)
|
||||||
# Sort by weight, lower first (lower is better)
|
# Sort by weight, lower first (lower is better)
|
||||||
return [s[1] for s in sorted(serverStats, key=lambda x: x[0].weight() if x[0] else 999999999)]
|
return [s[1] for s in sorted(serverStats, key=lambda x: x[0].weight() if x[0] else 999999999)]
|
||||||
|
|
||||||
|
@ -61,13 +61,13 @@ def _requestActor(
|
|||||||
url = userService.getCommsUrl()
|
url = userService.getCommsUrl()
|
||||||
if not url:
|
if not url:
|
||||||
# logger.warning('No notification is made because agent does not supports notifications: %s', userService.friendly_name)
|
# logger.warning('No notification is made because agent does not supports notifications: %s', userService.friendly_name)
|
||||||
raise exceptions.NoActorComms(f'No notification urls for {userService.friendly_name}')
|
raise exceptions.actor.NoActorComms(f'No notification urls for {userService.friendly_name}')
|
||||||
|
|
||||||
minVersion = minVersion or '3.5.0'
|
minVersion = minVersion or '3.5.0'
|
||||||
version = userService.properties.get('actor_version', '0.0.0')
|
version = userService.properties.get('actor_version', '0.0.0')
|
||||||
if '-' in version or version < minVersion:
|
if '-' in version or version < minVersion:
|
||||||
logger.warning('Pool %s has old actors (%s)', userService.deployed_service.name, version)
|
logger.warning('Pool %s has old actors (%s)', userService.deployed_service.name, version)
|
||||||
raise exceptions.OldActorVersion(
|
raise exceptions.actor.OldActorVersion(
|
||||||
f'Old actor version {version} for {userService.friendly_name}'.format(
|
f'Old actor version {version} for {userService.friendly_name}'.format(
|
||||||
version, userService.friendly_name
|
version, userService.friendly_name
|
||||||
)
|
)
|
||||||
@ -155,7 +155,7 @@ def checkUuid(userService: 'UserService') -> bool:
|
|||||||
uuid,
|
uuid,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
except exceptions.NoActorComms:
|
except exceptions.actor.NoActorComms:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return True # Actor does not supports checking
|
return True # Actor does not supports checking
|
||||||
@ -172,7 +172,7 @@ def requestScreenshot(userService: 'UserService') -> bytes:
|
|||||||
png = _requestActor(
|
png = _requestActor(
|
||||||
userService, 'screenshot', minVersion='3.0.0'
|
userService, 'screenshot', minVersion='3.0.0'
|
||||||
) # First valid version with screenshot is 3.0
|
) # First valid version with screenshot is 3.0
|
||||||
except exceptions.NoActorComms:
|
except exceptions.actor.NoActorComms:
|
||||||
png = None
|
png = None
|
||||||
|
|
||||||
return base64.b64decode(png or emptyPng)
|
return base64.b64decode(png or emptyPng)
|
||||||
@ -187,7 +187,7 @@ def sendScript(userService: 'UserService', script: str, forUser: bool = False) -
|
|||||||
if forUser:
|
if forUser:
|
||||||
data['user'] = forUser
|
data['user'] = forUser
|
||||||
_requestActor(userService, 'script', data=data)
|
_requestActor(userService, 'script', data=data)
|
||||||
except exceptions.NoActorComms:
|
except exceptions.actor.NoActorComms:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -197,7 +197,7 @@ def requestLogoff(userService: 'UserService') -> None:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
_requestActor(userService, 'logout', data={})
|
_requestActor(userService, 'logout', data={})
|
||||||
except exceptions.NoActorComms:
|
except exceptions.actor.NoActorComms:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -207,5 +207,5 @@ def sendMessage(userService: 'UserService', message: str) -> None:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
_requestActor(userService, 'message', data={'message': message})
|
_requestActor(userService, 'message', data={'message': message})
|
||||||
except exceptions.NoActorComms:
|
except exceptions.actor.NoActorComms:
|
||||||
pass
|
pass
|
||||||
|
@ -69,7 +69,7 @@ class MessageProcessorThread(BaseThread):
|
|||||||
or time.time() - self._cached_stamp > CACHE_TIMEOUT
|
or time.time() - self._cached_stamp > CACHE_TIMEOUT
|
||||||
):
|
):
|
||||||
self._cached_providers = [
|
self._cached_providers = [
|
||||||
(p.level, p.getInstance()) for p in Notifier.objects.all()
|
(p.level, p.getInstance()) for p in Notifier.objects.filter(enabled=True)
|
||||||
]
|
]
|
||||||
self._cached_stamp = time.time()
|
self._cached_stamp = time.time()
|
||||||
return self._cached_providers
|
return self._cached_providers
|
||||||
|
@ -73,7 +73,7 @@ class Cache:
|
|||||||
] = _basic_deserialize
|
] = _basic_deserialize
|
||||||
|
|
||||||
def __init__(self, owner: typing.Union[str, bytes]):
|
def __init__(self, owner: typing.Union[str, bytes]):
|
||||||
self._owner = owner.decode('utf-8') if isinstance(owner, bytes) else owner
|
self._owner = typing.cast(str, owner.decode('utf-8') if isinstance(owner, bytes) else owner)
|
||||||
self._bowner = self._owner.encode('utf8')
|
self._bowner = self._owner.encode('utf8')
|
||||||
|
|
||||||
def __getKey(self, key: typing.Union[str, bytes]) -> str:
|
def __getKey(self, key: typing.Union[str, bytes]) -> str:
|
||||||
|
@ -45,7 +45,7 @@ _saveLater: typing.List[typing.Tuple['Config.Value', typing.Any]] = []
|
|||||||
_getLater: typing.List['Config.Value'] = []
|
_getLater: typing.List['Config.Value'] = []
|
||||||
|
|
||||||
# For custom params (for choices mainly)
|
# For custom params (for choices mainly)
|
||||||
_configParams = {}
|
_configParams: typing.Dict[str, typing.Any] = {}
|
||||||
|
|
||||||
# Pair of section/value removed from current UDS version
|
# Pair of section/value removed from current UDS version
|
||||||
# Note: As of version 4.0, all previous REMOVED values has been moved to migration script 0043
|
# Note: As of version 4.0, all previous REMOVED values has been moved to migration script 0043
|
||||||
|
@ -56,6 +56,7 @@ class Transport(ManagedObjectModel, TaggingMixin):
|
|||||||
|
|
||||||
Sample of transports are RDP, Spice, Web file uploader, etc...
|
Sample of transports are RDP, Spice, Web file uploader, etc...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Constants for net_filter
|
# Constants for net_filter
|
||||||
NO_FILTERING = 'n'
|
NO_FILTERING = 'n'
|
||||||
ALLOW = 'a'
|
ALLOW = 'a'
|
||||||
@ -89,9 +90,7 @@ class Transport(ManagedObjectModel, TaggingMixin):
|
|||||||
ordering = ('name',)
|
ordering = ('name',)
|
||||||
app_label = 'uds'
|
app_label = 'uds'
|
||||||
|
|
||||||
def getInstance(
|
def getInstance(self, values: typing.Optional[typing.Dict[str, str]] = None) -> 'transports.Transport':
|
||||||
self, values: typing.Optional[typing.Dict[str, str]] = None
|
|
||||||
) -> 'transports.Transport':
|
|
||||||
return typing.cast('transports.Transport', super().getInstance(values=values))
|
return typing.cast('transports.Transport', super().getInstance(values=values))
|
||||||
|
|
||||||
def getType(self) -> typing.Type['transports.Transport']:
|
def getType(self) -> typing.Type['transports.Transport']:
|
||||||
@ -127,11 +126,16 @@ class Transport(ManagedObjectModel, TaggingMixin):
|
|||||||
|
|
||||||
:note: Ip addresses has been only tested with IPv4 addresses
|
:note: Ip addresses has been only tested with IPv4 addresses
|
||||||
"""
|
"""
|
||||||
|
# Avoid circular import
|
||||||
|
from uds.models import Network # pylint: disable=import-outside-toplevel
|
||||||
|
|
||||||
if self.net_filtering == Transport.NO_FILTERING:
|
if self.net_filtering == Transport.NO_FILTERING:
|
||||||
return True
|
return True
|
||||||
ip, version = net.ipToLong(ipStr)
|
ip, version = net.ipToLong(ipStr)
|
||||||
# Allow
|
# Allow
|
||||||
exists = self.networks.filter(start__lte=Network.hexlify(ip), end__gte=Network.hexlify(ip), version=version).exists()
|
exists = self.networks.filter(
|
||||||
|
start__lte=Network.hexlify(ip), end__gte=Network.hexlify(ip), version=version
|
||||||
|
).exists()
|
||||||
if self.net_filtering == Transport.ALLOW:
|
if self.net_filtering == Transport.ALLOW:
|
||||||
return exists
|
return exists
|
||||||
# Deny, must not be in any network
|
# Deny, must not be in any network
|
||||||
|
File diff suppressed because one or more lines are too long
@ -102,6 +102,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</uds-root>
|
</uds-root>
|
||||||
<script src="/uds/res/admin/runtime.js?stamp=1699891841" type="module"></script><script src="/uds/res/admin/polyfills.js?stamp=1699891841" type="module"></script><script src="/uds/res/admin/main.js?stamp=1699891841" type="module"></script></body>
|
<script src="/uds/res/admin/runtime.js?stamp=1700178945" type="module"></script><script src="/uds/res/admin/polyfills.js?stamp=1700178945" type="module"></script><script src="/uds/res/admin/main.js?stamp=1700178945" type="module"></script></body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -1 +1 @@
|
|||||||
hPF/H21BzZ0VU9HjimiOa1go9TJVsqmKsgGqB4V4X3cjB6G3NkSa1O45ub3gf+gOriElibPRbXzet4zma4qnBACLzAZQjmmxDRK9gC7eN6p0z0TKg18U6/hKf5gs6ICjQZyONg2gvBBrYUz7JhYoefq9SZRAna7373N0minKoJoBPLB+ylkemBNhKcsN5bRQlEGU03mLREGk/h642qN6ebQhIzY+JMo3c9CwtRS6SCiLhiaYkPFxdEX9e7AGSpTgGSFh4+Mc3QEge4sOquWszX4ocJJc23SHl5GdZSfhn3Bst5lLKILVUT5w7zaZOqa/xx/HjpCmxROJKm/bkR+HK8VMRh4N5c5yvavXid0IwgN8Uc+SOIUh4+wxbOyJI3ySS/77gUI/1TDw+r/gRkSUPwcRSFKuyQMN2yR6yr2BgOXlGmlNwZ/MzsDIIwgGeMlkKZZzLI64aj9VG2QsWHrlw2HWuFfBGGIX4T49p0tvdRm5o1jg/cdsLsHC8of5Vn4wgwyZwlScndDLcBJq76v4wnWUMg4IQpeOoI/PEbgeUfZDuntSt/ZT5icB78nN07hUo56d5Z5q1ktuWEHJSWCVSg59KGOcvaaPub50+anm6Uzac5rynZ0/gqA8Y0IV3dgRalBMZCiCy89lHW1YyO52A4DBka6hG4gKt5VUs+eT+vI=
|
mzEKyhRHnv5zWHZvuCfZrW6pq0V9NNTVfLrLmuP649dw5+Llpl80QL6F84vzaCuiTGebBrObT9cTO+JKi3gIlmgSfLVHSkOFaLOfX/IVzsQclcP7zn5B6dYwUPgBhrMN1CyvHP0hqvjXQkVgLxI3OFR/wSZ7VllyFBAnD4/EawUa2CIXRTnnCUBr4K5YFiV3esQu8sOCVYYMONwk4FAJUdt1UDfjGKQo15x9Y8S96tuBpVwQOGvF+ioEndQTUSC1GBlmPT6IgffJhI6Tm0xOji8QIdvUcJ/8ipK7qaWFplcJ7Sc/uKSgvcM7P2r6D45TmqbmGYhQgdMnQd7HmRaDi25gEnFckTCAzdEHiy8iibMATs0zqGS07h85vtlkllsi2xFF12ZcRR5AAIjSKmo5CDApjdQegVcHJgwYV8XjZRDyc2kBWvywbN7zob/rDuF5ncvFLwDDLFV6EZ1a15dEP/L6rR2NI1lGDdAqWwQmm0c28dfM3jv3GvyGOwTqJjmXgUrlSIBpM6EkdRMz8yIzdhJAhUxrwxVMi6qi+pWkWbtM9BXqgBgLqfaF5c8cGPup2uUvwvarexgrON28zsqwVvIHo2dqrrRWzjIrZzZpXAvteVLw9SFRlTVi3ilqHF7Zs9pQ7lp/D83SuI7+CWDu4co+3PX2Y9HYT2JfrlYnEjY=
|
Loading…
x
Reference in New Issue
Block a user