2024-05-23 00:08:46 +02:00
import json , datetime , fcntl , glob , os , re , socket , subprocess , time , requests
2021-09-18 17:38:52 +02:00
2022-03-19 22:01:58 +01:00
from flask import jsonify , request , abort
2021-09-18 17:38:52 +02:00
from flask import Blueprint
from app import email
2024-05-23 00:08:46 +02:00
from dotenv import load_dotenv
load_dotenv ( )
2021-09-18 17:38:52 +02:00
bp = Blueprint ( " routes " , __name__ )
# --------------------------------------------------------------
DIR = " /opt/installmgr/ "
REPOSIT_DIR = " /opt/installmgr/incoming_dumps "
2024-05-26 22:54:24 +02:00
MAX_REPOSIT_SIZE = 2000 * 20 * 1024 # kB (here, max 2000 dumps of 20MB)
2021-09-18 17:38:52 +02:00
2022-03-14 13:10:34 +01:00
ALERT_MAIL_FROM = " root@scodoc.org "
2024-05-26 22:54:24 +02:00
ALERT_MAIL_TO = " viennet "
2021-09-18 17:38:52 +02:00
LOG_FILENAME = os . path . join ( DIR , " upload-dump-errors.log " )
UPLOAD_LOG_FILENAME = os . path . join ( DIR , " upload-dump-log.json " )
DEBUG = False # if false, don't publish error messages
2022-03-19 14:58:40 +01:00
# Les paquets publiés:
2024-05-26 22:54:24 +02:00
#DEBIAN_PACKAGES_EXP = "/srv/packages/pool/main/s/scodoc9/scodoc9_*.deb"
DEBIAN_PACKAGES_EXP = " /srv/bookworm/pool/main/s/scodoc9/scodoc9_*.deb "
2022-03-22 09:01:29 +01:00
RELEASE_LOG_FILE = " /home/viennet/scodoc-releases.log "
2024-05-28 10:13:32 +02:00
GITEA_URL = " https://git.scodoc.org/ "
2024-05-23 00:08:46 +02:00
GITEA_REPO = " ScoDoc/ScoDoc "
GITEA_LABEL_ID = None
2022-03-22 09:01:29 +01:00
@bp.route ( " /scodoc-installmgr/check_version/<client_version> " )
def check_version ( client_version : str ) :
""" check version vs release
return json
{
" status " : " ok " ,
" client_version " : " 9.1.81 " , / / la version du client
" client_version_date " : int / / timestamp version
" last_version " : " 9.1.82 " / / derniere version dispo
" last_version_date " : int / / timestamp derniere version
}
Le client devrait se plaindre de manque de mise à jour si
( client_version != last_version ) et ( now - last_version_date ) > 1 jour
Si la version du client n ' est pas trouvée dans RELEASE_LOG_FILE
renvoie client_version et client_version_date nulls .
"""
try :
with open ( RELEASE_LOG_FILE ) as f :
data = [ l . strip ( ) . split ( ) for l in f ]
except FileNotFoundError :
return jsonify ( { " status " : " error 1 " } )
if len ( data ) < 1 :
return jsonify ( { " status " : " error 2 " } )
release_date = { k : v for v , k in data } # version : date
last_version = data [ - 1 ] [ 1 ]
last_version_date_str = data [ - 1 ] [ 0 ]
try :
dt = datetime . datetime . strptime ( last_version_date_str , " % Y- % m- %d T % H: % M: % S % z " )
except ValueError :
return jsonify ( { " status " : " error 3 " } )
last_version_date = dt . timestamp ( )
#
client_version_date_str = release_date . get ( client_version , None )
if client_version_date_str is None :
client_version = None # not found ? (client altered version ?)
client_version_date = None
else :
try :
dt = datetime . datetime . strptime (
client_version_date_str , " % Y- % m- %d T % H: % M: % S % z "
)
client_version_date = dt . timestamp ( )
except ValueError :
return jsonify ( { " status " : " error 4 " } )
return jsonify (
{
" status " : " ok " ,
" client_version " : client_version ,
" client_version_date " : client_version_date ,
" last_version " : last_version ,
" last_version_date " : last_version_date ,
}
)
2022-03-19 14:58:40 +01:00
2021-09-18 17:38:52 +02:00
2021-09-18 21:19:01 +02:00
@bp.route ( " /scodoc-installmgr/last_stable_version " )
2021-09-18 17:38:52 +02:00
def last_stable_version ( ) :
2022-03-19 14:58:40 +01:00
""" version du dernier paquet ScoDoc 9 publié.
2022-03-19 22:01:58 +01:00
= > json
2022-03-19 14:58:40 +01:00
"""
2021-09-18 17:38:52 +02:00
# LAST_RELEASE_TAG=$(curl "$GITEA_RELEASE_URL" | jq ".[].tag_name" | tr -d -c "0-9.\n" | sort --version-sort | tail -1)
2022-03-19 14:58:40 +01:00
debs = glob . glob ( DEBIAN_PACKAGES_EXP )
2022-03-22 09:01:29 +01:00
version_tuples = [ ] # (9,1,81)
2022-03-19 14:58:40 +01:00
for filename in debs :
m = re . match ( r " .*scodoc9_9 \ .([0-9] { 1,2}) \ .([0-9] { 1,3})-1_amd64.deb " , filename )
if m :
2022-03-22 09:01:29 +01:00
version_tuples . append ( ( 9 , int ( m . group ( 1 ) ) , int ( m . group ( 2 ) ) , filename ) )
2022-03-19 14:58:40 +01:00
if len ( version_tuples ) == 0 :
return " ?.?.? "
version_tuples . sort ( )
2022-03-19 22:01:58 +01:00
last_package_version = version_tuples [ - 1 ] [ : - 1 ]
last_package_filename = version_tuples [ - 1 ] [ - 1 ]
package_mtime = os . path . getmtime ( last_package_filename )
package_version_string = " . " . join ( [ str ( x ) for x in last_package_version ] )
2022-03-22 09:01:29 +01:00
return jsonify (
{
" publication_time " : package_mtime , # float, time
" version " : package_version_string ,
2022-03-19 22:01:58 +01:00
}
)
2021-09-18 17:38:52 +02:00
2024-05-23 00:08:46 +02:00
@bp.route ( " /scodoc-installmgr/report " , methods = [ " POST " ] )
def report ( ) :
""" Création d ' un ticket Gitea depuis ScoDoc
= > json
"""
log = open ( LOG_FILENAME , " a " )
log . write ( " report \n " )
now = datetime . datetime . now ( )
fulltime = now . isoformat ( )
remote_addr = request . environ . get ( " HTTP_X_REAL_IP " , request . remote_addr )
log . write ( fulltime + " request from " + remote_addr + " \n " )
log . flush ( )
request_data = request . get_json ( silent = True )
if request_data is None :
log . write ( " json proccessing error \n " )
log . close ( )
return (
jsonify (
{
" message " : " Une erreur est survenue lors du traitement de la requête. Veuillez réessayer. "
}
) ,
400 ,
)
ticket = request_data . get ( " ticket " , " " )
user = request_data . get ( " user " , " " )
dump = request_data . get ( " dump " , " " )
scodoc = request_data . get ( " scodoc " , " " )
if (
not ticket
or not user
or not dump
or not scodoc
or not ticket . get ( " title " )
or not ticket . get ( " message " )
) :
log . write ( " missing json fields \n " )
log . close ( )
return (
jsonify (
{
" message " : " Une erreur est survenue lors du traitement de la requête (données requises manquantes). Veuillez réessayer. "
}
) ,
400 ,
)
meta = " Informations complémentaires : "
meta + = " \n - Version de ScoDoc : " + scodoc . get ( " version " , " inconnue " )
meta + = " \n - Dump : " + (
( " inclus (id : " + dump . get ( " id " , " inconnu " ) + " ) " )
if dump . get ( " included " , False )
else " non inclus "
)
meta + = " \n - Établissement : " + ticket . get ( " etab " ) if ticket . get ( " etab " ) else " "
meta + = " \n - Département : " + ticket . get ( " dept " ) if ticket . get ( " dept " ) else " "
ticket_body = ticket . get ( " message " ) + " \n \n --- \n \n " + meta
response = requests . post (
GITEA_URL + " /api/v1/repos/ " + GITEA_REPO + " /issues " ,
json = {
" title " : ticket . get ( " title " ) ,
" body " : ticket_body ,
" labels " : [ GITEA_LABEL_ID ] ,
} ,
headers = { " Authorization " : " token " + os . getenv ( " GITEA_TOKEN " ) } ,
)
if response . status_code != 201 :
2024-05-26 22:54:24 +02:00
log . write ( f " gitea error: status code= { response . status_code } \n " )
2024-05-23 00:08:46 +02:00
try :
log . write ( " sending notification to {} \n " . format ( ALERT_MAIL_TO ) )
email . send_email (
" [report] Gitea error ! " ,
ALERT_MAIL_FROM ,
[ ALERT_MAIL_TO ] ,
2024-05-26 22:54:24 +02:00
f """ Error { response . status_code }
while creating the gitea ticket :
{ response . text }
Ticket info : { ticket . get ( " title " ) }
{ ticket_body }
Utilisateur : { user . get ( " name " , " Nom inconnu " ) } < { user . get ( " email " , " Adresse email inconnue " ) } >
"""
2024-05-23 00:08:46 +02:00
)
2024-05-26 22:54:24 +02:00
except Exception as exc :
log . write ( " exception while sending email (1) ! \n " )
log . write ( f " { type ( exc ) . __name__ } , Exception message: { exc } \n " )
2024-05-23 00:08:46 +02:00
log . close ( )
return (
jsonify (
{
" message " : " Une erreur est survenue lors de la création du ticket. Veuillez réessayer. "
}
) ,
500 ,
)
try :
2024-05-26 22:54:24 +02:00
log . write ( f " sending notification to { ALERT_MAIL_TO } \n " )
2024-05-23 00:08:46 +02:00
email . send_email (
2024-05-26 22:54:24 +02:00
f """ [report] Ticket # { response . json ( ) [ " id " ] } créé: { ticket . get ( " title " ) } """ ,
2024-05-23 00:08:46 +02:00
ALERT_MAIL_FROM ,
[ ALERT_MAIL_TO ] ,
2024-05-26 22:54:24 +02:00
f """ Nouveau ticket utilisateur :
{ meta }
2024-05-28 10:13:32 +02:00
Lien du ticket : { response . json ( ) [ " html_url " ] }
2024-05-26 22:54:24 +02:00
Utilisateur : { user . get ( " name " , " Nom inconnu " ) } < { user . get ( " email " , " Adresse email inconnue " ) } >
"""
2024-05-23 00:08:46 +02:00
)
2024-05-26 22:54:24 +02:00
except Exception as exc :
log . write ( " exception while sending email (2) ! \n " )
log . write ( f " { type ( exc ) . __name__ } , Exception message: { exc } \n " )
2024-05-23 00:08:46 +02:00
log . close ( )
return (
jsonify (
{
" message " : " Votre demande a été enregistrée. Vous pouvez suivre son avancement sur <a target= ' _blank ' href= ' "
+ response . json ( ) [ " html_url " ]
+ " ' > "
+ response . json ( ) [ " html_url " ]
2024-05-26 22:54:24 +02:00
+ " </a>. Vous êtes susceptible d ' être contacté(e) par email à l ' adresse liée à votre compte ScoDoc si des informations supplémentaires sont nécessaires. "
2024-05-23 00:08:46 +02:00
}
) ,
201 ,
)
2021-09-18 22:40:07 +02:00
@bp.route ( " /scodoc-installmgr/upload-dump " , methods = [ " POST " ] )
2021-09-19 16:01:18 +02:00
def upload_scodoc9 ( ) :
""" Réception d ' un fichier de dump """
2021-09-18 17:38:52 +02:00
log = open ( LOG_FILENAME , " a " )
2024-05-26 22:54:24 +02:00
log . write ( " upload_scodoc9 \n " )
2021-09-18 17:38:52 +02:00
now = datetime . datetime . now ( )
fulltime = now . isoformat ( )
2021-09-18 21:27:30 +02:00
# client addr:
2021-09-18 22:36:55 +02:00
remote_addr = request . environ . get ( " HTTP_X_REAL_IP " , request . remote_addr )
2021-09-19 16:01:18 +02:00
log . write ( f " { fulltime } request from { remote_addr } \n " )
2024-05-26 22:54:24 +02:00
log . write ( f " { request . form } " )
2021-09-19 16:01:18 +02:00
log . flush ( )
2021-09-18 17:38:52 +02:00
# Avec seulement alphanum et tiret:
clean_deptname = re . sub ( r " [^A-Za-z-] " , " " , request . form [ " dept_name " ] )
2021-09-19 16:01:18 +02:00
if not clean_deptname :
2024-05-23 00:08:46 +02:00
return (
jsonify (
{
" message " : ' Erreur: champ dept_name manquant. \n Merci de contacter <a href= " mailto: '
+ ALERT_MAIL_TO
+ ' " > '
+ ALERT_MAIL_TO
+ " </a></p> "
}
) ,
400 ,
)
2021-09-18 17:38:52 +02:00
try :
remote_host = socket . gethostbyaddr ( remote_addr ) [ 0 ]
except :
2024-05-26 22:54:24 +02:00
log . write ( f " reverse DNS lookup failed for { remote_addr } " )
2021-09-18 17:38:52 +02:00
remote_host = " "
2021-09-19 16:01:18 +02:00
the_file = request . files [ " file " ]
filename = the_file . filename
data = the_file . read ( )
2021-09-18 17:38:52 +02:00
D = {
" dept_name " : request . form [ " dept_name " ] ,
" serial " : request . form [ " serial " ] ,
" sco_user " : request . form [ " sco_user " ] ,
" sent_by " : request . form [ " sent_by " ] ,
2021-09-19 18:00:22 +02:00
" sco_version " : request . form . get ( " sco_version " , " " ) , # release
2021-09-19 16:01:18 +02:00
" sco_subversion " : request . form . get ( " sco_subversion " , " " ) ,
2024-05-26 22:54:24 +02:00
" traceback_str " : request . form . get ( " traceback_str " , " " ) ,
2021-09-18 17:38:52 +02:00
" dump_filename " : fulltime + " _ " + clean_deptname + " .gz " ,
" dump_size " : len ( data ) ,
2022-03-22 09:01:29 +01:00
" message " : request . form . get ( " message " , " " ) ,
" request_url " : request . form . get ( " request_url " , " " ) ,
2021-09-18 17:38:52 +02:00
" remote_addr " : remote_addr ,
" remote_host " : remote_host ,
}
log . write ( " received data ( {} bytes) \n " . format ( D [ " dump_size " ] ) )
json_descr = json . dumps ( D , sort_keys = True , indent = 4 )
# --- Check disk space
2021-09-19 18:00:22 +02:00
cur_size = int (
subprocess . check_output ( [ " du " , " -skx " , REPOSIT_DIR ] )
. decode ( " utf-8 " )
. split ( " \t " ) [ 0 ]
)
2021-09-18 17:38:52 +02:00
if ( cur_size + len ( data ) / 1024 ) > MAX_REPOSIT_SIZE :
# out of space !
log . write (
" Out of space: cur_size= {} kB, limit= {} \n " . format ( cur_size , MAX_REPOSIT_SIZE )
)
# Send alert
try :
email . send_email (
" [upload-dump] Out of space ! " ,
ALERT_MAIL_FROM ,
[ ALERT_MAIL_TO ] ,
" Out space ! \n New upload was canceled: \n " + json_descr ,
)
except :
log . write ( " exception while sending email ! \n " )
2021-09-19 16:01:18 +02:00
log . close ( )
2024-05-23 00:08:46 +02:00
return (
jsonify (
{
" message " : ' Erreur: espace de stockage insuffisant. \n Merci de contacter <a href= " mailto: '
+ ALERT_MAIL_TO
+ ' " > '
+ ALERT_MAIL_TO
+ " </a></p> "
}
) ,
507 ,
)
2021-09-18 17:38:52 +02:00
else :
log . write ( " writing dump to {} \n " . format ( D [ " dump_filename " ] ) )
# dump:
f = open ( os . path . join ( REPOSIT_DIR , D [ " dump_filename " ] ) , " wb " )
f . write ( data )
f . close ( )
uplog = open ( UPLOAD_LOG_FILENAME , " a " )
uplog . write ( json_descr )
uplog . write ( " \n , \n " ) # separator
uplog . close ( )
# Send notification
try :
log . write ( " sending notification to {} \n " . format ( ALERT_MAIL_TO ) )
email . send_email (
f " [upload-dump] new dump { D [ ' dept_name ' ] } from { D [ ' remote_addr ' ] } ( { D [ ' remote_host ' ] } ) " ,
ALERT_MAIL_FROM ,
[ ALERT_MAIL_TO ] ,
" New upload: \n " + json_descr ,
)
except :
log . write ( " exception while sending email ! \n " )
2021-09-19 16:01:18 +02:00
2021-09-18 22:36:55 +02:00
log . close ( )
2024-05-23 00:08:46 +02:00
return jsonify (
{
" message " : " Données envoyées " ,
" dump_id " : fulltime + " _ " + clean_deptname ,
}
)
2021-09-18 17:38:52 +02:00
2021-09-19 16:01:18 +02:00
@bp.route ( " /scodoc-installmgr/scodoc9 " )
def scodoc9 ( ) :
"""
Réception d ' un fichier de dump uploadé
"""
log = open ( LOG_FILENAME , " a " )
log . write ( " hello \n " )
2021-09-18 17:38:52 +02:00
# Lock for counter
class Lock :
def acquire ( self ) :
self . f = open ( " lock " , " w " )
fcntl . flock ( self . f . fileno ( ) , fcntl . LOCK_EX )
def release ( self ) :
self . f . close ( )
def increment ( ) :
L = Lock ( )
L . acquire ( )
try :
try :
val = int ( open ( DIR + " /counter " ) . read ( ) )
except :
val = 0
val + = 1
open ( " counter " , " w " ) . write ( " %d " % val )
finally :
L . release ( )
return val
2021-09-18 21:19:01 +02:00
@bp.route ( " /scodoc-installmgr/version " , methods = [ " GET " ] )
2021-09-18 17:38:52 +02:00
def version ( ) :
"""
2021-09-18 17:52:33 +02:00
echo - e " DATE \t IP \t SVN \t SERIAL \t OP " > installs . log ; chown scodoc installs . log
2021-09-18 17:38:52 +02:00
"""
2021-09-18 21:35:52 +02:00
remote_addr = request . environ . get ( " HTTP_X_REAL_IP " , request . remote_addr )
2021-09-18 17:38:52 +02:00
mode = request . args . get ( " mode " , " ? " )
2021-09-18 17:52:33 +02:00
sn = request . args . get ( " sn " , " -1 " ) # serial number
2021-09-18 21:40:49 +02:00
svn = request . args . get ( " svn " , " " ) # installed subversion (ScoDoc 7)
2021-09-18 21:35:52 +02:00
release = request . args . get ( " release " , " " ) # ScoDoc 9 prod installs
commit = request . args . get ( " commit " , " " ) # installed git commit (devs)
2021-09-18 17:38:52 +02:00
if mode == " install " or not sn :
serial = increment ( )
else :
serial = sn
f = open ( DIR + " installs.log " , " a " )
f . write (
2021-09-18 21:40:49 +02:00
" %s \t %s \t %s \t %s \t %s \n "
2021-09-18 17:38:52 +02:00
% (
time . strftime ( " % Y- % m- %d % H: % M: % S " ) ,
2021-09-18 21:32:52 +02:00
remote_addr ,
2021-09-18 21:35:52 +02:00
svn or commit or release or " - " ,
2021-09-18 17:38:52 +02:00
serial ,
mode ,
)
)
f . close ( )
2021-09-19 18:00:22 +02:00
return str ( serial )