Compare commits

..

12 Commits

13 changed files with 618 additions and 60 deletions

View File

@@ -3,7 +3,7 @@
## Explication de l'usage des scripts et de la configuration : ## Explication de l'usage des scripts et de la configuration :
### Fichier de configuration global settings.json ### Fichier de configuration global settings.json
Les scripts prennent les informations de configuration du fichier de configuration présent dans le répertoire conf, si celui-ci n'existe pas au lancement d'un script, il est automatiquement créé avec des valeurs d'exemples. Un chemin personnalisé vers un fichier de configuration peut être renseigné avec le paramètre -c. Les scripts prennent les informations de configuration du fichier de configuration présent dans le répertoire conf, si celui-ci n'existe pas au lancement d'un script, il est automatiquement créé avec des valeurs d'exemples.
Voici un exemple du fichier de configuration : Voici un exemple du fichier de configuration :
@@ -85,6 +85,10 @@ Il récupère le nom de l'utilisateur de staging dans le fichier de configuratio
- **-s / --silent** : exécute le script sans faire de retour dans la console, les informations seront toujours présentes dans les fichiers de log - **-s / --silent** : exécute le script sans faire de retour dans la console, les informations seront toujours présentes dans les fichiers de log
- **-v / --verbose** : affiche des informations lors de son exécution, utile pour résoudre des problèmes liés à des droits d'accès API ou des problèmes d'ouvertures réseaux - **-v / --verbose** : affiche des informations lors de son exécution, utile pour résoudre des problèmes liés à des droits d'accès API ou des problèmes d'ouvertures réseaux
## Installation avec docker
## Dépendances ## Dépendances
Installation des paquets linux : Installation des paquets linux :
@@ -98,9 +102,28 @@ apt-get install -y python3 python3-pip git
yum install -y python3 python3-pip git yum install -y python3 python3-pip git
``` ```
**Dépendances python** ## Modules python nécessaires
Voici la liste des modules python nécessaires au fonctionnement des scripts :
- cryptography
- requests
- toml
Un fichier requirements.txt contenant les modules dont dépendent les scripts est présent dans le répertoire scripts.
Les modules peuvent être installés de plusieurs façons :
**Avec le gestionnaire de paquet python Pip**
``` ```
python3 -m pip install cryptography requests toml python -m pip install -r requirements.txt
```
**Avec le gestionnaire de paquet apt (debian/ubuntu)**
```
apt-get install -y python3-cryptography python3-requests python3-toml
```
**Avec le gestionnaire de paquet yum (redhat/centos)**
```
yum install -y python3-cryptography python3-requests python3-toml
``` ```
## Installation des scripts ## Installation des scripts
@@ -172,7 +195,7 @@ Description=Script qui assigne les appareils en staging aux utilisateurs en se b
[Timer] [Timer]
OnUnitInactiveSec=1m OnUnitInactiveSec=1m
AccuracySec=1us AccuracySec=1us
Unit=airwatchSync.service Unit=airwatchStaging.service
[Install] [Install]
WantedBy=timers.target WantedBy=timers.target

28
compose.yml Normal file
View File

