settings.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import json
  2. import subprocess
  3. from datetime import datetime
  4. from flask import (
  5. Blueprint,
  6. Response,
  7. current_app,
  8. flash,
  9. jsonify,
  10. redirect,
  11. render_template,
  12. request,
  13. url_for,
  14. )
  15. from db import db, Setting, Job, Destination, RemoteInstance, JobDestination
  16. bp = Blueprint("cfg", __name__)
  17. _SETTING_KEYS = [
  18. "smtp_host", "smtp_port", "smtp_user", "smtp_password",
  19. "smtp_from", "smtp_to", "smtp_tls", "smtp_ssl",
  20. "notify_on_success", "notify_on_error",
  21. ]
  22. def _get_setting(key, default=""):
  23. s = Setting.query.filter_by(key=key).first()
  24. return s.value if s else default
  25. @bp.route("/settings", methods=["GET", "POST"])
  26. def settings():
  27. if request.method == "POST":
  28. action = request.form.get("action")
  29. if action == "test_smtp":
  30. from notifications import send_test_email
  31. try:
  32. send_test_email(
  33. host=request.form.get("smtp_host", "").strip(),
  34. port=int(request.form.get("smtp_port", 587) or 587),
  35. user=request.form.get("smtp_user", "").strip(),
  36. password=request.form.get("smtp_password", ""),
  37. from_addr=request.form.get("smtp_from", "").strip(),
  38. to_addr=request.form.get("smtp_to", "").strip(),
  39. use_ssl=request.form.get("smtp_ssl") == "1",
  40. use_tls=request.form.get("smtp_tls") == "1",
  41. )
  42. flash("Email de test envoyé avec succès.", "success")
  43. except Exception as exc:
  44. flash(f"Échec du test SMTP : {exc}", "error")
  45. else:
  46. for key in _SETTING_KEYS:
  47. if key in ("smtp_tls", "smtp_ssl", "notify_on_success", "notify_on_error"):
  48. value = "1" if request.form.get(key) == "1" else "0"
  49. else:
  50. value = request.form.get(key, "").strip()
  51. s = Setting.query.filter_by(key=key).first()
  52. if s is None:
  53. s = Setting(key=key, value=value)
  54. db.session.add(s)
  55. else:
  56. s.value = value
  57. db.session.commit()
  58. flash("Paramètres enregistrés.", "success")
  59. return redirect(url_for("cfg.settings"))
  60. cfg = {k: _get_setting(k) for k in _SETTING_KEYS}
  61. cfg.setdefault("smtp_port", "587")
  62. cfg["smtp_tls"] = cfg.get("smtp_tls") or "1"
  63. cfg["smtp_ssl"] = cfg.get("smtp_ssl") or "0"
  64. cfg["notify_on_error"] = cfg.get("notify_on_error") or "1"
  65. api_token = current_app.config.get("API_TOKEN", "")
  66. instance_url = current_app.config.get("INSTANCE_URL", "")
  67. destinations = Destination.query.order_by(Destination.name).all()
  68. instances = RemoteInstance.query.order_by(RemoteInstance.name).all()
  69. return render_template("settings.html", cfg=cfg, api_token=api_token,
  70. instance_url=instance_url, destinations=destinations,
  71. instances=instances)
  72. @bp.route("/settings/export-config")
  73. def export_config():
  74. jobs_data = []
  75. for j in Job.query.order_by(Job.name).all():
  76. dest_names = [jd.label for jd in j.job_destinations if jd.dest_type == "ssh"]
  77. inst_names = [jd.label for jd in j.job_destinations if jd.dest_type == "instance"]
  78. jobs_data.append({
  79. "name": j.name,
  80. "type": j.type,
  81. "config_json": j.config_json,
  82. "cron_expr": j.cron_expr,
  83. "retention_mode": j.retention_mode,
  84. "retention_value": j.retention_value,
  85. "retention_gfs_config": j.retention_gfs_config,
  86. "enabled": j.enabled,
  87. "core_only": j.core_only,
  88. "destination_names": dest_names,
  89. "remote_instance_names": inst_names,
  90. })
  91. dest_data = []
  92. for d in Destination.query.order_by(Destination.name).all():
  93. dest_data.append({
  94. "name": d.name,
  95. "host": d.host,
  96. "port": d.port,
  97. "user": d.user,
  98. "remote_path": d.remote_path,
  99. "key_name": d.key_name,
  100. "enabled": d.enabled,
  101. })
  102. inst_data = []
  103. for i in RemoteInstance.query.order_by(RemoteInstance.name).all():
  104. inst_data.append({
  105. "name": i.name,
  106. "url": i.url,
  107. "api_key": i.api_key,
  108. })
  109. settings_data = {k: _get_setting(k) for k in _SETTING_KEYS}
  110. payload = {
  111. "version": 1,
  112. "exported_at": datetime.utcnow().isoformat(),
  113. "instance_name": current_app.config.get("INSTANCE_NAME", ""),
  114. "jobs": jobs_data,
  115. "destinations": dest_data,
  116. "remote_instances": inst_data,
  117. "settings": settings_data,
  118. }
  119. filename = f"backupmanager_config_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.json"
  120. return Response(
  121. json.dumps(payload, ensure_ascii=False, indent=2),
  122. mimetype="application/json",
  123. headers={"Content-Disposition": f'attachment; filename="{filename}"'},
  124. )
  125. @bp.route("/settings/import-config", methods=["POST"])
  126. def import_config():
  127. from scheduler import schedule_job, remove_job
  128. f = request.files.get("config_file")
  129. if not f or not f.filename:
  130. flash("Aucun fichier sélectionné.", "error")
  131. return redirect(url_for("cfg.settings") + "?tab=config")
  132. try:
  133. payload = json.loads(f.read().decode("utf-8"))
  134. except Exception:
  135. flash("Fichier invalide — JSON attendu.", "error")
  136. return redirect(url_for("cfg.settings") + "?tab=config")
  137. if payload.get("version") != 1:
  138. flash("Format de fichier non reconnu (version != 1).", "error")
  139. return redirect(url_for("cfg.settings") + "?tab=config")
  140. counts = {"destinations": 0, "instances": 0, "jobs": 0, "settings": 0}
  141. # --- Destinations ---
  142. for d_data in payload.get("destinations", []):
  143. name = d_data.get("name", "").strip()
  144. if not name:
  145. continue
  146. dest = Destination.query.filter_by(name=name).first()
  147. if dest is None:
  148. dest = Destination()
  149. db.session.add(dest)
  150. dest.name = name
  151. dest.host = d_data.get("host", "")
  152. dest.port = int(d_data.get("port", 22))
  153. dest.user = d_data.get("user", "root")
  154. dest.remote_path = d_data.get("remote_path", "")
  155. dest.key_name = d_data.get("key_name") or None
  156. dest.enabled = bool(d_data.get("enabled", True))
  157. counts["destinations"] += 1
  158. db.session.flush()
  159. # --- Instances distantes ---
  160. for i_data in payload.get("remote_instances", []):
  161. name = i_data.get("name", "").strip()
  162. if not name:
  163. continue
  164. inst = RemoteInstance.query.filter_by(name=name).first()
  165. if inst is None:
  166. inst = RemoteInstance()
  167. db.session.add(inst)
  168. inst.name = name
  169. inst.url = i_data.get("url", "").rstrip("/")
  170. inst.api_key = i_data.get("api_key", "")
  171. counts["instances"] += 1
  172. db.session.flush()
  173. # --- Jobs ---
  174. dest_by_name = {d.name: d for d in Destination.query.all()}
  175. inst_by_name = {i.name: i for i in RemoteInstance.query.all()}
  176. for j_data in payload.get("jobs", []):
  177. name = j_data.get("name", "").strip()
  178. if not name:
  179. continue
  180. job = Job.query.filter_by(name=name).first()
  181. if job is None:
  182. job = Job()
  183. db.session.add(job)
  184. else:
  185. remove_job(job.id)
  186. job.name = name
  187. job.type = j_data.get("type", "ynh_app")
  188. job.config_json = j_data.get("config_json") or "{}"
  189. job.cron_expr = j_data.get("cron_expr") or ""
  190. job.retention_mode = j_data.get("retention_mode", "count")
  191. job.retention_value = int(j_data.get("retention_value", 2))
  192. job.retention_gfs_config = j_data.get("retention_gfs_config")
  193. job.enabled = bool(j_data.get("enabled", True))
  194. job.core_only = bool(j_data.get("core_only", False))
  195. job.updated_at = datetime.utcnow()
  196. # Compat ancien format (destination_name / remote_instance_name) et nouveau (listes)
  197. dest_names = j_data.get("destination_names") or (
  198. [j_data["destination_name"]] if j_data.get("destination_name") else []
  199. )
  200. inst_names = j_data.get("remote_instance_names") or (
  201. [j_data["remote_instance_name"]] if j_data.get("remote_instance_name") else []
  202. )
  203. new_jds = []
  204. for dname in dest_names:
  205. if dname and dname in dest_by_name:
  206. new_jds.append(JobDestination(dest_type="ssh", dest_id=dest_by_name[dname].id))
  207. for iname in inst_names:
  208. if iname and iname in inst_by_name:
  209. new_jds.append(JobDestination(dest_type="instance", dest_id=inst_by_name[iname].id))
  210. job.job_destinations = new_jds
  211. counts["jobs"] += 1
  212. db.session.flush()
  213. # --- Paramètres SMTP ---
  214. for key, value in payload.get("settings", {}).items():
  215. if key not in _SETTING_KEYS:
  216. continue
  217. s = Setting.query.filter_by(key=key).first()
  218. if s is None:
  219. s = Setting(key=key, value=value)
  220. db.session.add(s)
  221. else:
  222. s.value = value
  223. counts["settings"] += 1
  224. db.session.commit()
  225. # Replanifier les jobs actifs
  226. for job in Job.query.filter_by(enabled=True).all():
  227. schedule_job(job)
  228. flash(
  229. f"Import réussi — {counts['jobs']} job(s), "
  230. f"{counts['destinations']} destination(s), "
  231. f"{counts['instances']} instance(s), "
  232. f"{counts['settings']} paramètre(s).",
  233. "success",
  234. )
  235. return redirect(url_for("cfg.settings") + "?tab=config")
  236. @bp.route("/internal/databases/<db_type>")
  237. def internal_databases(db_type):
  238. """Liste les bases de données disponibles pour le formulaire job."""
  239. databases = []
  240. try:
  241. if db_type == "mysql":
  242. result = subprocess.run(
  243. ["sudo", "mysql", "--skip-column-names", "-e", "SHOW DATABASES;"],
  244. capture_output=True, text=True, timeout=10,
  245. )
  246. if result.returncode == 0:
  247. exclude = {"information_schema", "performance_schema", "mysql", "sys"}
  248. databases = [d.strip() for d in result.stdout.splitlines()
  249. if d.strip() and d.strip() not in exclude]
  250. elif db_type == "postgresql":
  251. result = subprocess.run(
  252. ["sudo", "-u", "postgres", "psql", "-Atc",
  253. "SELECT datname FROM pg_database WHERE datistemplate = false;"],
  254. capture_output=True, text=True, timeout=10,
  255. )
  256. if result.returncode == 0:
  257. databases = [d.strip() for d in result.stdout.splitlines() if d.strip()]
  258. except Exception:
  259. pass
  260. return jsonify(databases)