Files
glpi-airwatch-sync/scripts/syncGLPI.py

263 lines
11 KiB
Python

#!/usr/bin/python3
import os
import argparse
import logging
import time
from datetime import datetime
from functions import getSettings
from includes.airwatchAPI import *
from includes.GLPIAPI import *
parser = argparse.ArgumentParser()
parser.add_argument("-sF", "--searchFilter", dest="searchfilter", type=str, choices=["Id", "SerialNumber", "Imei", "UserName"])
parser.add_argument("-sV","--searchValue", dest="searchvalue", type=str)
parser.add_argument("-c", "--configPath", dest="configpath", type=str)
parser.add_argument("-f", "--force", dest="force", action="store_true")
parser.add_argument("-s", "--silent", dest="silent", action="store_true")
parser.add_argument("-v", "--verbose", dest="debug", action="store_true")
args = parser.parse_args()
# Récupération des informations du fichier de configuration
if(args.configpath != None and args.configpath != ''):
settings = getSettings(args.configpath)
else:
settings = getSettings(f"{os.path.realpath(os.path.dirname(__file__))}/conf/settings.conf")
#=========== Configuration des logs ===========#
# handler pour les logs de base
logger = logging.getLogger(__name__)
# handler pour log les doublons dans GLPI
loggerDouble = logging.getLogger('doubleGLPI')
# hander pour log les appareils manquants dans GLPI
loggerMissing = logging.getLogger('missingGLPI')
if(args.debug or settings["LOGS"]["Debug"]):
logginglevel = logging.DEBUG
else:
logginglevel = logging.INFO
logger.setLevel(logginglevel)
loggerDouble.setLevel(logginglevel)
loggerMissing.setLevel(logginglevel)
formatter = logging.Formatter(fmt='%(asctime)s | %(levelname)s: %(message)s', datefmt='%Y/%m/%d %H:%M:%S')
# handler pour log dans un fichier
if(settings["LOGS"]["Enabled"]):
# File Handler
if(settings["LOGS"].get("Path") and settings["LOGS"].get("Path") != ""):
fileHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}syncGLPI.log")
fileErrorHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}syncGLPI-errors.log")
fileDoubleHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}syncGLPI-double.log")
fileMissingHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}syncGLPI-missing.log")
else:
fileHandler = logging.FileHandler('{os.path.realpath(os.path.dirname(__file__))}/logs/syncGLPI.log')
fileErrorHandler = logging.FileHandler("{os.path.realpath(os.path.dirname(__file__))}/logs/syncGLPI-errors.log")
fileDoubleHandler = logging.FileHandler("{os.path.realpath(os.path.dirname(__file__))}/logs/syncGLPI-double.log")
fileMissingHandler = logging.FileHandler("{os.path.realpath(os.path.dirname(__file__))}/logs/syncGLPI-missing.log")
# Set Logging Level to files handler
fileHandler.setLevel(logginglevel)
fileErrorHandler.setLevel(logging.ERROR)
fileDoubleHandler.setLevel(logging.ERROR)
fileMissingHandler.setLevel(logging.ERROR)
# Set Formatter to file handler
fileHandler.setFormatter(formatter)
fileErrorHandler.setFormatter(formatter)
fileDoubleHandler.setFormatter(formatter)
fileMissingHandler.setFormatter(formatter)
# Add Handler to loggers
logger.addHandler(fileHandler)
logger.addHandler(fileErrorHandler)
loggerDouble.addHandler(fileDoubleHandler)
loggerMissing.addHandler(fileMissingHandler)
# handler pour log dans la console
if(not args.silent):
consoleHandler = logging.StreamHandler()
consoleHandler.setLevel(logginglevel)
consoleHandler.setFormatter(formatter)
logger.addHandler(consoleHandler)
#======== Paramètres du script ========#
# Emplacement du verrou
nameForLockFile = settings["GLPI"]["UserAgent"].replace(' ', '-')
lockFile = f'{os.path.realpath(os.path.dirname(__file__))}/{nameForLockFile}_SyncGLPI.lock'
logger.debug(f"============ Settings ============")
logger.debug(f"Airwatch server: {settings['AIRWATCH']['Server']}")
logger.debug(f"Authentication method : {settings['AIRWATCH']['AuthenticationMethod']}")
logger.debug(f"Staging user: {settings['AIRWATCH']['StagingUser']}")
logger.debug(f"GLPI server: {settings['GLPI']['Server']}")
logger.debug(f"UserAgent: {settings['GLPI']['UserAgent']}")
# Filtres
searchFilter = args.searchfilter
searchValue = args.searchvalue
# Platform exclusion (12 = computer)
platformFilterEnabled = True
platformFilterOut = [12]
# ====================================== #
# Vérification de la présence du verrou avant de continuer
if(os.path.isfile(lockFile) and not args.force):
# Récupération du temps de création de verrou en minutes
lockTime = (time.time() - os.path.getmtime(lockFile)) // 60
# 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:
# Création du verrou s'il n'existe pas
open(lockFile, "w").close()
logger.info("========= Synchronization started =========")
try:
airwatch = AirwatchAPI(settings)
# recherche des appareils
devices = airwatch.GetDevices()
logger.info("Airwatch server connection succeeded")
except FileNotFoundError as F:
logger.critical(f"Certificate file not found for CMSURL authentication : {F}")
os.remove(lockFile)
exit(1)
except Exception as error:
logger.critical(f"Connection to Airwatch server failed : {error}")
os.remove(lockFile)
exit(1)
# Initialisation de l'api GLPI
try:
glpiapi = GLPIAPI(settings)
logger.info("GLPI server connection succeeded")
except requests.exceptions.ConnectionError as error:
logger.critical(f"Connection to GLPI server failed : {error}")
os.remove(lockFile)
exit(1)
logger.info(f"Number of devices found in Airwatch : {len(devices)}")
# ====================== Début suppression des doublons ================================= #
# On récupére les numéros de série
serials = [device.SerialNumber for device in devices]
# On garde ceux qui sont présent plus d'une fois et qui n'ont pas HUBNOSERIAL (BYOD) comme numéro de série
serials = {serial for serial in serials if serials.count(serial) > 1 and serial != 'HUBNOSERIAL'}
# Récupération des devices et de la dernière date d'enrolement
devicesDouble = {}
for serial in serials:
# on fait une liste des appareils avec le même numéro de série
# que l'on stocke dans un dictionnaire avec le numéro de série en clé
devicesDouble[serial] = [[device,datetime.strptime(device.LastEnrolledOn, "%Y-%m-%dT%H:%M:%S.%f")] for device in devices if serial == device.SerialNumber]
logger.info(f"Duplicates detected : {len(devicesDouble)}")
# On supprime les doublons qui ne se sont pas enrôlés en dernier
devicesToDelete = []
for k,v in devicesDouble.items():
latest = None
for d in v:
if(latest == None):
latest = d
else:
if(latest[1] < d[1]):
devicesToDelete += [latest[0]]
latest = d
else:
devicesToDelete += [d[0]]
# envoi de la requête de suppression des appareils sur airwatch
for device in devicesToDelete:
logger.info(f"Deleting {device.Id} - {device.FriendlyName} in Airwatch")
airwatch.DeleteDevice(device)
devices = airwatch.GetDevices()
# ====================== Fin suppression des doublons ================================= #
if(searchFilter != None):
logger.debug(f"SearchFilter set to {searchFilter}")
logger.debug(f"SearchValue set to {searchValue}")
if(searchFilter == 'Id'):
devices = [device for device in devices if getattr(device, "Id") == searchValue]
else:
devices = [device for device in devices if getattr(device, "searchFilter") == searchValue]
for device in devices:
if(device.EnrollmentStatus != 'Enrolled'):
logger.warning(f"Device with Airwatch id {device.Id} not enrolled, skipping this device...")
continue
if(device.SerialNumber == 'HUBNOSERIAL'):
logger.info(f"Device with Airwatch id {device.Id} is using work profile, skipping...")
continue
logger.info(f"Searching device {device.FriendlyName} (Airwatch id={device.Id}) on GLPI")
deviceID, data, count = glpiapi.GetDevice(device)
apps = airwatch.GetDeviceApps(device)
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...")
continue
if(count == 0):
deviceIDTrash, dataTrash, countTrash = glpiapi.GetDevice(device)
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...")
elif(countTrash == 1):
logger.warning(f"Device {device.FriendlyName} (Airwatch id={device.Id}) in GLPI trashbin (GLPI id={deviceIDTrash}), skipping...")
else:
loggerMissing.error(f"Device {device.FriendlyName} (Airwatch id={device.Id}) not found in GLPI.")
continue
inventory = glpiapi.CreateInventoryForAirwatchDevice(device, data["1"], apps)
# Mise à jour du friendly name sur Airwatch
platformName = inventory.operatingsystem["name"]
osVersion = inventory.operatingsystem["version"]
if(device.FriendlyName != f"{data['1']} {platformName} {osVersion} - {device.User}"):
newFriendlyName = f"{data['1']} {platformName} {osVersion} - {device.User}"
logger.info(f"Updating device friendlyname to {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.UpdateAirwatchLink(deviceID, airwatchlink)
# filtre des plateformes
if(platformFilterEnabled):
if device.PlatformId in platformFilterOut:
logger.info(f"Device platform ({device.PlatformId}) is filtered out, not updating GLPI")
continue
logger.info(f"Updating {deviceID} on GLPI")
glpiapi.UpdateInventory(inventory.Json())
if(data['70'] == None and device.User != settings["AIRWATCH"]["StagingUser"]):
userID, userData, userCount = glpiapi.GetUser(device.User)
if(userCount == 1):
logger.info(f"Updating user from {data['70']} to {device.User} in GLPI (id={deviceID})")
glpiapi.UpdateUser(deviceID, userID)
if(data['5'] != device.SerialNumber):
logger.info(f"Updating serial number from {data['5']} to {device.SerialNumber} in GLPI (id={deviceID})")
glpiapi.UpdateSerialNumber(deviceID, device.SerialNumber)
logger.info("========= End of synchronization =========")
logger.debug('Removing lock file')
os.remove(lockFile)