@@ -0,0 +1,28 @@
services:
sync:
image: emitlinks/airwatchConnector
restart: unless-stopped
volumes:
- ./conf:/airwatchConnector/conf
- ./certs:/airwatchConnector/certs
- ./logs:/airwatchConnector/logs
healthcheck:
test: if [ -f $(ls /airwatchConnector/*_SyncGLPI.lock) ]; then exit 0; else exit 1; fi
interval: 5m
start_period: 3h
environment:
TASK: "syncGLPI"
stagingAssignment:
image: emitlinks/airwatchConnector
restart: unless-stopped
volumes:
- ./conf:/airwatchConnector/conf
- ./certs:/airwatchConnector/certs
- ./logs:/airwatchConnector/logs
healthcheck:
test: if [ -f $(ls /airwatchConnector/*_StagingUserAssignation.lock) ]; then exit 0; else exit 1; fi
interval: 15s
start_period: 5m
environment:
TASK: "stagingAssignment"

10
dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM python
RUN <<-EOF
apt-get update
apt-get install -y python3-cryptography python3-requests python3-toml
EOF
ADD scripts\* /airwatchConnector
ADD pre-start.py /
ADD start.sh /
RUN chmod u+x /start.sh
ENTRYPOINT ["/start.sh"]

26
pre-start.py Normal file
View File

@@ -0,0 +1,26 @@
import os
import toml
baseDir = "/airwatchConnector"
logDir = f"{baseDir}/logs"
confDir = f"{baseDir}/conf"
confFiles = os.listdir(confDir)
# On récupère que les fichiers .conf
confFiles = [conf for conf in confFiles if os.path.isfile(f"{confDir}/{conf}") and not conf.endswith(".conf")]
for conf in confFiles:
# On forme un nom à partir du nom du fichier de conf sans l'extension
# et on enlève les espaces
confName = conf[:5].replace(' ', '')
with open(f"{confDir}/{conf}", "r") as f:
settings = toml.load(f)
# Create log folder
if(settings["LOGS"]["Enabled"]):
logPath = settings["LOGS"].get(Path)
if not os.path.exists(f"{logDir}/{logPath}"):
os.makedirs(f"{logDir}/{logPath}")

View File

@@ -2,7 +2,6 @@
import os import os
import argparse import argparse
import logging import logging
import time
from functions import getSettings from functions import getSettings
from includes.airwatchAPI import * from includes.airwatchAPI import *
from includes.GLPIAPI import * from includes.GLPIAPI import *
@@ -21,7 +20,7 @@ args = parser.parse_args()
if(args.configpath != None and args.configpath != ''): if(args.configpath != None and args.configpath != ''):
settings = getSettings(args.configpath) settings = getSettings(args.configpath)
else: else:
settings = getSettings("./conf/settings.conf") settings = getSettings(f"{os.path.realpath(os.path.dirname(__file__))}/conf/settings.conf")
#=========== Configuration des logs ===========# #=========== Configuration des logs ===========#
@@ -42,7 +41,7 @@ if(settings["LOGS"]["Enabled"]):
if(settings["LOGS"].get("Path") and settings["LOGS"].get("Path") != ""): if(settings["LOGS"].get("Path") and settings["LOGS"].get("Path") != ""):
fileHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}stagingUserAssignation.log") fileHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}stagingUserAssignation.log")
else: else:
fileHandler = logging.FileHandler('./logs/stagingUserAssignation.log') fileHandler = logging.FileHandler(f'{os.path.realpath(os.path.dirname(__file__))}/logs/stagingUserAssignation.log')
fileHandler.setLevel(logginglevel) fileHandler.setLevel(logginglevel)
fileHandler.setFormatter(formatter) fileHandler.setFormatter(formatter)
logger.addHandler(fileHandler) logger.addHandler(fileHandler)
@@ -57,7 +56,8 @@ if(not args.silent):
#======== Paramètres du script ========# #======== Paramètres du script ========#
# Emplacement du verrou # Emplacement du verrou
lockFile = './airwatchStagingUserAssignation.lock' nameForLockFile = settings["GLPI"]["UserAgent"].replace(' ', '-')
lockFile = f'{os.path.realpath(os.path.dirname(__file__))}/{nameForLockFile}_StagingUserAssignation.lock'
debug=args.debug debug=args.debug
@@ -68,20 +68,12 @@ if(args.staginguser != None):
# ====================================== # # ====================================== #
# Vérification de la présence du verrou avant de continuer # Vérification de la présence du verrou avant de continuer
if(os.path.isfile(lockFile) and not args.force): if(os.path.isfile(lockFile) and not args.force):
# Récupération du temps de création de verrou en minutes logger.debug('Lock file exists, exiting...')
lockTime = (time.time() - os.path.getmtime(lockFile)) // 60 exit(0)
# Recréation du verrou s'il existe depuis plus de 3 heures (crash)
# sinon on quitte, une synchro est déjà en cours
if(lockTime > 180):
os.remove(lockFile)
open(lockFile, "w").close()
else:
logger.debug('Lock file exists, exiting...')
exit(0)
else: else:
# Création du verrou s'il n'existe pas
open(lockFile, "w").close() open(lockFile, "w").close()
# Initialisation de l'api Airwatch # Initialisation de l'api Airwatch
@@ -151,9 +143,15 @@ for device in devices:
else: else:
logger.warning(f"Device with id {device.Id} is not assigned to any user in GLPI, skipping the device") logger.warning(f"Device with id {device.Id} is not assigned to any user in GLPI, skipping the device")
elif(deviceCount > 1): elif(deviceCount > 1):
logger.info(f"More than one entry found in GLPI for device with id {device.Id}") logger.error(f"{count} devices matching airwatch device {device.FriendlyName} (Airwatch id={device.Id}) in GLPI trashbin (GLPI ids = {', '.join(deviceID)}), skipping this device...")
else: else:
logger.error(f"Device {device.Id} with serialnumber {device.SerialNumber} not found in GLPI (in trash bin ?)") deviceIDTrash, dataTrash, deviceCountTrash = glpiapi.GetDevice(device, trashbin=True)
if(countTrash > 1):
logger.error(f"{countTrash} devices matching airwatch device {device.FriendlyName} (Airwatch id={device.Id}) in GLPI trashbin (GLPI ids = {', '.join(deviceIDTrash)}), skipping this device...")
elif(countTrash == 1):
logger.warning(f"Device {device.FriendlyName} (Airwatch id={device.Id}) in GLPI trashbin (GLPI id={deviceIDTrash}), skipping...")
else:
logger.error(f"Device {device.FriendlyName} (Airwatch id={device.Id}) not found in GLPI.")
# Suppression du verrou # Suppression du verrou

View File

@@ -5,7 +5,6 @@ def getSettings(settingsPath):
settingsDefault =""" settingsDefault ="""
[AIRWATCH] [AIRWATCH]
Server = "https://airwatchServer" Server = "https://airwatchServer"
ConsoleURI = "https://airwatchConsole"
APIKey = "APIKEY" APIKey = "APIKEY"
# Méthode d'authentification (CMSURL or PASSWORD) # Méthode d'authentification (CMSURL or PASSWORD)

243
scripts/includes/GLPIAPI.py Normal file
View File

@@ -0,0 +1,243 @@
import requests
import json
from datetime import datetime
class GLPIAPI:
def __init__(self, settings):
self.Server = settings["GLPI"]["Server"]
self.AppToken = settings["GLPI"]["AppToken"]
self.UserToken = settings["GLPI"]["UserToken"]
self.UserAgent = settings["GLPI"]["UserAgent"]
self.SessionToken = None
self.StatusCode = None
self.Headers = None
self.InitConnection()
def InitConnection(self):
initURI = '/apirest.php/initSession/'
GLPIHeaders = {
'Content-Type': 'application/json',
"Authorization": f"user_token {self.UserToken}",
"App-Token": self.AppToken
}
# Récupération d'un token de session
uri = f"{self.Server}{initURI}"
result = requests.get(uri, headers=GLPIHeaders)
self.StatusCode = result.status_code
if(result.status_code == 200):
self.SessionToken = result.json()["session_token"]
self.Headers = {
'Content-Type': 'application/json',
"Session-Token": self.SessionToken,
"App-Token": self.AppToken
}
def GetDevice(self, device):
if(device.Imei != ''):
# Recherche des appareils en fonction du numéro de série ou de l'imei
# l'imei pouvant être dans le champ numéro de série ou les champs imei custom
search_parameter = f'is_deleted=0&criteria[0][field]=5&withindexes=true&criteria[0][searchtype]=contains&criteria[0][value]=^{device.SerialNumber}$'\
f'&criteria[1][link]=OR&criteria[1][field]=5&criteria[1][searchtype]=contains&criteria[1][value]=^{device.Imei}$'\
f'&criteria[2][link]=OR&criteria[2][field]=76667&criteria[2][searchtype]=contains&criteria[2][value]=^{device.Imei}$'\
f'&criteria[3][link]=OR&criteria[3][field]=76670&criteria[3][searchtype]=contains&criteria[3][value]=^{device.Imei}$'
else:
# Recherche des appareils en fonction du numéro de série seulement
search_parameter = f'is_deleted=0&criteria[0][field]=5&withindexes=true&criteria[0][searchtype]=contains&criteria[0][value]=^{device.SerialNumber}$'
searchUri = f"{self.Server}/apirest.php/search/computer?{search_parameter}"
search = requests.get(searchUri, headers=self.Headers)
if(search.status_code == 200):
search = search.json()
if(search["totalcount"] == 1):
deviceID = list(search["data"].keys())[0]
data = search["data"][deviceID]
return deviceID, data, search["totalcount"]
elif(search["totalcount"] > 1):
deviceID = list(search["data"].keys())
return deviceID, search["data"], search["totalcount"]
else:
return None, None, 0
return None, None, None
def GetUser(self, username=None, email=None):
if(username != None):
search_parameter = f'is_deleted=0&criteria[0][field]=1&withindexes=true&criteria[0][searchtype]=contains&criteria[0][value]=^{username}$'
elif(email != None):
search_parameter = f'is_deleted=0&criteria[0][field]=5&withindexes=true&criteria[0][searchtype]=contains&criteria[0][value]=^{email}$'
searchUri = f"{self.Server}/apirest.php/search/user?{search_parameter}"
search = requests.get(searchUri, headers=self.Headers)
if(search.status_code == 200):
search = search.json()
if(search["totalcount"] == 1):
userID = list(search["data"].keys())[0]
data = search["data"][userID]
return userID, data, search["totalcount"]
elif(search["totalcount"] > 1):
userID = list(search["data"].keys())
return userID, search["data"], search["totalcount"]
else:
return None, None, 0
def UpdateInventory(self, inventory):
headers = {
"Content-Type":"Application/x-compress",
"user-agent":self.UserAgent
}
return requests.post(self.Server, headers=headers, data=inventory)
def UpdateSerialNumber(self, deviceid, serialnumber):
body = {
"input" : {
"id" : deviceid,
"serial" : serialnumber
}
}
uri = f"{self.Server}/apirest.php/Computer/"
return requests.put(uri, headers=self.Headers, json=body)
def UpdateUser(self, deviceid, username):
body = {
"input" : {
"id" : deviceid,
"users_id" : username
}
}
uri = f"{self.Server}/apirest.php/Computer/"
return requests.put(uri, headers=self.Headers, json=body)
def CreateInventoryForAirwatchDevice(self, device, deviceName, apps=None):
platforms = {
2:"Apple iOS",
5:"Android",
12:"Windows Desktop"
}
if(device.PlatformId in platforms.keys()):
platformName = platforms[device.PlatformId]
else:
platformName = "Unknown"
processorArchs = {
0:{
"osArch":"arm64",
"softwareArch":"arm64"
},
9:{
"osArch":"64-bit",
"softwareArch":"x86_64"
}
}
if(device.Arch in processorArchs.keys()):
osArch = processorArchs[device.Arch]["osArch"]
softwareArch = processorArchs[device.Arch]["softwareArch"]
else:
osArch = "Unknown"
softwareArch = "Unknown"
logDate = datetime.strptime(device.LastSeen, "%Y-%m-%dT%H:%M:%S.%f").strftime("%Y-%m-%d %H:%M:%S")
inventory = GLPIInventory(logdate=logDate, versionclient=self.UserAgent, tag=device.Group, deviceid=f"{deviceName} - {device.SerialNumber}", itemtype="Computer")
inventory.SetOperatingSystem(platformName, device.OS, osArch)
inventory.SetHardware(deviceName, device.Uuid, device.TotalMemory)
inventory.AddUser(device.User)
if(apps != None):
for app in apps:
if(app.Status != "Installed"):
continue
install_date = datetime.strptime(app.InstallDate, "%Y-%m-%dT%H:%M:%S.%f").strftime("%Y-%m-%d")
if(install_date == "1-01-01"):
inventory.AddSoftware(app.Name, app.Version, app.Size, softwareArch, app.Guid)
else:
inventory.AddSoftware(app.Name, app.Version, app.Size, softwareArch, app.Guid, install_date)
return inventory
class GLPIInventory:
def __init__(self, logdate=None, versionclient=None, tag=None, deviceid=None, itemtype=None):
self.logdate = logdate
self.versionclient = versionclient
self.users = []
self.operatingsystem = {}
self.softwares = []
self.hardware = {}
self.tag = tag
self.deviceId = deviceid
self.itemType = itemtype
def AddUser(self, user):
self.users += [{
"login": user
}]
def DelUser(self, user):
for i in range(0, len(self.users)):
if(self.users[i]["login"] == user):
del self.users[i]
def AddSoftware(self, name, version, filesize, arch, guid, install_date=None):
if(install_date == None):
self.softwares += [{
"name": name,
"guid": guid,
"version": version,
"filesize": filesize,
"arch": arch
}]
else:
self.softwares += [{
"name": name,
"guid": guid,
"version": version,
"install_date": install_date,
"filesize": filesize,
"arch": arch
}]
def DelSoftware(self, guid):
for i in range(0, len(self.softwares)):
if(self.softwares[i]["guid"] == guid):
del self.softwares[i]
def SetHardware(self, name, uuid, memory):
self.hardware = {
"name": name,
"uuid": uuid,
"memory": memory
}
def SetOperatingSystem(self, name, version, arch):
self.operatingsystem = {
"name": name,
"version": version,
"full_name": f"{name} {version}",
"arch": arch
}
def Json(self):
inventory = {
"action": "inventory",
"content":{
"accesslog":{
"logdate": self.logdate
},
"versionclient": self.versionclient,
"users": self.users,
"operatingsystem": self.operatingsystem,
"softwares": self.softwares,
"hardware": self.hardware
},
"tag": self.tag,
"deviceid": self.deviceId,
"itemtype": self.itemType
}
return json.dumps(inventory)

View File

View File

@@ -0,0 +1,212 @@
import base64
import requests
from cryptography.hazmat.primitives.serialization import pkcs12, pkcs7
from cryptography.hazmat.primitives import hashes, serialization
class AirwatchAPI:
def __init__(self, settings):
self.Server = settings["AIRWATCH"]["Server"]
self.APIKey = settings["AIRWATCH"]["APIKey"]
self.AuthMethod = (settings["AIRWATCH"]["AuthenticationMethod"]).upper()
if(self.AuthMethod == "PASSWORD"):
self.APIUser = settings["AIRWATCH"]["APIUser"]
self.APIPassword = settings["AIRWATCH"]["APIPassword"]
if(self.AuthMethod == "CMSURL"):
self.CertificatePath = settings["AIRWATCH"]["CertificatePath"]
self.CertificatePassword = settings["AIRWATCH"]["CertificatePassword"]
def GetHeaders(self, uri):
if(self.AuthMethod == "PASSWORD"):
airwatchAPIUserToken = base64.b64encode(f"{self.APIUser}:{self.APIPassword}".encode('ascii')).decode("ascii")
return {
"Authorization": f"Basic {airwatchAPIUserToken}",
"aw-tenant-code": self.APIKey,
"Accept": "application/json"
}
else:
signing_data = uri.split('?')[0]
with open(self.CertificatePath, 'rb') as certfile:
cert = certfile.read()
key, certificate, additional_certs = pkcs12.load_key_and_certificates(cert, self.CertificatePassword.encode())
options = [pkcs7.PKCS7Options.DetachedSignature]
signed_data = pkcs7.PKCS7SignatureBuilder().set_data(signing_data.encode("UTF-8")).add_signer(certificate, key, hashes.SHA256()).sign(serialization.Encoding.DER, options)
signed_data_b64 = base64.b64encode(signed_data).decode()
return {
"Authorization": f"CMSURL'1 {signed_data_b64}",
"aw-tenant-code": self.APIKey,
"Accept": "application/json"
}
def GetUser(self, username):
cmdURI = f'/API/system/users/search?username={username}'
airwatchHeaders = self.GetHeaders(cmdURI)
uri = f"{self.Server}{cmdURI}"
user = requests.get(uri, headers=airwatchHeaders)
if(user.status_code == 200):
return AirwatchUser(user.json()["Users"][0])
return None
def GetDevices(self, user: str = ""):
if(user == ""):
cmdURI = f"/API/mdm/devices/search?pagesize=500&page="
else:
cmdURI = f"/API/mdm/devices/search?user={user}&pagesize=500&page="
airwatchHeaders = self.GetHeaders(cmdURI)
pageNum = 0
devices = []
uri = f"{self.Server}{cmdURI}{pageNum}"
result = requests.get(uri, headers=airwatchHeaders)
if(result.status_code == 200):
deviceTotalCount = result.json()["Total"]
while(len(devices) != deviceTotalCount):
uri = f"{self.Server}{cmdURI}{pageNum}"
result = requests.get(uri, headers=airwatchHeaders).json()["Devices"]
for device in result:
devices += [AirwatchDevice(device)]
pageNum += 1
return devices
return None
def GetDeviceApps(self, device):
cmdURI = f"/api/mdm/devices/{device.Uuid}/apps/search"
airwatchHeaders = self.GetHeaders(cmdURI)
uri = f"{self.Server}{cmdURI}"
apps = []
result = requests.get(uri, headers=airwatchHeaders)
if(result.status_code == 200):
for app in result.json()["app_items"]:
apps += [AirwatchApplication(app)]
return apps
return None
def ResetDEPProfiles(self, groupUuid):
cmdURI = f"/API/mdm/dep/groups/{groupUuid}/devices"
uri = f"{self.Server}{cmdURI}"
airwatchHeaders = self.GetHeaders(cmdURI)
result = requests.get(uri, headers=airwatchHeaders)
if(result.status_code == 200):
for device in result.json():
if (device["enrollmentStatus"] != "Unenrolled"):
continue
assignDEPProfileURI = f"/API/mdm/dep/profiles/{device['profileUuid']}/devices/{device['deviceSerialNumber']}?action=Assign"
uri = f"{airwatchServer}{assignDEPProfileURI}"
airwatchHeaders = self.GetHeaders(assignDEPProfileURI)
requests.put(uri, headers=airwatchHeaders)
return True
return False
def GetEnrollmentTokens(self, groupUuid, deviceType=2):
cmdURI = f"/API/mdm/groups/{groupUuid}/enrollment-tokens?device_type={deviceType}&page_size=500"
uri = f"{self.Server}{cmdURI}"
airwatchHeaders = self.GetHeaders(cmdURI)
result = requests.get(uri, headers=airwatchHeaders)
return result.json()["tokens"]
def UpdateUserOnEnrollmentTokens(self, groupUuid,user, serialnumber):
body = {
"registration_type": "REGISTER_DEVICE",
"device_registration_record": {
"user_uuid": user.Uuid,
"friendly_name": serialnumber,
"ownership_type": "CORPORATE_DEDICATED",
"serial_number": serialnumber,
"to_email_address": user.Email,
"message_type": 0
}
}
cmdURI = f"/API/mdm/groups/{groupUuid}/enrollment-tokens"
airwatchHeaders = self.GetHeaders(cmdURI)
airwatchHeaders["Accept"] = "application/json;version=2"
uri = f"{self.Server}{cmdURI}"
print(uri)
return requests.post(uri, headers=airwatchHeaders, json=body)
def SyncDEPDevices(self, groupUuid):
cmdURI = f"/API/mdm/dep/groups/{groupUuid}/devices?action=sync"
uri = f"{self.Server}{cmdURI}"
airwatchHeaders = self.GetHeaders(cmdURI)
return requests.put(uri, headers=airwatchHeaders).status_code
def SetDeviceFriendlyName(self, device, friendlyName):
cmdURI = f"/API/mdm/devices/{device.Id}"
airwatchHeaders = self.GetHeaders(cmdURI)
uri = f"{self.Server}{cmdURI}"
body = {
"DeviceFriendlyName":friendlyName
}
return requests.put(uri, headers=airwatchHeaders, json=body).status_code
def SetDeviceUser(self, device, airwatchUser):
cmdURI = f'/API/mdm/devices/{device.Id}/enrollmentuser/{airwatchUser.Id}'
uri = f"{self.Server}{cmdURI}"
airwatchHeaders = self.GetHeaders(cmdURI)
return requests.patch(uri, headers=airwatchHeaders).status_code
def DeleteDevice(self, device):
cmdURI = f"/API/mdm/devices/{device.Id}"
airwatchHeaders = self.GetHeaders(cmdURI)
uri = f"{self.Server}{cmdURI}"
return requests.delete(uri, headers=airwatchHeaders).status_code
class AirwatchUser:
def __init__(self, user):
self.Id = user["Id"]["Value"]
self.Uuid = user["Uuid"]
self.UserName = user["UserName"]
self.FirstName = user["FirstName"]
self.LastName = user["LastName"]
self.Enabled = user["Status"]
self.Email = user["Email"]
self.DisplayName = user["DisplayName"]
self.Group = user["Group"]
self.GroupId = user["LocationGroupId"]
self.OrgUuid = user["OrganizationGroupUuid"]
if(user["EnrolledDevicesCount"] != ''):
self.DeviceCount = int(user["EnrolledDevicesCount"])
else:
self.DeviceCount = 0
class AirwatchDevice:
def __init__(self, device):
self.Id = device["Id"]["Value"]
self.Uuid = device["Uuid"]
self.SerialNumber = device["SerialNumber"]
self.Imei = device["Imei"]
self.MacAddress = device["MacAddress"]
self.FriendlyName = device["DeviceFriendlyName"]
self.GroupId = device["LocationGroupId"]["Id"]["Value"]
self.Group = device["LocationGroupName"]
self.GroupUuid = device["LocationGroupId"]["Uuid"]
if(device["UserId"].get("Id") != None):
self.UserId = device["UserId"]["Id"]["Value"]
self.User = device["UserName"]
else:
self.UserId = None
self.User = None
self.UserEmail = device["UserEmailAddress"]
self.PlatformId = device["PlatformId"]["Id"]["Value"]
self.Platform = device["Platform"]
self.OS = device["OperatingSystem"]
self.Arch = device["ProcessorArchitecture"]
self.TotalMemory = device["TotalPhysicalMemory"]
self.EnrollmentStatus = device["EnrollmentStatus"]
self.LastEnrolledOn = device["LastEnrolledOn"]
self.LastSeen = device["LastSeen"]
class AirwatchApplication:
def __init__(self, application):
self.Name = application["name"]
self.Guid = application["bundle_id"]
self.Size = application["size"]
self.Version = application["installed_version"]
self.InstallDate = application["latest_uem_action_time"]
self.Status = application["installed_status"]

View File

@@ -1,5 +1,3 @@
--extra-index-url=https://gitlab.forge.education.gouv.fr/api/v4/projects/13771/packages/pypi/simple cryptography
--extra-index-url=https://gitlab.forge.education.gouv.fr/api/v4/projects/13820/packages/pypi/simple requests
AirwatchAPI==1.0.3
GLPIAPI==1.0.3
toml toml

View File

@@ -2,11 +2,10 @@
import os import os
import argparse import argparse
import logging import logging
import time
from datetime import datetime from datetime import datetime
from functions import getSettings from functions import getSettings
from AirwatchAPI.AirwatchAPI import * from includes.airwatchAPI import *
from GLPIAPI.GLPIAPI import * from includes.GLPIAPI import *
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("-sF", "--searchFilter", dest="searchfilter", type=str, choices=["Id", "SerialNumber", "Imei", "UserName"]) parser.add_argument("-sF", "--searchFilter", dest="searchfilter", type=str, choices=["Id", "SerialNumber", "Imei", "UserName"])
@@ -23,7 +22,7 @@ args = parser.parse_args()
if(args.configpath != None and args.configpath != ''): if(args.configpath != None and args.configpath != ''):
settings = getSettings(args.configpath) settings = getSettings(args.configpath)
else: else:
settings = getSettings(f"{os.path.realpath(os.path.dirname(__file__))}/conf/settings.conf") settings = getSettings("{os.path.realpath(os.path.dirname(__file__))}/conf/settings.conf")
#=========== Configuration des logs ===========# #=========== Configuration des logs ===========#
@@ -106,27 +105,17 @@ platformFilterOut = [12]
# ====================================== # # ====================================== #
# Vérification de la présence du verrou avant de continuer # Vérification de la présence du verrou avant de continuer
if(os.path.isfile(lockFile) and not args.force): if(os.path.isfile(lockFile) and not args.force):
# Récupération du temps de création de verrou en minutes logger.debug('Lock file exists, exiting...')
lockTime = (time.time() - os.path.getmtime(lockFile)) // 60 exit(0)
# Recréation du verrou s'il existe depuis plus de 3 heures (crash)
# sinon on quitte, une synchro est déjà en cours
if(lockTime > 180):
os.remove(lockFile)
open(lockFile, "w").close()
else:
logger.debug('Lock file exists, exiting...')
exit(0)
else: else:
# Création du verrou s'il n'existe pas
open(lockFile, "w").close() open(lockFile, "w").close()
logger.info("========= Synchronization started =========") logger.info("========= Synchronization started =========")
try: try:
airwatch = AirwatchAPI(Server=settings["AIRWATCH"]["Server"], APIKey=settings["AIRWATCH"]["APIKey"], AuthMethod=settings["AIRWATCH"]["AuthenticationMethod"], CertPath=settings["AIRWATCH"]["CertificatePath"], CertPass=settings["AIRWATCH"]["CertificatePassword"]) airwatch = AirwatchAPI(settings)
# recherche des appareils # recherche des appareils
devices = airwatch.GetDevices() devices = airwatch.GetDevices()
logger.info("Airwatch server connection succeeded") logger.info("Airwatch server connection succeeded")
@@ -141,7 +130,7 @@ except Exception as error:
# Initialisation de l'api GLPI # Initialisation de l'api GLPI
try: try:
glpiapi = GLPIAPI(Server=settings["GLPI"]["Server"], AppToken=settings["GLPI"]["AppToken"], UserToken=settings["GLPI"]["UserToken"], UserAgent=settings["GLPI"]["UserAgent"]) glpiapi = GLPIAPI(settings)
logger.info("GLPI server connection succeeded") logger.info("GLPI server connection succeeded")
except requests.exceptions.ConnectionError as error: except requests.exceptions.ConnectionError as error:
logger.critical(f"Connection to GLPI server failed : {error}") logger.critical(f"Connection to GLPI server failed : {error}")
@@ -196,7 +185,7 @@ if(searchFilter != None):
if(searchFilter == 'Id'): if(searchFilter == 'Id'):
devices = [device for device in devices if getattr(device, "Id") == searchValue] devices = [device for device in devices if getattr(device, "Id") == searchValue]
else: else:
devices = [device for device in devices if getattr(device, searchFilter) == searchValue] devices = [device for device in devices if getattr(device, "searchFilter") == searchValue]
for device in devices: for device in devices:
if(device.EnrollmentStatus != 'Enrolled'): if(device.EnrollmentStatus != 'Enrolled'):
@@ -209,13 +198,13 @@ for device in devices:
logger.info(f"Searching device {device.FriendlyName} (Airwatch id={device.Id}) on GLPI") logger.info(f"Searching device {device.FriendlyName} (Airwatch id={device.Id}) on GLPI")
deviceID, data, count = glpiapi.GetComputers(airwatchDevice=device) deviceID, data, count = glpiapi.GetDevice(device)
apps = airwatch.GetDeviceApps(device) apps = airwatch.GetDeviceApps(device)
if(count > 1): if(count > 1):
loggerDouble.error(f"{count} devices matching airwatch device {device.FriendlyName} (Airwatch id={device.Id}) in GLPI (GLPI ids = {', '.join(deviceID)}), skipping this device...") loggerDouble.error(f"{count} devices matching airwatch device {device.FriendlyName} (Airwatch id={device.Id}) in GLPI (GLPI ids = {', '.join(deviceID)}), skipping this device...")
continue continue
if(count == 0): if(count == 0):
deviceIDTrash, dataTrash, countTrash = glpiapi.GetComputers(device) deviceIDTrash, dataTrash, countTrash = glpiapi.GetDevice(device)
if(countTrash > 1): if(countTrash > 1):
loggerDouble.error(f"{countTrash} devices matching airwatch device {device.FriendlyName} (Airwatch id={device.Id}) in GLPI trashbin (GLPI ids = {', '.join(deviceIDTrash)}), skipping this device...") loggerDouble.error(f"{countTrash} devices matching airwatch device {device.FriendlyName} (Airwatch id={device.Id}) in GLPI trashbin (GLPI ids = {', '.join(deviceIDTrash)}), skipping this device...")
elif(countTrash == 1): elif(countTrash == 1):
@@ -227,18 +216,11 @@ for device in devices:
inventory = glpiapi.CreateInventoryForAirwatchDevice(device, data["1"], apps) inventory = glpiapi.CreateInventoryForAirwatchDevice(device, data["1"], apps)
# Mise à jour du friendly name sur Airwatch # Mise à jour du friendly name sur Airwatch
platformName = inventory.operatingsystem["name"] platformName = inventory.operatingsystem["name"]
osVersion = inventory.operatingsystem["version"] if(device.FriendlyName != f"{data['1']} {platformName} {device.OS} - {device.User}"):
if(device.FriendlyName != f"{data['1']} {platformName} {osVersion} - {device.User}"): newFriendlyName = f"{data['1']} {platformName} {device.OS} - {device.User}"
newFriendlyName = f"{data['1']} {platformName} {osVersion} - {device.User}"
logger.info(f"Updating device friendlyname to {newFriendlyName}") logger.info(f"Updating device friendlyname to {newFriendlyName}")
airwatch.SetDeviceFriendlyName(device, newFriendlyName) airwatch.SetDeviceFriendlyName(device, newFriendlyName)
# Mise à jour de l'url vers la page airwatch de l'appareil sur GLPI
airwatchlink = f"{settings['AIRWATCH']['ConsoleURI']}/AirWatch/#/AirWatch/Device/Details/Summary/{device.Id}"
if(data['76689'] != airwatchlink):
glpiapi.SetCustomField(itemType="Computer", containerName="mdt", containerID=4, itemId=deviceID, fieldName="appareilsurmagentafield", data=airwatchlink)
# filtre des plateformes # filtre des plateformes
if(platformFilterEnabled): if(platformFilterEnabled):
if device.PlatformId in platformFilterOut: if device.PlatformId in platformFilterOut:
@@ -249,10 +231,10 @@ for device in devices:
glpiapi.UpdateInventory(inventory.Json()) glpiapi.UpdateInventory(inventory.Json())
if(data['70'] == None and device.User != settings["AIRWATCH"]["StagingUser"]): if(data['70'] == None and device.User != settings["AIRWATCH"]["StagingUser"]):
userID, userData, userCount = glpiapi.GetUsers(username=device.User) userID, userData, userCount = glpiapi.GetUser(device.User)
if(userCount == 1): if(userCount == 1):
logger.info(f"Updating user from {data['70']} to {device.User} in GLPI (id={deviceID})") logger.info(f"Updating user from {data['70']} to {device.User} in GLPI (id={deviceID})")
glpiapi.UpdateItemUser(deviceID, "Computer", userID) glpiapi.UpdateUser(deviceID, userID)
if(data['5'] != device.SerialNumber): if(data['5'] != device.SerialNumber):
logger.info(f"Updating serial number from {data['5']} to {device.SerialNumber} in GLPI (id={deviceID})") logger.info(f"Updating serial number from {data['5']} to {device.SerialNumber} in GLPI (id={deviceID})")

39
start.sh Normal file
View File

@@ -0,0 +1,39 @@
#!/bin/bash
python /pre-start.py
if [ $CONF != '' ]; then
case $TASK in
syncGLPI)
python3 /airwatchConnector/sync.py -c $CONF -f
;;
stagingAssignment)
python3 /airwatchConnector/stagingUserAssignation.py -c $CONF -f
;;
*)
exit 0
;;
esac
else
confFiles = $(ls /airwatchConnector/conf/*.conf)
if [ $confFiles = '' ]; then exit 1; do
for CONF in $confFiles; do
case $TASK in
syncGLPI)
python3 /airwatchConnector/sync.py -c $CONF -for
;;
stagingAssignment)
python3 /airwatchConnector/stagingUserAssignation.py -c $CONF -f
;;
*)
exit 0
;;
esac
done
fi