Browse Source

Fix API login and add PEP8 format

master
Joshua Rubingh 3 months ago
parent
commit
d7721db23b
  1. 3
      .gitignore
  2. 3
      .pep8
  3. 248
      agent.py
  4. 2
      requirements.txt

3
.gitignore

@ -140,4 +140,5 @@ dmypy.json
cython_debug/
# Development IDE
.vscode/launch.json
.vscode/launch.json
.running

3
.pep8

@ -0,0 +1,3 @@
[pycodestyle]
max_line_length = 120
ignore = E501

248
agent.py

@ -1,3 +1,11 @@
import os
import re
import requests
from datetime import datetime, timedelta
from slugify import slugify
from dotenv import dotenv_values
from ldap3.utils.conv import escape_filter_chars
from ldap3 import LEVEL, MODIFY_REPLACE, Server, Connection, ALL, MODIFY_ADD, MODIFY_DELETE
import logging
import logging.handlers
from logging.config import dictConfig, fileConfig
@ -9,10 +17,12 @@ import glob
import codecs
import zipfile
class TimedCompressedRotatingFileHandler(logging.handlers.TimedRotatingFileHandler):
"""
Extended version of TimedRotatingFileHandler that compress logs on rollover.
"""
def doRollover(self):
"""
do a rollover; in this case, a date/time stamp is appended to the filename
@ -36,7 +46,7 @@ class TimedCompressedRotatingFileHandler(logging.handlers.TimedRotatingFileHandl
if len(s) > self.backupCount:
s.sort()
os.remove(s[0])
#print "%s -> %s" % (self.baseFilename, dfn)
# print "%s -> %s" % (self.baseFilename, dfn)
if self.encoding:
self.stream = codecs.open(self.baseFilename, 'w', self.encoding)
else:
@ -49,35 +59,35 @@ class TimedCompressedRotatingFileHandler(logging.handlers.TimedRotatingFileHandl
file.close()
os.remove(dfn)
if Path('logging.conf').exists():
# Custom logging config
fileConfig('logging.conf')
else:
# Default logging config
logging_config = dict(
version = 1,
formatters = {
'f': {'format':
'%(asctime)s %(name)-12s %(levelname)-8s %(message)s'}
},
handlers = {
version=1,
formatters={
'f': {'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'}
},
handlers={
'console': {'class': 'logging.StreamHandler',
'formatter': 'f',
'level': logging.WARNING},
'formatter': 'f',
'level': logging.WARNING},
'file' : {
'file': {
'class': 'agent.TimedCompressedRotatingFileHandler',
'formatter': 'f',
'filename': 'log/agent.log',
'when': 'midnight',
'interval' : 1,
'backupCount' : 3,
'interval': 1,
'backupCount': 3,
'level': logging.INFO}
},
root = {
'handlers': ['console','file'],
},
root={
'handlers': ['console', 'file'],
'level': logging.DEBUG,
},
},
)
dictConfig(logging_config)
@ -85,18 +95,10 @@ else:
logger = logging.getLogger(__name__)
# https://ldap3.readthedocs.io/en/latest/
from ldap3 import LEVEL, MODIFY_REPLACE, Server, Connection, ALL, MODIFY_ADD, MODIFY_DELETE
from ldap3.utils.conv import escape_filter_chars
from dotenv import dotenv_values
from slugify import slugify
from pathlib import Path
from datetime import datetime, timedelta
import requests
import re
import os
WORKINGDIR, _ = os.path.split(os.path.abspath(__file__))
class LDAPError(Exception):
"""General LDAP error exception
@ -120,6 +122,7 @@ class DuplicateResearchGroup(Exception):
def __init__(self, message):
super().__init__(message)
class ResearchGroupDoesNotExists(Exception):
"""Researchgroup does not exists exception
@ -145,6 +148,7 @@ class DuplicateResearcher(Exception):
def __init__(self, message):
super().__init__(message)
class ResearcherDoesNotExists(Exception):
"""Researcher does not exists exception
@ -172,10 +176,10 @@ class VRWLDAP():
VRWLDAP: LDAP client
"""
__RESOURCE_GROUPS_BASE = 'ou=researchgroups,ou=dfh,o=co'
__RESOURCE_GROUPS_BASE = 'ou=researchgroups,ou=dfh,o=co'
__RESOURCE_MEMBERS_BASE = 'ou=users,ou=dfh,o=co'
def __init__(self, host, port, login, password, ssl = False, production = False):
def __init__(self, host, port, login, password, ssl=False, production=False):
"""Create a new VRW LDAP client
Args:
@ -199,7 +203,7 @@ class VRWLDAP():
"""
_field = 'hsrGroupGID'
group_id = 0
self.connection.search(self.__RESOURCE_GROUPS_BASE,f'({_field}=*)', attributes=[_field])
self.connection.search(self.__RESOURCE_GROUPS_BASE, f'({_field}=*)', attributes=[_field])
for data in self.connection.entries:
if data.entry_attributes_as_dict[_field][0] > group_id:
group_id = data.entry_attributes_as_dict[_field][0]
@ -216,16 +220,16 @@ class VRWLDAP():
"""
_field = 'hsrUserUID'
researcher_id = 0
self.connection.search(self.__RESOURCE_MEMBERS_BASE,f'({_field}=*)', attributes=[_field])
self.connection.search(self.__RESOURCE_MEMBERS_BASE, f'({_field}=*)', attributes=[_field])
for data in self.connection.entries:
if data.entry_attributes_as_dict[_field][0] > researcher_id:
researcher_id = data.entry_attributes_as_dict[_field][0]
return researcher_id + 1
def __safe_dn_name(self,value, slug = True):
def __safe_dn_name(self, value, slug=True):
# The following characters give problems: +
value = value.replace("+","-")
value = value.replace("+", "-")
if slug:
value = slugify(value)
@ -239,7 +243,7 @@ class VRWLDAP():
"""
return self.connection.bind()
def search_research_group(self, name, id = None):
def search_research_group(self, name, id=None):
"""Find the LDAP DN path for a researchgroup.
It can search both on name and existing ID. When the 'id' parameter is set (not None), it will do first a LDAP DN key lookup. If that fails, if will fallback on the 'name' parameter.
@ -252,12 +256,12 @@ class VRWLDAP():
Returns:
string, bool: Return the LDAP DN name when found or False when not found
"""
search_base = self.__RESOURCE_GROUPS_BASE
search_base = self.__RESOURCE_GROUPS_BASE
search_filter = f'(ou={self.__safe_dn_name(name)})'
if id is not None:
split_index = id.index(',')
search_base = id[split_index+1:]
search_base = id[split_index + 1:]
search_filter = f'({id[:split_index]})'
logger.debug(f'Searching for research group "({search_filter[1:-1]})" in DN "{search_base}"')
@ -273,7 +277,7 @@ class VRWLDAP():
logger.debug(f'Research group {name} does not exist.')
return False
def create_research_group(self, name, subgroups = False):
def create_research_group(self, name, subgroups=False):
"""Create a new research group in the LDAP server based on the name.
The name will be changed to strip if from special characters and spaces. The name will bug slugified.
@ -302,9 +306,7 @@ class VRWLDAP():
f'{group_name}:WS:Shared']
for sub_group in sub_groups:
self.connection.add(f'cn={sub_group},{parent_dn}',
['groupOfNames', 'hsrGroup', 'Top'],
{'description' : f'{sub_group}'})
self.connection.add(f'cn={sub_group},{parent_dn}', ['groupOfNames', 'hsrGroup', 'Top'], {'description': f'{sub_group}'})
start_id += 1
@ -318,12 +320,13 @@ class VRWLDAP():
logger.debug(f'Creating a new research group with the name "{name}"')
dn_name = self.__safe_dn_name(name)
dn = f'ou={dn_name},{self.__RESOURCE_GROUPS_BASE}'
add_ok = self.connection.add(dn,
['hsrGroup', 'ndsContainerLoginProperties', 'ndsLoginProperties', 'organizationalUnit', 'Top'],
{'description' : f'{name} Researchgroup',
'hsrGroupOrganisation' : 'RUG',
'hsrGroupProjectCode' : 'Support'})
dn = f'ou={dn_name},{self.__RESOURCE_GROUPS_BASE}'
add_ok = self.connection.add(dn,
['hsrGroup', 'ndsContainerLoginProperties',
'ndsLoginProperties', 'organizationalUnit', 'Top'],
{'description': f'{name} Researchgroup',
'hsrGroupOrganisation': 'RUG',
'hsrGroupProjectCode': 'Support'})
if add_ok:
if subgroups:
@ -374,8 +377,7 @@ class VRWLDAP():
research_group = self.search_research_group(name)
# Update the description of the LDAP object
update_ok = update_ok and self.connection.modify(research_group,
{'description' : [(MODIFY_REPLACE, [f'{name} Researchgroup'])] })
update_ok = update_ok and self.connection.modify(research_group, {'description': [(MODIFY_REPLACE, [f'{name} Researchgroup'])]})
# Rename the subgroups also.
self.connection.search(research_group, '(cn=*)', LEVEL)
@ -389,7 +391,8 @@ class VRWLDAP():
else:
raise LDAPError(f'Could not update the researchgroup {name}! Got some errors...... LDAP Error: {self.connection.last_error}')
raise ResearchGroupDoesNotExists(f'Invalid research group DN: {group_dn}')
raise ResearchGroupDoesNotExists(
f'Invalid research group DN: {group_dn}')
def delete_research_group(self, group_dn):
"""Delete an existing researchgroup from the LDAP server based on DN key
@ -432,7 +435,7 @@ class VRWLDAP():
logger.debug(f'Getting all researchers for group "{group_dn}"')
members = []
if not self.search_research_group('',group_dn):
if not self.search_research_group('', group_dn):
raise ResearchGroupDoesNotExists(f'Invalid research group DN: {group_dn}')
if self.connection.search(f'{group_dn}', '(cn=*:Members)', attributes=['member']):
@ -452,7 +455,7 @@ class VRWLDAP():
string, bool: Return the LDAP DN key when found or False when not found
"""
logger.debug(f'Searching for research member "{email}" in DN "{self.__RESOURCE_MEMBERS_BASE}"')
if self.connection.search(self.__RESOURCE_MEMBERS_BASE,f'(uid={self.__safe_dn_name(email, False)})'):
if self.connection.search(self.__RESOURCE_MEMBERS_BASE, f'(uid={self.__safe_dn_name(email, False)})'):
if len(self.connection.entries) == 1:
logger.debug(f'Found research member "{email}" at "{self.connection.entries[0].entry_dn}"')
return self.connection.entries[0].entry_dn
@ -468,7 +471,7 @@ class VRWLDAP():
lastname (string): Middle and lastname of the researcher
email (string): Email address of the researcher
mobile (string): Mobile number used for MFA.
pnumber (string): Researcher ID number at the univeristy
pnumber (string): Researcher ID number at the university
Raises:
LDAPError: When there is a general LDAP error
@ -485,18 +488,19 @@ class VRWLDAP():
dn = f'uid={self.__safe_dn_name(email, False)},{self.__RESOURCE_MEMBERS_BASE}'
user_id = self.__find_next_researcher_id()
add_ok = self.connection.add(dn,
['hsrUser', 'inetOrgPerson', 'ldapPublicKey', 'ndsLoginProperties', 'organizationalPerson', 'Person', 'Top'],
{'cn' : f'{firstname} {lastname}',
'mail' : email,
'sn' : lastname,
'displayName' : f'{firstname} {lastname}',
'givenName' : firstname,
'loginDisabled' : 'FALSE',
'hsrUserUID' : user_id,
'employeeNumber' : pnumber,
'description' : f'Researcher {firstname} {lastname}',
'mobile' : mobile,
'o' : 'RUG'})
['hsrUser', 'inetOrgPerson', 'ldapPublicKey',
'ndsLoginProperties', 'organizationalPerson', 'Person', 'Top'],
{'cn': f'{firstname} {lastname}',
'mail': email,
'sn': lastname,
'displayName': f'{firstname} {lastname}',
'givenName': firstname,
'loginDisabled': 'FALSE',
'hsrUserUID': user_id,
'employeeNumber': pnumber,
'description': f'Researcher {firstname} {lastname}',
'mobile': mobile,
'o': 'RUG'})
if add_ok:
logger.info(f'Created new researcher "{firstname} {lastname}" with the email address "{email}". DN value: {dn}')
@ -514,7 +518,7 @@ class VRWLDAP():
lastname (string): Middle and lastname of the researcher
email (string): Email address of the researcher
mobile (string): Mobile number used for MFA.
pnumber (string): Researcher ID number at the univeristy
pnumber (string): Researcher ID number at the university
Raises:
LDAPError: When there is a general LDAP error
@ -524,13 +528,13 @@ class VRWLDAP():
"""
researcher = self.search_researcher(email)
if researcher is not False:
update_ok = self.connection.modify(researcher, {'cn' : [(MODIFY_REPLACE, [f'{firstname} {lastname}'])],
'sn' : [(MODIFY_REPLACE, [lastname])],
'displayName' : [(MODIFY_REPLACE, [f'{firstname} {lastname}'])],
'givenName' : [(MODIFY_REPLACE, [firstname])],
'employeeNumber' : [(MODIFY_REPLACE, [pnumber])],
'description' : [(MODIFY_REPLACE, [f'Researcher {firstname} {lastname}'])],
'mobile' : [(MODIFY_REPLACE, [mobile])] })
update_ok = self.connection.modify(researcher, {'cn': [(MODIFY_REPLACE, [f'{firstname} {lastname}'])],
'sn': [(MODIFY_REPLACE, [lastname])],
'displayName': [(MODIFY_REPLACE, [f'{firstname} {lastname}'])],
'givenName': [(MODIFY_REPLACE, [firstname])],
'employeeNumber': [(MODIFY_REPLACE, [pnumber])],
'description': [(MODIFY_REPLACE, [f'Researcher {firstname} {lastname}'])],
'mobile': [(MODIFY_REPLACE, [mobile])]})
if update_ok:
logger.info(f'Updated researcher ({firstname} {lastname}) with the email address "{email}"')
@ -538,7 +542,7 @@ class VRWLDAP():
raise LDAPError(f'Could not update the researcher {firstname} {lastname} - {email}! LDAP Error: {self.connection.last_error}')
def researcher_to_group(self, group_dn, researcher_dn, role = None, workstation = None):
def researcher_to_group(self, group_dn, researcher_dn, role=None, workstation=None):
"""Add a researcher to a researchgroup. And add roles and a workstation for this researcher is needed.
Args:
@ -559,7 +563,7 @@ class VRWLDAP():
# We do not have to make the 'groupMembership' attribute at the researcher. This is automatically done by the LDAP server.
if not self.production:
self.connection.modify(researcher_dn, {'groupMembership': [(MODIFY_ADD, [members_dn])]})
self.connection.modify(researcher_dn, {'groupMembership': [(MODIFY_ADD, [members_dn])]})
# Add the correct researcher rights
if role in ['researcher']:
@ -574,12 +578,11 @@ class VRWLDAP():
if not self.production:
self.connection.modify(researcher_dn, {'groupMembership': [(MODIFY_ADD, [members_dn])]})
# Add correct machine
# The naming is very confusing. But from the 'old days' it was Shared for Basic, and Premium is Small :(
if workstation in ['premium']:
workstation_dn = f'cn={matches.group("group_name")}:WS:Dedicated:Small,{group_dn}'
if workstation in ['standaard','basic']:
if workstation in ['standaard', 'basic']:
workstation_dn = f'cn={matches.group("group_name")}:WS:Dedicated:Shared,{group_dn}'
self.connection.modify(workstation_dn, {'member': [(MODIFY_ADD, [researcher_dn])]})
@ -600,7 +603,7 @@ class VRWLDAP():
researcher_dn (string): The LDAP DN key of the researcher
"""
logger.debug(f'Removing researcher {researcher_dn} from group {group_dn}')
if self.connection.search(researcher_dn,'(objectclass=person)', attributes=['groupMembership']) and len(self.connection.entries) == 1:
if self.connection.search(researcher_dn, '(objectclass=person)', attributes=['groupMembership']) and len(self.connection.entries) == 1:
logger.debug(f'Found researcher {researcher_dn} with {len(self.connection.entries[0]["groupMembership"])} memberships.')
for group in self.connection.entries[0]['groupMembership']:
if group.endswith(group_dn):
@ -609,14 +612,15 @@ class VRWLDAP():
# We do not have to delete the 'groupMembership' attribute at the researcher. This is automatically done by the LDAP server.
if not self.production:
self.connection.modify(researcher_dn, {'groupMembership': [(MODIFY_DELETE, [group])]}) # It looks like this is taken care by the LDAP server
# It looks like this is taken care by the LDAP server
self.connection.modify(researcher_dn, {'groupMembership': [(MODIFY_DELETE, [group])]})
class VRE_API_CLient():
"""This is the VRE API client. With this client you can easyly get the latest VRW actions from the VRE API.
"""This is the VRE API client. With this client you can easily get the latest VRW actions from the VRE API.
Args:
base_url (string): The full url to the base API. Including protocol and optionally port nunber
base_url (string): The full url to the base API. Including protocol and optionally port number
api_prefix (string): The current API version
username (string): The login name for the VRE API server
password (string): The password for the VRE API server
@ -626,10 +630,10 @@ class VRE_API_CLient():
"""
def __init__(self, base_url, api_prefix, username, password):
"""This is the VRE API client. With this client you can easyly get the latest VRW actions from the VRE API.
"""This is the VRE API client. With this client you can easily get the latest VRW actions from the VRE API.
Args:
base_url (string): The full url to the base API. Including protocol and optionally port nunber
base_url (string): The full url to the base API. Including protocol and optionally port number
api_prefix (string): The current API version
username (string): The login name for the VRE API server
password (string): The password for the VRE API server
@ -637,10 +641,10 @@ class VRE_API_CLient():
Returns:
VRE_API_CLient: VRE API client
"""
self.base_url = base_url.strip('/')
self.username = username
self.password = password
self.api_prefix = api_prefix
self.base_url = base_url.strip('/')
self.username = username
self.password = password
self.api_prefix = api_prefix
self.__authorization_header = None
@ -653,42 +657,42 @@ class VRE_API_CLient():
Returns:
string: The full url including protocol and complete path.
"""
return f'{self.base_url}/{self.api_prefix.strip("/")}/{part.lstrip("/")}'.replace(f'/{self.api_prefix.strip("/")}/auth/','/auth/')
return f'{self.base_url}/{self.api_prefix.strip("/")}/{part.lstrip("/")}'.replace(f'/{self.api_prefix.strip("/")}/auth/', '/auth/')
def __get_JWT_token(self):
"""Helperfunction: This will get a new JWT token from the VRE API server based on the user login.
"""
self.__authorization_header = None
login = requests.post(self.__get_full_url('/auth/jwt/create/'), json={
'username' : self.username,
'password' : self.password
'username': self.username,
'password': self.password
})
self.__authorization_header = { 'Authorization' : f'JWT {login.json().get("access")}' }
self.__authorization_header = {'Authorization': f'JWT {login.json().get("access")}'}
def __process_workspace(self, workspace):
workspace_id = workspace['id']
workspace = workspace['workspace']
return {
'workspace_id' : workspace_id,
'workspace_type' : workspace['type'],
'workspace_dn' : workspace['cloud_id'],
'study_id' : workspace['study']['id'],
'study_name' : workspace['study']['name'],
'study_dn' : None if workspace['cloud_id'] is None else workspace['cloud_id'][workspace['cloud_id'].index(',') + 1:], # https://stackoverflow.com/a/12572399
'researcher_first_name' : workspace['researcher']['first_name'],
'researcher_last_name' : workspace['researcher']['last_name'],
'researcher_email' : workspace['researcher']['email_address'],
'researcher_mobile_phone' : workspace['researcher']['mobilephone'],
'researcher_Pnumber' : 'N/A', # Is currently missing in the VRE-VRW API
'researcher_role' : workspace['role'].lower(),
'workspace_id': workspace_id,
'workspace_type': workspace['type'],
'workspace_dn': workspace['cloud_id'],
'study_id': workspace['study']['id'],
'study_name': workspace['study']['name'],
# https://stackoverflow.com/a/12572399
'study_dn': None if workspace['cloud_id'] is None else workspace['cloud_id'][workspace['cloud_id'].index(',') + 1:],
'researcher_first_name': workspace['researcher']['first_name'],
'researcher_last_name': workspace['researcher']['last_name'],
'researcher_email': workspace['researcher']['email_address'],
'researcher_mobile_phone': workspace['researcher']['mobilephone'],
'researcher_Pnumber': 'N/A', # Is currently missing in the VRE-VRW API
'researcher_role': workspace['role'].lower(),
}
def _get_data(self, url, retry = True):
def _get_data(self, url, retry=True):
online_data = requests.get(self.__get_full_url(url), headers=self.__authorization_header)
if online_data.status_code == 200:
@ -700,16 +704,16 @@ class VRE_API_CLient():
return None
def _post_data(self, url, data, retry = True):
def _post_data(self, url, data, retry=True):
online_data = requests.post(self.__get_full_url(url), headers=self.__authorization_header, json=data)
if online_data.status_code == 201:
if online_data.status_code in [200, 201]:
return online_data.json()
def _put_data(self, url, data, retry = True):
def _put_data(self, url, data, retry=True):
online_data = requests.put(self.__get_full_url(url), headers=self.__authorization_header, json=data)
if online_data.status_code == 201:
if online_data.status_code in [200, 201]:
return online_data.json()
def check_connection(self):
@ -718,8 +722,13 @@ class VRE_API_CLient():
Returns:
bool: True when the connection is successfull, else False
"""
data = self._get_data('/auth/users/me/')
return False if data is None else data.get('username') == self.username
if self.__authorization_header is None:
self.__get_JWT_token()
data = self._post_data('/auth/jwt/verify/', data={'token': self.__authorization_header.get('Authorization')[4:]})
# We should get back an empty dict.... Else there was an error...
return data == {}
def get_new_workspaces(self):
"""Get a list of new workstations to make
@ -769,7 +778,7 @@ class VRE_API_CLient():
id (int): The VRE API workstation ID
cloud_id (string): The cloud id that is created with this workstation
"""
data = self._put_data(f'vrw/{id}/status/', data={'status' : 'DONE', 'cloud_id' : cloud_id})
data = self._put_data(f'vrw/{id}/status/', data={'status': 'DONE', 'cloud_id': cloud_id})
def workspace_deleted(self, id, cloud_id):
"""Update back to the VRE API that a workstation has been deleted.
@ -778,12 +787,13 @@ class VRE_API_CLient():
id (int): The VRE API workstation ID
cloud_id (string): The cloud id that is created with this workstation
"""
data = self._put_data(f'vrw/{id}/status/', data={'status' : 'TERMINATED', 'cloud_id' : cloud_id})
data = self._put_data(f'vrw/{id}/status/', data={'status': 'TERMINATED', 'cloud_id': cloud_id})
if __name__ == "__main__":
# Load config settings in a dict.
config = dotenv_values(f'{WORKINGDIR}/.env')
if '' != config.get('SENTRY_DSN',''):
if '' != config.get('SENTRY_DSN', ''):
# Load Sentry logging
# All of this is already happening by default!
@ -814,7 +824,8 @@ if __name__ == "__main__":
# Load API call.
vre_api = VRE_API_CLient(config['VRE_API_HOST'], config['VRE_API_PREFIX'], config['VRE_API_USER'], config['VRE_API_PASS'])
if not vre_api.check_connection():
logger.error(f'Could not login to the VRE API server. Check connection credentials.')
logger.error(
f'Could not login to the VRE API server. Check connection credentials.')
quit()
# Load LDAP client for making LDAP changes
@ -828,14 +839,14 @@ if __name__ == "__main__":
logger.info(f'We have {len(workspaces)} new workstations to create.')
for workspace in workspaces:
study_dn = ldap_client.search_research_group(workspace['study_name'], workspace['study_dn'])
study_dn = ldap_client.search_research_group(
workspace['study_name'], workspace['study_dn'])
if not study_dn:
try:
study_dn = ldap_client.create_research_group(workspace['study_name'], config['LDAP_MANUAL_GROUPS'])
except DuplicateResearchGroup as ex:
print(ex)
user_dn = ldap_client.search_researcher(workspace['researcher_email'])
if not user_dn:
try:
@ -850,7 +861,6 @@ if __name__ == "__main__":
else:
logger.info(f'Using existing Member DN: {user_dn}')
# Remove old member data if exists
ldap_client.remove_researcher_from_group(study_dn, user_dn)
@ -862,7 +872,6 @@ if __name__ == "__main__":
# Done processing new Workspaces..
# Update workspaces
workspaces = vre_api.get_changing_workspaces()
logger.info(f'We have {len(workspaces)} workstations to update.')
@ -891,7 +900,6 @@ if __name__ == "__main__":
# Done processing updated Workspaces..
# Delete existing workspaces
workspaces = vre_api.get_deleted_workspaces()
logger.info(f'We have {len(workspaces)} new workstations to delete.')
@ -917,4 +925,4 @@ if __name__ == "__main__":
vre_api.workspace_deleted(workspace['workspace_id'], workspace['workspace_dn'])
# Release the lockfile
lockfile.unlink()
lockfile.unlink()

2
requirements.txt

@ -1,5 +1,5 @@
requests==2.26.0
ldap3==2.9.1
python-dotenv==0.19.0
python-dotenv==0.19.1
python-slugify==5.0.2
sentry-sdk==1.4.3
Loading…
Cancel
Save