Compare commits

..

10 Commits

12 changed files with 165 additions and 129 deletions

View File

@ -3,7 +3,7 @@
## Explication de l'usage des scripts et de la configuration :
### 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 :
@ -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
- **-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
Installation des paquets linux :
@ -98,9 +102,28 @@ apt-get 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
@ -172,7 +195,7 @@ Description=Script qui assigne les appareils en staging aux utilisateurs en se b
[Timer]
OnUnitInactiveSec=1m
AccuracySec=1us
Unit=airwatchSync.service
Unit=airwatchStaging.service
[Install]
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 argparse
import logging
import time
from functions import getSettings
from includes.airwatchAPI import *
from includes.GLPIAPI import *
@ -21,7 +20,7 @@ args = parser.parse_args()
if(args.configpath != None and args.configpath != ''):
settings = getSettings(args.configpath)
else:
settings = getSettings("./conf/settings.conf")
settings = getSettings(f"{os.path.realpath(os.path.dirname(__file__))}/conf/settings.conf")
#=========== Configuration des logs ===========#
@ -42,7 +41,7 @@ if(settings["LOGS"]["Enabled"]):
if(settings["LOGS"].get("Path") and settings["LOGS"].get("Path") != ""):
fileHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}stagingUserAssignation.log")
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.setFormatter(formatter)
logger.addHandler(fileHandler)
@ -57,7 +56,8 @@ if(not args.silent):
#======== Paramètres du script ========#
# 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
@ -68,20 +68,12 @@ if(args.staginguser != None):
# ====================================== #
# 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()
# Initialisation de l'api Airwatch
@ -151,9 +143,15 @@ for device in devices:
else:
logger.warning(f"Device with id {device.Id} is not assigned to any user in GLPI, skipping the device")
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:
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

View File

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

View File

@ -33,17 +33,21 @@ class GLPIAPI:
"App-Token": self.AppToken
}
def GetDevice(self, device):
def GetDevice(self, device, trashbin=False):
if(trashbin):
checkDeleted = 1
else:
checkDeleted = 0
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}$'\
search_parameter = f'is_deleted={checkDeleted}&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}$'
search_parameter = f'is_deleted={checkDeleted}&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)
@ -61,27 +65,6 @@ class GLPIAPI:
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",
@ -100,33 +83,11 @@ class GLPIAPI:
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 UpdateAirwatchLink(self, deviceid, airwatchlink):
body = {
"input": {
"id": deviceid,
"appareilsurmagentafield": airwatchlink
}
}
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"
12:"Windows Desktop"
}
if(device.PlatformId in platforms.keys()):
@ -151,27 +112,9 @@ class GLPIAPI:
osArch = "Unknown"
softwareArch = "Unknown"
windowsOSTranslation = {
"10.0.19043":"10 21H1",
"10.0.19044":"10 21H2",
"10.0.19045":"10 22H2",
"10.0.22000":"11 21H2",
"10.0.22621":"11 22H2",
"10.0.22631":"11 23H2",
"10.0.26100":"11 24H2",
"10.0.26200":"11 25H2"
}
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")
if(platformName == "Windows"):
if(device.OS in windowsOSTranslation.keys()):
inventory.SetOperatingSystem(platformName, windowsOSTranslation[str(device.OS)], osArch)
else:
platformName = "Windows Desktop"
inventory.SetOperatingSystem(platformName, device.OS, osArch)
else:
inventory.SetOperatingSystem(platformName, device.OS, osArch)
inventory.SetHardware(deviceName, device.Uuid, device.TotalMemory)
inventory.AddUser(device.User)

View File

@ -168,10 +168,7 @@ class AirwatchUser:
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:
@ -185,12 +182,8 @@ class AirwatchDevice:
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"]

View File

@ -2,7 +2,6 @@
import os
import argparse
import logging
import time
from datetime import datetime
from functions import getSettings
from includes.airwatchAPI import *
@ -23,7 +22,7 @@ args = parser.parse_args()
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")
settings = getSettings("{os.path.realpath(os.path.dirname(__file__))}/conf/settings.conf")
#=========== Configuration des logs ===========#
@ -106,21 +105,11 @@ 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 =========")
@ -215,7 +204,7 @@ for device in devices:
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)
deviceIDTrash, dataTrash, countTrash = glpiapi.GetDevice(device, trashbin=True)
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):
@ -227,17 +216,11 @@ for device in devices:
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}"
if(device.FriendlyName != f"{data['1']} {platformName} {device.OS} - {device.User}"):
newFriendlyName = f"{data['1']} {platformName} {device.OS} - {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:
@ -247,12 +230,6 @@ for device in devices:
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)

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