Compare commits
42 Commits
c474ecc4c7
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 00a4652e0b | |||
| f004382573 | |||
| d52e0b7a2a | |||
| a89a17020a | |||
| 078d4f0923 | |||
| 20f28b71db | |||
| 2bb3eec219 | |||
| 2fa704f381 | |||
| ad634c7ec3 | |||
| 84c52ae234 | |||
| 5eb7b6b778 | |||
| e0e5ea37ce | |||
| 70211dc9ac | |||
| 43c4ba4639 | |||
| b63ff9d788 | |||
| 05a0b50741 | |||
| c46e426282 | |||
| 1228e2ac2a | |||
| 0a5e780546 | |||
| ce003e206a | |||
| 3e19b4d3e6 | |||
| 074a1e5313 | |||
| f697d2fcb4 | |||
| eb1bc4cc3b | |||
| 92a6259f04 | |||
| 570b89e50c | |||
| 15a34ba66c | |||
| 68a0d7d6ab | |||
| ecbd48cbc0 | |||
| 914abf5018 | |||
| 9f04afb153 | |||
| a4ee8a1ae8 | |||
| 8ea3fa0717 | |||
| e1174ce34b | |||
| 21e7736b8d | |||
| 6e288c0b47 | |||
| 3f6a46c345 | |||
| d3727f3c37 | |||
| 2fcb381062 | |||
| d97b4b60f9 | |||
| 6fb48c452e | |||
| a4c5eb7670 |
210
README.md
Normal file
210
README.md
Normal file
@ -0,0 +1,210 @@
|
||||
# Scripts Airwatch GLPI
|
||||
|
||||
## 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.
|
||||
|
||||
Voici un exemple du fichier de configuration :
|
||||
|
||||
```toml
|
||||
[AIRWATCH]
|
||||
Server = "https://airwatchServer"
|
||||
APIKey = "APIKEY"
|
||||
# Méthode d'authentification (CMSURL or PASSWORD)
|
||||
# CMSURL permet l'authentification avec un certificat utilisateur (CertificatePath, CertificatePassword)
|
||||
# PASSWORD permet l'authentification avec un nom d'utilisateur et un mot de passe (APIUser, APIPassword)
|
||||
AuthenticationMethod = "CMSURL"
|
||||
CertificatePath = "/path/to/cert"
|
||||
CertificatePassword = "12345"
|
||||
APIUser = "UserAPI"
|
||||
APIPassword = "PasswordUserAPI"
|
||||
|
||||
# Utilisateur de staging que l'on va remplacer par l'utilisateur trouvé dans GLPI
|
||||
StagingUser = "staging-pr"
|
||||
|
||||
[GLPI]
|
||||
Server = "http://127.0.0.1/glpi"
|
||||
AppToken = "GLPIAppToken"
|
||||
UserToken = "GLPIUserToken"
|
||||
|
||||
# User agent qui sera visible sur GLPI lors de la synchronisation
|
||||
UserAgent = "Airwatch Synchronizer"
|
||||
|
||||
[LOGS]
|
||||
Enabled = true
|
||||
# Chemin où seront créé les fichiers de log
|
||||
Path = "./logs/"
|
||||
# Mode debug pour avoir plus d'informations
|
||||
Debug = false
|
||||
|
||||
```
|
||||
---
|
||||
|
||||
### syncGLPI.py
|
||||
Le script syncGLPI.py permet de synchroniser les données des appareils présents dans Airwatch avec un inventaire GLPI. Les actions du script :
|
||||
- vérification et suppression des doublons en fonction du numéro de série et de la date de dernier enrôlement
|
||||
- vérification de la présence des appareils dans l'inventaire GLPI et envoi d'un inventaire à partir des données d'Airwatch pour mettre à jours les informations
|
||||
- modification du numéro de série de l'appareil sur GLPI si celui-ci n'est pas identique à celui d'Airwatch
|
||||
- modification du friendlyname de l'appareil sur Airwatch à partir du nom d'inventaire de l'appareil sur GLPI
|
||||
|
||||
#### Synchronisation
|
||||
Les éléments synchronisés de Airwatch vers GLPI :
|
||||
- l'utilisateur de l'appareil
|
||||
- la dernière date de remontée vers le serveur Airwatch
|
||||
- UUID
|
||||
- le nom du système d'exploitation et sa version
|
||||
- les logiciels présents sur la machine
|
||||
- le numéro de série
|
||||
|
||||
Les éléments synchronisés de GLPI vers Airwatch :
|
||||
- Le nom d'inventaire de la machine qui est mis pour le friendlyname
|
||||
|
||||
#### Paramètres
|
||||
Ce script possède les paramètres suivants qui sont optionnels pour son exécution :
|
||||
|
||||
- **-sF / --searchFilter** : permet de filtrer la recherche des appareils dans airwatch sur un attribut spécifique parmi la liste suivante : "Id", "SerialNumber", "Imei", "UserName"
|
||||
- **-sV / --searchValue** : la valeur pour la recherche lorsque -searchFilter est utilisé
|
||||
- **-f / --force** : permet d'outrepasser la vérification du verrou posé par le script lors de son exécution
|
||||
- **-c / --configPath** : permet de définir un chemin vers un fichier de configuration a utilisé pour l'exécution du script
|
||||
- **-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
|
||||
|
||||
---
|
||||
|
||||
### StagingUserAssignation.py
|
||||
Le script StagingUserAssignation.py permet d'assigner les appareils en staging qui sont assignés à un utilisateur de staging à l'utilisateur renseigné dans l'inventaire GLPI.
|
||||
|
||||
Il récupère le nom de l'utilisateur de staging dans le fichier de configuration.
|
||||
|
||||
#### Paramètres
|
||||
- **-u / --staginguser** : permet de préciser l'utilisateur de staging pour la recherche des appareils à modifier (override le fichier de paramètres)
|
||||
- **-sn / --serialnumber** : permet de filtrer sur un numéro de série précis
|
||||
- **-f / --force** : permet d'outrepasser la vérification du verrou posé par le script lors de son exécution
|
||||
- **-c / --configPath** : permet de définir un chemin vers un fichier de configuration a utilisé pour l'exécution du script
|
||||
- **-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 :
|
||||
|
||||
**debian/ubuntu**
|
||||
```
|
||||
apt-get install -y python3 python3-pip git
|
||||
```
|
||||
|
||||
**redhat/centos**
|
||||
```
|
||||
yum install -y python3 python3-pip git
|
||||
```
|
||||
|
||||
## 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**
|
||||
```
|
||||
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
|
||||
|
||||
Pour installer les scripts :
|
||||
|
||||
1. Télécharger le contenu de ce projet dans un répertoire du serveur :
|
||||
|
||||
```
|
||||
git clone https://gitlab.forge.education.gouv.fr/jsecula/glpi-airwatch-sync.git
|
||||
```
|
||||
|
||||
2. Modifier le contenu du fichier de configuration global (settings.json) avec les valeurs correspondant à votre infrastructure.
|
||||
Les clés et certificats API se récupèrent aux emplacements suivants :
|
||||
- **Clé API Airwatch**: Groupes et Paramètres > Tous les paramètres > Système > Avancé > Interface de programmation (API) > API REST
|
||||
|
||||
- **Certificat Airwatch**: lors de la création ou la modification d'un utilisateur Admin
|
||||
|
||||
- **Token d'application GLPI**: Configuration > Générale > API
|
||||
|
||||
- **Token utilisateur GLPI**: Administration > Utilisateurs > choisir l'utilisateur > Jeton d'API
|
||||
|
||||
3. Lancement automatique avec systemd :
|
||||
|
||||
Création des services et des timers dans /etc/systemd/system/ :
|
||||
|
||||
**airwatchSync.service**
|
||||
```
|
||||
[Unit]
|
||||
Description=Script Airwatch pour la synchronisation des données Airwatch et GLPI.
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=/opt/airwatchSync
|
||||
ExecStart=/opt/airwatchSync/syncGLPI.py
|
||||
User=root
|
||||
```
|
||||
**airwatchSync.timer** (exécution toutes les heures)
|
||||
```
|
||||
[Unit]
|
||||
Description=Script Airwatch pour la synchronisation des données Airwatch et GLPI.
|
||||
|
||||
[Timer]
|
||||
OnUnitInactiveSec=1h
|
||||
AccuracySec=1us
|
||||
Unit=airwatchSync.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
```
|
||||
|
||||
**airwatchStaging.service**
|
||||
```
|
||||
[Unit]
|
||||
Description=Script qui assigne les appareils en staging aux utilisateurs en se basant sur GLPI.
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=/opt/airwatchSync
|
||||
ExecStart=/opt/airwatchSync/StagingUserAssignation.py
|
||||
User=root
|
||||
```
|
||||
|
||||
**airwatchStaging.timer** (exécution toutes les minutes)
|
||||
```
|
||||
[Unit]
|
||||
Description=Script qui assigne les appareils en staging aux utilisateurs en se basant sur GLPI.
|
||||
|
||||
[Timer]
|
||||
OnUnitInactiveSec=1m
|
||||
AccuracySec=1us
|
||||
Unit=airwatchStaging.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
```
|
||||
|
||||
Rechargement de systemd et activation des timers
|
||||
```
|
||||
systemctl daemon-reload
|
||||
systemctl enable airwatchSync.timer airwatchStaging.timer
|
||||
systemctl start airwatchSync.timer airwatchStaging.timer
|
||||
```
|
||||
|
||||
28
compose.yml
Normal file
28
compose.yml
Normal 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
10
dockerfile
Normal 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
26
pre-start.py
Normal 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}")
|
||||
|
||||
|
||||
301
scripts/StagingUserAssignation.py
Normal file → Executable file
301
scripts/StagingUserAssignation.py
Normal file → Executable file
@ -1,241 +1,158 @@
|
||||
#!/usr/bin/python3
|
||||
import os
|
||||
import base64
|
||||
import requests
|
||||
import json
|
||||
import argparse
|
||||
from cryptography.hazmat.primitives.serialization import pkcs12, pkcs7
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
import logging
|
||||
from functions import getSettings
|
||||
from includes.airwatchAPI import *
|
||||
from includes.GLPIAPI import *
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-debug", action=argparse.BooleanOptionalAction)
|
||||
parser.add_argument("-force", action=argparse.BooleanOptionalAction)
|
||||
parser.add_argument("-sn", "--serialnumber", dest="serialnumber", type=str)
|
||||
parser.add_argument("-u", "--staginguser", dest="staginguser", 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()
|
||||
|
||||
settingsDefault = {
|
||||
"airwatchServer":"https://airwatchServer/",
|
||||
"airwatchAPIKey":"APIKEY",
|
||||
"airwatchAuthMethod":"CMSURL",
|
||||
"airwatchCertPath":"/path/to/cert",
|
||||
"airwatchCertPass":"certPassword",
|
||||
"airwatchAPIUser":"UserAPI",
|
||||
"airwatchAPIPassword":"PasswordUserAPI",
|
||||
"glpiServer":"http://127.0.0.1/glpi/",
|
||||
"glpiAppToken":"GLPIAppToken",
|
||||
"glpiUserToken":"GLPIUserToken",
|
||||
"stagingUser":"staging-pr"
|
||||
}
|
||||
|
||||
settings = None
|
||||
|
||||
if(not os.path.isfile("./settings.json")):
|
||||
f = open("./settings.json", "w")
|
||||
f.write(json.dumps(settingsDefault, indent=4))
|
||||
f.close()
|
||||
exit(1)
|
||||
# Récupération des informations du fichier de configuration
|
||||
if(args.configpath != None and args.configpath != ''):
|
||||
settings = getSettings(args.configpath)
|
||||
else:
|
||||
with open("./settings.json", "r") as f:
|
||||
settings = json.load(f)
|
||||
settings = getSettings(f"{os.path.realpath(os.path.dirname(__file__))}/conf/settings.conf")
|
||||
|
||||
#=========== Configuration des logs ===========#
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if(args.debug or settings["LOGS"]["Debug"]):
|
||||
debug = True
|
||||
logginglevel = logging.DEBUG
|
||||
else:
|
||||
logginglevel = logging.INFO
|
||||
|
||||
logger.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"]):
|
||||
if(settings["LOGS"].get("Path") and settings["LOGS"].get("Path") != ""):
|
||||
fileHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}stagingUserAssignation.log")
|
||||
else:
|
||||
fileHandler = logging.FileHandler(f'{os.path.realpath(os.path.dirname(__file__))}/logs/stagingUserAssignation.log')
|
||||
fileHandler.setLevel(logginglevel)
|
||||
fileHandler.setFormatter(formatter)
|
||||
logger.addHandler(fileHandler)
|
||||
|
||||
# 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
|
||||
lockFile = './airwatchStagingUserAssignation.lock'
|
||||
nameForLockFile = settings["GLPI"]["UserAgent"].replace(' ', '-')
|
||||
lockFile = f'{os.path.realpath(os.path.dirname(__file__))}/{nameForLockFile}_StagingUserAssignation.lock'
|
||||
|
||||
debug=args.debug
|
||||
|
||||
# Informations du serveur Airwatch
|
||||
airwatchServer = settings["airwatchServer"]
|
||||
airwatchAPIKey = settings["airwatchAPIKey"]
|
||||
airwatchAuthMethod = settings["airwatchAuthMethod"]
|
||||
airwatchAPIUser = None
|
||||
airwatchAPIPassword = None
|
||||
airwatchCertPath = None
|
||||
airwatchCertPass = None
|
||||
if(airwatchAuthMethod == 'password'):
|
||||
airwatchAPIUser = settings["airwatchAPIUser"]
|
||||
airwatchAPIPassword = settings["airwatchAPIPassword"]
|
||||
elif(airwatchAuthMethod == 'CMSURL'):
|
||||
airwatchCertPath = settings["airwatchCertPath"]
|
||||
airwatchCertPass = settings["airwatchCertPass"]
|
||||
stagingUser = settings["stagingUser"]
|
||||
stagingUser = settings["AIRWATCH"]["StagingUser"]
|
||||
|
||||
# Informations du serveur GLPI
|
||||
GLPIServer = settings["glpiServer"]
|
||||
GLPIAppToken = settings["glpiAppToken"]
|
||||
GLPIUserToken = settings["glpiUserToken"]
|
||||
if(args.staginguser != None):
|
||||
stagingUser = args.staginguser
|
||||
|
||||
# ====================================== #
|
||||
|
||||
def getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri, User=None, password=None, CertPath=None, CertPassword=None):
|
||||
if(airwatchAuthMethod == "password"):
|
||||
airwatchAPIUserToken = base64.b64encode(f"{airwatchAPIUser}:{airwatchAPIPassword}".encode('ascii')).decode("ascii")
|
||||
|
||||
return {
|
||||
"Authorization": f"Basic {airwatchAPIUserToken}",
|
||||
"aw-tenant-code": airwatchAPIKey,
|
||||
"Accept": "application/json"
|
||||
}
|
||||
else:
|
||||
signing_data = uri.split('?')[0]
|
||||
with open(CertPath, 'rb') as certfile:
|
||||
cert = certfile.read()
|
||||
key, certificate, additional_certs = pkcs12.load_key_and_certificates(cert, CertPassword.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": airwatchAPIKey,
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
|
||||
# Vérification de la présence du verrou avant de continuer
|
||||
if(os.path.isfile(lockFile) and not args.force):
|
||||
if(debug):
|
||||
print('Lock file is present, exiting...')
|
||||
logger.debug('Lock file exists, exiting...')
|
||||
exit(0)
|
||||
else:
|
||||
open(lockFile, "w").close()
|
||||
|
||||
# Adresse de recherche des appareils filtré sur l'utilisateur de staging
|
||||
# avec limite de 500 appareils par page (limite max de l'API)
|
||||
airwatchAPIDevicesSearchURI = f"/API/mdm/devices/search?user={stagingUser}&pagesize=500&page="
|
||||
|
||||
airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIDevicesSearchURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass)
|
||||
|
||||
# Page de départ pour la recherche
|
||||
pageNumber = 0
|
||||
|
||||
# Initialisation de la variable devices qui va stocker l'ensemble des appareils trouvés
|
||||
devices = []
|
||||
|
||||
uri = f"{airwatchServer}{airwatchAPIDevicesSearchURI}{pageNumber}"
|
||||
if(debug):
|
||||
print(f"Uri for device search on airwatch : {uri}")
|
||||
result = requests.get(uri, headers=airwatchHeaders)
|
||||
|
||||
if(debug):
|
||||
print(f"Result of request : {result}")
|
||||
|
||||
# On vérifie qu'on a bien un retour OK pour la requête API
|
||||
if(result.status_code != 200):
|
||||
# Suppression du verrou
|
||||
# Initialisation de l'api Airwatch
|
||||
try:
|
||||
airwatch = AirwatchAPI(settings)
|
||||
# Recherche des appareils filtré sur l'utilisateur de staging
|
||||
devices = airwatch.GetDevices(stagingUser)
|
||||
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(0)
|
||||
|
||||
result = result.json()
|
||||
|
||||
# On fait une requête pour chaque page en fonction tant qu'on a pas atteint le nombre
|
||||
# d'appareils trouvé dans la première requête
|
||||
while(len(devices) != result["Total"]):
|
||||
uri = f"{airwatchServer}{airwatchAPIDevicesSearchURI}{pageNumber}"
|
||||
result = requests.get(uri, headers=airwatchHeaders).json()
|
||||
devices += result["Devices"]
|
||||
pageNumber += 1
|
||||
|
||||
|
||||
# Adresse d'initalisation de l'api GLPI
|
||||
GLPIAPIInitUri = '/apirest.php/initSession/'
|
||||
|
||||
GLPIHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
"Authorization": f"user_token {GLPIUserToken}",
|
||||
"App-Token": GLPIAppToken
|
||||
}
|
||||
|
||||
# Récupération d'un token de session
|
||||
uri = f"{GLPIServer}{GLPIAPIInitUri}"
|
||||
result = requests.get(uri, headers=GLPIHeaders)
|
||||
|
||||
if(debug):
|
||||
print(f"GLPI api access : {result}")
|
||||
|
||||
if(result.status_code != 200):
|
||||
# Suppression du verrou
|
||||
exit(1)
|
||||
except Exception as error:
|
||||
logger.critical(f"Connection to Airwatch server failed : {error}")
|
||||
os.remove(lockFile)
|
||||
exit(1)
|
||||
|
||||
GLPISessionToken = result.json()["session_token"]
|
||||
if(devices == None):
|
||||
logger.info(f"No device found with staging user ({stagingUser}), exiting...")
|
||||
os.remove(lockFile)
|
||||
exit(0)
|
||||
else:
|
||||
logger.info(f"{len(devices)} devices found with staging user ({stagingUser})")
|
||||
|
||||
if(debug):
|
||||
print(f"GLPI session Token: {GLPISessionToken}")
|
||||
# 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)
|
||||
|
||||
# Changement des headers pour remplacer l'user token par le token de session
|
||||
GLPIHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
"Session-Token": GLPISessionToken,
|
||||
"App-Token": GLPIAppToken
|
||||
}
|
||||
|
||||
# Adresse de recherche des appareils présents dans ordinateurs sur GLPI
|
||||
GLPIAPISearchComputer = 'apirest.php/search/computer?'
|
||||
|
||||
for device in devices:
|
||||
if(device["EnrollmentStatus"] != 'Enrolled'):
|
||||
if(device.EnrollmentStatus != 'Enrolled'):
|
||||
logger.error(f"Device with id {device.Id} not enrolled, should it be deleted ?")
|
||||
continue
|
||||
|
||||
if(device["Imei"] != ''):
|
||||
if(debug):
|
||||
print(f"Imei = {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"]}$'
|
||||
if(args.serialnumber != None and device.SerialNumber != args.serialnumber):
|
||||
continue
|
||||
|
||||
if(debug):
|
||||
print(f"Serial Number = {device['SerialNumber']}")
|
||||
logger.debug(f"Serial Number = {device.SerialNumber}")
|
||||
|
||||
searchUri = f"{GLPIServer}{GLPIAPISearchComputer}{search_parameter}"
|
||||
if(debug):
|
||||
print(f"searchURI = {searchUri}")
|
||||
search = requests.get(searchUri, headers=GLPIHeaders)
|
||||
deviceID, data, deviceCount = glpiapi.GetDevice(device)
|
||||
|
||||
|
||||
# On ne gère pas pour l'instant d'autres code que le code 200
|
||||
# voir en fonction des codes erreurs retournés par le serveur
|
||||
if(search.status_code != 200):
|
||||
break
|
||||
|
||||
search = search.json()
|
||||
|
||||
if(search["totalcount"] == 1):
|
||||
if(deviceCount == 1):
|
||||
# Récupération de l'utilisateur de l'appareil dans la fiche GLPI de l'appareil
|
||||
for device_id, data in search["data"].items():
|
||||
device_user = search["data"][device_id]["70"]
|
||||
if(debug):
|
||||
print(f"user on device in GLPI : {device_user}")
|
||||
|
||||
device_user = data["70"]
|
||||
|
||||
logger.info(f"Found device {device.Id} in GLPI with id = {deviceID}")
|
||||
|
||||
logger.debug(f"user on device in GLPI : {device_user}")
|
||||
|
||||
# Vérification que l'appareil est associé à un utilisateur dans GLPI
|
||||
if(device_user != None):
|
||||
# Récupération de l'utilisateur sur Magenta
|
||||
cmdURI = f'/API/system/users/search?username={device_user}'
|
||||
airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=cmdURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass)
|
||||
uri = f"{airwatchServer}{cmdURI}"
|
||||
if(debug):
|
||||
print(f"Airwatch user search uri : {uri}")
|
||||
user = requests.get(uri, headers=airwatchHeaders)
|
||||
# Récupération de l'utilisateur sur Airwatch
|
||||
airwatchUser = airwatch.GetUser(device_user)
|
||||
|
||||
# On ne gère pas pour l'instant d'autres code que le code 200
|
||||
# voir en fonction des codes erreurs retournés par le serveur
|
||||
if(user.status_code != 200):
|
||||
break
|
||||
if(airwatchUser == None):
|
||||
logger.error(f"User {device_user} not found in Airwatch")
|
||||
continue
|
||||
logger.info(f"Assigning device with id {device.Id} to user {device_user} (id={airwatchUser.Id}) in Airwatch")
|
||||
result = airwatch.SetDeviceUser(device, airwatchUser)
|
||||
else:
|
||||
logger.warning(f"Device with id {device.Id} is not assigned to any user in GLPI, skipping the device")
|
||||
elif(deviceCount > 1):
|
||||
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:
|
||||
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.")
|
||||
|
||||
user = user.json()
|
||||
# Changement de l'utilisateur assigné sur l'appareil dans Magenta
|
||||
cmdURI = f'/API/mdm/devices/{device["Id"]["Value"]}/enrollmentuser/{user["Users"][0]["Id"]["Value"]}'
|
||||
patchUri = f'{airwatchServer}{cmdURI}'
|
||||
airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=cmdURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass)
|
||||
if(debug):
|
||||
print(f"patchUri = {patchUri}")
|
||||
requests.patch(patchUri, headers=airwatchHeaders)
|
||||
|
||||
# Suppression du verrou
|
||||
os.remove(lockFile)
|
||||
|
||||
0
scripts/certs/empty
Normal file
0
scripts/certs/empty
Normal file
30
scripts/conf/settings.conf.dist
Normal file
30
scripts/conf/settings.conf.dist
Normal file
@ -0,0 +1,30 @@
|
||||
[AIRWATCH]
|
||||
Server = "https://airwatchServer"
|
||||
APIKey = "APIKEY"
|
||||
|
||||
# M<>thode d'authentification (CMSURL or PASSWORD)
|
||||
# CMSURL permet l'authentification avec un certificat utilisateur
|
||||
# PASSWORD permet l'authentification avec un nom d'utilisateur et un mot de passe (APIUser, APIPassword)
|
||||
AuthenticationMethod = "CMSURL"
|
||||
CertificatePath = "/path/to/cert"
|
||||
CertificatePassword = "12345"
|
||||
APIUser = "UserAPI"
|
||||
APIPassword = "PasswordUserAPI"
|
||||
|
||||
# Utilisateur de staging que l'on va remplacer par l'utilisateur trouv<75> dans GLPI
|
||||
StagingUser = "staging-pr"
|
||||
|
||||
[GLPI]
|
||||
Server = "http://127.0.0.1/glpi"
|
||||
AppToken = "GLPIAppToken"
|
||||
UserToken = "GLPIUserToken"
|
||||
|
||||
# User agent qui sera visible sur GLPI lors de la synchronisation
|
||||
UserAgent = "Airwatch Synchronizer"
|
||||
|
||||
[LOGS]
|
||||
Enabled = true
|
||||
# Chemin o<> seront cr<63><72> les fichiers de log
|
||||
Path = "./logs/"
|
||||
# Mode debug pour avoir plus d'informations
|
||||
Debug = false
|
||||
46
scripts/functions.py
Executable file
46
scripts/functions.py
Executable file
@ -0,0 +1,46 @@
|
||||
import os
|
||||
import toml
|
||||
|
||||
def getSettings(settingsPath):
|
||||
settingsDefault ="""
|
||||
[AIRWATCH]
|
||||
Server = "https://airwatchServer"
|
||||
APIKey = "APIKEY"
|
||||
|
||||
# Méthode d'authentification (CMSURL or PASSWORD)
|
||||
# CMSURL permet l'authentification avec un certificat utilisateur
|
||||
# PASSWORD permet l'authentification avec un nom d'utilisateur et un mot de passe (APIUser, APIPassword)
|
||||
AuthenticationMethod = "CMSURL"
|
||||
CertificatePath = "/path/to/cert"
|
||||
CertificatePassword = "12345"
|
||||
APIUser = "UserAPI"
|
||||
APIPassword = "PasswordUserAPI"
|
||||
|
||||
# Utilisateur de staging que l'on va remplacer par l'utilisateur trouvé dans GLPI
|
||||
StagingUser = "staging-pr"
|
||||
|
||||
[GLPI]
|
||||
Server = "http://127.0.0.1/glpi"
|
||||
AppToken = "GLPIAppToken"
|
||||
UserToken = "GLPIUserToken"
|
||||
|
||||
# User agent qui sera visible sur GLPI lors de la synchronisation
|
||||
UserAgent = "Airwatch Synchronizer"
|
||||
|
||||
[LOGS]
|
||||
Enabled = true
|
||||
# Chemin où seront créé les fichiers de log
|
||||
Path = "./logs/"
|
||||
# Mode debug pour avoir plus d'informations
|
||||
Debug = false
|
||||
"""
|
||||
|
||||
settings = None
|
||||
|
||||
if(not os.path.isfile(settingsPath)):
|
||||
f = open(settingsPath, "w")
|
||||
f.write(settingsDefault)
|
||||
f.close()
|
||||
with open(settingsPath, "r") as f:
|
||||
settings = toml.load(f)
|
||||
return settings
|
||||
243
scripts/includes/GLPIAPI.py
Executable file
243
scripts/includes/GLPIAPI.py
Executable 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)
|
||||
0
scripts/includes/__init__.py
Executable file
0
scripts/includes/__init__.py
Executable file
212
scripts/includes/airwatchAPI.py
Executable file
212
scripts/includes/airwatchAPI.py
Executable 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"]
|
||||
0
scripts/logs/empty
Normal file
0
scripts/logs/empty
Normal file
3
scripts/requirements.txt
Normal file
3
scripts/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
cryptography
|
||||
requests
|
||||
toml
|
||||
@ -1,13 +0,0 @@
|
||||
{
|
||||
"airwatchServer": "https://airwatchServer/",
|
||||
"airwatchAPIKey": "APIKEY",
|
||||
"airwatchAuthMethod": "CMSURL",
|
||||
"airwatchCertPath": "/path/to/cert",
|
||||
"airwatchCertPass": "certPassword",
|
||||
"airwatchAPIUser": "UserAPI",
|
||||
"airwatchAPIPassword": "PasswordUserAPI",
|
||||
"glpiServer": "http://127.0.0.1/glpi/",
|
||||
"glpiAppToken": "GLPIAppToken",
|
||||
"glpiUserToken": "GLPIUserToken",
|
||||
"stagingUser": "staging-pr"
|
||||
}
|
||||
483
scripts/syncGLPI.py
Normal file → Executable file
483
scripts/syncGLPI.py
Normal file → Executable file
@ -1,77 +1,103 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
import base64
|
||||
import requests
|
||||
import json
|
||||
import argparse
|
||||
from cryptography.hazmat.primitives.serialization import pkcs12, pkcs7
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from functions import getSettings
|
||||
from includes.airwatchAPI import *
|
||||
from includes.GLPIAPI import *
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-debug", action=argparse.BooleanOptionalAction)
|
||||
parser.add_argument("-searchFilter", type=str, choices=["Id", "SerialNumber", "Imei", "UserName"])
|
||||
parser.add_argument("-searchValue", type=str)
|
||||
parser.add_argument("-force", action=argparse.BooleanOptionalAction)
|
||||
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()
|
||||
|
||||
settingsDefault = {
|
||||
"airwatchServer":"https://airwatchServer/",
|
||||
"airwatchAPIKey":"APIKEY",
|
||||
"airwatchAuthMethod":"CMSURL",
|
||||
"airwatchCertPath":"/path/to/cert",
|
||||
"airwatchCertPass":"certPassword",
|
||||
"airwatchAPIUser":"UserAPI",
|
||||
"airwatchAPIPassword":"PasswordUserAPI",
|
||||
"glpiServer":"http://127.0.0.1/glpi/",
|
||||
"glpiAppToken":"GLPIAppToken",
|
||||
"glpiUserToken":"GLPIUserToken",
|
||||
"stagingUser":"staging-pr"
|
||||
}
|
||||
|
||||
settings = None
|
||||
|
||||
if(not os.path.isfile("./settings.json")):
|
||||
f = open("./settings.json", "w")
|
||||
f.write(json.dumps(settingsDefault, indent=4))
|
||||
f.close()
|
||||
exit(1)
|
||||
# Récupération des informations du fichier de configuration
|
||||
if(args.configpath != None and args.configpath != ''):
|
||||
settings = getSettings(args.configpath)
|
||||
else:
|
||||
with open("./settings.json", "r") as f:
|
||||
settings = json.load(f)
|
||||
settings = getSettings("{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
|
||||
lockFile = './airwatchSyncGLPI.lock'
|
||||
nameForLockFile = settings["GLPI"]["UserAgent"].replace(' ', '-')
|
||||
lockFile = f'{os.path.realpath(os.path.dirname(__file__))}/{nameForLockFile}_SyncGLPI.lock'
|
||||
|
||||
debug=args.debug
|
||||
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']}")
|
||||
|
||||
# Informations du serveur Airwatch
|
||||
airwatchServer = settings["airwatchServer"]
|
||||
airwatchAPIKey = settings["airwatchAPIKey"]
|
||||
airwatchAuthMethod = settings["airwatchAuthMethod"]
|
||||
airwatchAPIUser = None
|
||||
airwatchAPIPassword = None
|
||||
airwatchCertPath = None
|
||||
airwatchCertPass = None
|
||||
if(airwatchAuthMethod == 'password'):
|
||||
airwatchAPIUser = settings["airwatchAPIUser"]
|
||||
airwatchAPIPassword = settings["airwatchAPIPassword"]
|
||||
else:
|
||||
airwatchCertPath = settings["airwatchCertPath"]
|
||||
airwatchCertPass = settings["airwatchCertPass"]
|
||||
|
||||
# Informations du serveur GLPI
|
||||
GLPIServer = settings["glpiServer"]
|
||||
GLPIAppToken = settings["glpiAppToken"]
|
||||
GLPIUserToken = settings["glpiUserToken"]
|
||||
|
||||
# Filtres
|
||||
searchFilter = args.searchFilter
|
||||
searchValue = args.searchValue
|
||||
searchFilter = args.searchfilter
|
||||
searchValue = args.searchvalue
|
||||
|
||||
# Platform exclusion (12 = computer)
|
||||
platformFilterEnabled = True
|
||||
@ -79,100 +105,56 @@ platformFilterOut = [12]
|
||||
|
||||
# ====================================== #
|
||||
|
||||
def getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri, User=None, password=None, CertPath=None, CertPassword=None):
|
||||
if(airwatchAuthMethod == "password"):
|
||||
airwatchAPIUserToken = base64.b64encode(f"{airwatchAPIUser}:{airwatchAPIPassword}".encode('ascii')).decode("ascii")
|
||||
|
||||
return {
|
||||
"Authorization": f"Basic {airwatchAPIUserToken}",
|
||||
"aw-tenant-code": airwatchAPIKey,
|
||||
"Accept": "application/json"
|
||||
}
|
||||
else:
|
||||
signing_data = uri.split('?')[0]
|
||||
with open(CertPath, 'rb') as certfile:
|
||||
cert = certfile.read()
|
||||
key, certificate, additional_certs = pkcs12.load_key_and_certificates(cert, CertPassword.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": airwatchAPIKey,
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
|
||||
# Vérification de la présence du verrou avant de continuer
|
||||
if(os.path.isfile(lockFile) and not args.force):
|
||||
if(debug):
|
||||
print('Lock file is present, exiting...')
|
||||
logger.debug('Lock file exists, exiting...')
|
||||
exit(0)
|
||||
else:
|
||||
open(lockFile, "w").close()
|
||||
|
||||
logger.info("========= Synchronization started =========")
|
||||
|
||||
# Adresse de recherche des appareils
|
||||
# avec limite de 500 appareils par page (limite max de l'API)
|
||||
airwatchAPIDevicesSearchURI = f"/API/mdm/devices/search?pagesize=500&page="
|
||||
airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIDevicesSearchURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass)
|
||||
|
||||
# Page de départ pour la recherche
|
||||
pageNumber = 0
|
||||
|
||||
# Initialisation de la variable devices qui va stocker l'ensemble des appareils trouvés
|
||||
devices = []
|
||||
|
||||
uri = f"{airwatchServer}{airwatchAPIDevicesSearchURI}{pageNumber}"
|
||||
|
||||
if(debug):
|
||||
print(f"Uri for device search on airwatch : {uri}")
|
||||
result = requests.get(uri, headers=airwatchHeaders)
|
||||
|
||||
if(debug):
|
||||
print(f"Result of request : {result}")
|
||||
|
||||
|
||||
# On vérifie qu'on a bien un retour OK pour la requête API
|
||||
if(result.status_code != 200):
|
||||
print(result.json())
|
||||
# Suppression du verrou
|
||||
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(0)
|
||||
exit(1)
|
||||
except Exception as error:
|
||||
logger.critical(f"Connection to Airwatch server failed : {error}")
|
||||
os.remove(lockFile)
|
||||
exit(1)
|
||||
|
||||
result = result.json()
|
||||
# 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)
|
||||
|
||||
# On fait une requête pour chaque page tant qu'on a pas atteint le nombre
|
||||
# d'appareils trouvé dans la première requête
|
||||
while(len(devices) != result["Total"]):
|
||||
uri = f"{airwatchServer}{airwatchAPIDevicesSearchURI}{pageNumber}"
|
||||
result = requests.get(uri, headers=airwatchHeaders).json()
|
||||
devices += result["Devices"]
|
||||
pageNumber += 1
|
||||
|
||||
|
||||
if(debug):
|
||||
print(f"Nombre d'appareils {len(devices)}")
|
||||
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]
|
||||
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 id et de la dernière date d'enrolement
|
||||
# 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["Id"]["Value"],datetime.strptime(device["LastEnrolledOn"], "%Y-%m-%dT%H:%M:%S.%f")] for device in devices if serial == device["SerialNumber"]]
|
||||
devicesDouble[serial] = [[device,datetime.strptime(device.LastEnrolledOn, "%Y-%m-%dT%H:%M:%S.%f")] for device in devices if serial == device.SerialNumber]
|
||||
|
||||
|
||||
if(debug):
|
||||
print(f"Doublons détectés: {len(devicesDouble)}")
|
||||
logger.info(f"Duplicates detected : {len(devicesDouble)}")
|
||||
|
||||
# On supprime les doublons qui ne se sont pas enrôlés en dernier
|
||||
devicesToDelete = []
|
||||
@ -188,220 +170,77 @@ for k,v in devicesDouble.items():
|
||||
else:
|
||||
devicesToDelete += [d[0]]
|
||||
|
||||
# On retire ces appareils de la liste
|
||||
devices = [d for d in devices if d["Id"]["Value"] not in devicesToDelete]
|
||||
|
||||
# envoi de la requête de suppression des appareils sur magenta
|
||||
airwatchAPIDeleteURI = '/API/mdm/devices/'
|
||||
airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIDeleteURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass)
|
||||
# envoi de la requête de suppression des appareils sur airwatch
|
||||
for device in devicesToDelete:
|
||||
uri = f"{airwatchServer}{airwatchAPIDeleteURI}{device}"
|
||||
if(debug):
|
||||
print(f"Suppression de {uri}")
|
||||
requests.delete(uri, headers=airwatchHeaders)
|
||||
logger.info(f"Deleting {device.Id} - {device.FriendlyName} in Airwatch")
|
||||
airwatch.DeleteDevice(device)
|
||||
|
||||
devices = airwatch.GetDevices()
|
||||
|
||||
# ====================== Fin suppression des doublons ================================= #
|
||||
|
||||
if(searchFilter != None):
|
||||
if(debug):
|
||||
print(f"SearchFilter set to {searchFilter}")
|
||||
print(f"SearchValue set to {searchValue}")
|
||||
logger.debug(f"SearchFilter set to {searchFilter}")
|
||||
logger.debug(f"SearchValue set to {searchValue}")
|
||||
if(searchFilter == 'Id'):
|
||||
devices = [device for device in devices if device["Id"]["Value"] == searchValue]
|
||||
devices = [device for device in devices if getattr(device, "Id") == searchValue]
|
||||
else:
|
||||
devices = [device for device in devices if device[searchFilter] == searchValue]
|
||||
|
||||
# Adresse d'initalisation de l'api GLPI
|
||||
GLPIAPIInitUri = '/apirest.php/initSession/'
|
||||
|
||||
GLPIHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
"Authorization": f"user_token {GLPIUserToken}",
|
||||
"App-Token": GLPIAppToken
|
||||
}
|
||||
|
||||
# Récupération d'un token de session
|
||||
uri = f"{GLPIServer}{GLPIAPIInitUri}"
|
||||
result = requests.get(uri, headers=GLPIHeaders)
|
||||
|
||||
if(debug):
|
||||
print(f"GLPI api access : {result}")
|
||||
|
||||
if(result.status_code != 200):
|
||||
# Suppression du verrou
|
||||
os.remove(lockFile)
|
||||
exit(1)
|
||||
|
||||
GLPISessionToken = result.json()["session_token"]
|
||||
|
||||
if(debug):
|
||||
print(f"GLPI session Token: {GLPISessionToken}")
|
||||
|
||||
# Changement des headers pour remplacer l'user token par le token de session
|
||||
GLPIHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
"Session-Token": GLPISessionToken,
|
||||
"App-Token": GLPIAppToken
|
||||
}
|
||||
|
||||
# Adresse de recherche des appareils présents dans ordinateurs sur GLPI
|
||||
GLPIAPISearchComputer = 'apirest.php/search/computer?'
|
||||
|
||||
platforms = {
|
||||
2:"Apple iOS",
|
||||
5:"Android",
|
||||
12:"Windows Desktop"
|
||||
}
|
||||
|
||||
processorArchs = {
|
||||
0:{
|
||||
"osArch":"arm64",
|
||||
"softwareArch":"arm64"
|
||||
},
|
||||
9:{
|
||||
"osArch":"64-bit",
|
||||
"softwareArch":"x86_64"
|
||||
}
|
||||
}
|
||||
devices = [device for device in devices if getattr(device, "searchFilter") == searchValue]
|
||||
|
||||
for device in devices:
|
||||
if(device["EnrollmentStatus"] != 'Enrolled'):
|
||||
if(device.EnrollmentStatus != 'Enrolled'):
|
||||
logger.warning(f"Device with Airwatch id {device.Id} not enrolled, skipping this device...")
|
||||
continue
|
||||
|
||||
if(device["Imei"] != ''):
|
||||
if(debug):
|
||||
print(f"Imei = {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"]}$'
|
||||
if(device.SerialNumber == 'HUBNOSERIAL'):
|
||||
logger.info(f"Device with Airwatch id {device.Id} is using work profile, skipping...")
|
||||
continue
|
||||
|
||||
if(debug):
|
||||
print(f"Serial Number = {device['SerialNumber']}")
|
||||
logger.info(f"Searching device {device.FriendlyName} (Airwatch id={device.Id}) on GLPI")
|
||||
|
||||
searchUri = f"{GLPIServer}{GLPIAPISearchComputer}{search_parameter}"
|
||||
if(debug):
|
||||
print(f"searchURI = {searchUri}")
|
||||
search = requests.get(searchUri, headers=GLPIHeaders)
|
||||
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"]
|
||||
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)
|
||||
|
||||
# On ne gère pas pour l'instant d'autres code que le code 200
|
||||
# voir en fonction des codes erreurs retournés par le serveur
|
||||
if(search.status_code != 200):
|
||||
break
|
||||
# filtre des plateformes
|
||||
if(platformFilterEnabled):
|
||||
if device.PlatformId in platformFilterOut:
|
||||
logger.info(f"Device platform ({device.PlatformId}) is filtered out, not updating GLPI")
|
||||
continue
|
||||
|
||||
search = search.json()
|
||||
logger.info(f"Updating {deviceID} on GLPI")
|
||||
glpiapi.UpdateInventory(inventory.Json())
|
||||
|
||||
if(search["totalcount"] == 1):
|
||||
# Récupération de l'utilisateur de l'appareil dans la fiche GLPI de l'appareil
|
||||
for device_id, data in search["data"].items():
|
||||
platformId = device["PlatformId"]["Id"]["Value"]
|
||||
if(platformId in platforms.keys()):
|
||||
platformName = platforms[platformId]
|
||||
else:
|
||||
platformName = "Unknown"
|
||||
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)
|
||||
|
||||
processorArch = device["ProcessorArchitecture"]
|
||||
if(processorArch in processorArchs.keys()):
|
||||
osArch = processorArchs[processorArch]["osArch"]
|
||||
softwareArch = processorArchs[processorArch]["softwareArch"]
|
||||
else:
|
||||
osArch = "Unknown"
|
||||
softwareArch = "Unknown"
|
||||
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)
|
||||
|
||||
inventory = {
|
||||
"action":"inventory",
|
||||
"content":{
|
||||
"accesslog":{
|
||||
"logdate": datetime.strptime(device["LastSeen"], "%Y-%m-%dT%H:%M:%S.%f").strftime("%Y-%m-%d %H:%M:%S")
|
||||
},
|
||||
"versionclient":"Airwatch Synchronizer",
|
||||
"users":[
|
||||
{
|
||||
"login": device["UserName"]
|
||||
}
|
||||
],
|
||||
"operatingsystem":{
|
||||
"name": platformName,
|
||||
"version": device["OperatingSystem"],
|
||||
"full_name": f"{platformName} {device['OperatingSystem']}",
|
||||
"arch": osArch
|
||||
},
|
||||
"softwares":[
|
||||
logger.info("========= End of synchronization =========")
|
||||
|
||||
],
|
||||
"hardware":{
|
||||
"name":data["1"],
|
||||
"uuid":device["Uuid"],
|
||||
"memory":device["TotalPhysicalMemory"]
|
||||
}
|
||||
},
|
||||
"tag":device["LocationGroupName"],
|
||||
"deviceid":f"{data['1']} - {device['SerialNumber']}",
|
||||
"itemtype":"Computer"
|
||||
}
|
||||
# Récupération des applications présents sur les appareils
|
||||
airwatchAPIAppsSearchURI = f"/api/mdm/devices/{device['Uuid']}/apps/search"
|
||||
airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIAppsSearchURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass)
|
||||
uri = f"{airwatchServer}{airwatchAPIAppsSearchURI}"
|
||||
apps = requests.get(uri, headers=airwatchHeaders).json()
|
||||
|
||||
for app in apps["app_items"]:
|
||||
if(app["installed_status"] != "Installed"):
|
||||
continue
|
||||
|
||||
install_date = datetime.strptime(app["latest_uem_action_time"], "%Y-%m-%dT%H:%M:%S.%f").strftime("%Y-%m-%d")
|
||||
if(install_date == "1-01-01"):
|
||||
inventory["content"]["softwares"] += [{
|
||||
"name": app["name"],
|
||||
"guid": app["bundle_id"],
|
||||
"version": app["installed_version"],
|
||||
"filesize": app["size"],
|
||||
"arch": softwareArch
|
||||
}]
|
||||
else:
|
||||
inventory["content"]["softwares"] += [{
|
||||
"name": app["name"],
|
||||
"guid": app["bundle_id"],
|
||||
"version": app["installed_version"],
|
||||
"install_date": install_date,
|
||||
"filesize": app["size"],
|
||||
"arch": softwareArch
|
||||
}]
|
||||
|
||||
|
||||
|
||||
# Mise à jour du friendly name sur Airwatch
|
||||
if(device["DeviceFriendlyName"] != f"{data['1']} {platformName} {device['OperatingSystem']} - {device['UserName']}"):
|
||||
airwatchAPIURI = f"/API/mdm/devices/{device['Id']['Value']}"
|
||||
airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass)
|
||||
uri = f"{airwatchServer}{airwatchAPIURI}"
|
||||
updateDeviceDetails = {
|
||||
"DeviceFriendlyName":f"{data['1']} {platformName} {device['OperatingSystem']} - {device['UserName']}"
|
||||
}
|
||||
requests.put(uri, headers=airwatchHeaders, json=updateDeviceDetails)
|
||||
|
||||
headers = {
|
||||
"Content-Type":"Application/x-compress",
|
||||
"user-agent":"Airwatch Synchronizer"
|
||||
}
|
||||
if(debug):
|
||||
print(f"Updating {device_id} on GLPI")
|
||||
|
||||
# filtre des plateformes
|
||||
if(platformFilterEnabled):
|
||||
if device["PlatformId"]["Id"]["Value"] in platformFilterOut:
|
||||
continue
|
||||
result = requests.post(GLPIServer, headers=headers, json=inventory)
|
||||
|
||||
if(debug):
|
||||
print(result.json())
|
||||
|
||||
if(debug):
|
||||
print('Removing lock')
|
||||
logger.debug('Removing lock file')
|
||||
os.remove(lockFile)
|
||||
39
start.sh
Normal file
39
start.sh
Normal 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
|
||||
Reference in New Issue
Block a user