Compare commits
3 Commits
637798b2b9
...
e5ce3b2ec9
| Author | SHA1 | Date | |
|---|---|---|---|
| e5ce3b2ec9 | |||
| fb29b59625 | |||
| 1c3cb60153 |
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,15 +1,9 @@
|
|||||||
build/
|
|
||||||
dist/
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
*.pyd
|
*.pyd
|
||||||
*.spec.bak
|
|
||||||
*.log
|
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
.env
|
.env
|
||||||
*.db
|
|
||||||
*.sqlite
|
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
33
README.md
Normal file
33
README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# SANEF Patch Manager v2
|
||||||
|
|
||||||
|
Gestion du patching Linux via Excel + SSH + vSphere (PyQt/Tkinter + PyInstaller).
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
- Python 3.11+ sur Windows
|
||||||
|
- Accès réseau proxy SANEF : `http://proxy.sanef.fr:8080`
|
||||||
|
|
||||||
|
## Installation des dépendances
|
||||||
|
```
|
||||||
|
py -m pip install -r requirements.txt --proxy http://proxy.sanef.fr:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exécution depuis les sources
|
||||||
|
```
|
||||||
|
py patch_manager_v2.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build de l'exécutable (PyInstaller)
|
||||||
|
```
|
||||||
|
py -m PyInstaller patch_manager_v2.spec --clean --noconfirm
|
||||||
|
```
|
||||||
|
L'exe généré : `dist/SANEF_Patch_Manager.exe`
|
||||||
|
|
||||||
|
## Archive packagée
|
||||||
|
`dist/SANEF_Patch_Manager.7z` contient l'exe prêt à distribuer.
|
||||||
|
|
||||||
|
## Fichiers
|
||||||
|
- `patch_manager_v2.py` — code principal
|
||||||
|
- `patch_manager_v2.spec` — config PyInstaller
|
||||||
|
- `patch_manager_v2_backup_before_theme.py` — backup avant refonte thème
|
||||||
|
- `requirements.txt` — dépendances Python
|
||||||
|
- `build/`, `dist/` — artefacts PyInstaller (inclus dans le repo pour récup rapide)
|
||||||
5585
build/patch_manager_v2/Analysis-00.toc
Normal file
5585
build/patch_manager_v2/Analysis-00.toc
Normal file
File diff suppressed because it is too large
Load Diff
3062
build/patch_manager_v2/EXE-00.toc
Normal file
3062
build/patch_manager_v2/EXE-00.toc
Normal file
File diff suppressed because it is too large
Load Diff
3040
build/patch_manager_v2/PKG-00.toc
Normal file
3040
build/patch_manager_v2/PKG-00.toc
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/patch_manager_v2/PYZ-00.pyz
Normal file
BIN
build/patch_manager_v2/PYZ-00.pyz
Normal file
Binary file not shown.
2093
build/patch_manager_v2/PYZ-00.toc
Normal file
2093
build/patch_manager_v2/PYZ-00.toc
Normal file
File diff suppressed because it is too large
Load Diff
BIN
build/patch_manager_v2/SANEF_Patch_Manager.pkg
Normal file
BIN
build/patch_manager_v2/SANEF_Patch_Manager.pkg
Normal file
Binary file not shown.
BIN
build/patch_manager_v2/base_library.zip
Normal file
BIN
build/patch_manager_v2/base_library.zip
Normal file
Binary file not shown.
66
build/patch_manager_v2/warn-patch_manager_v2.txt
Normal file
66
build/patch_manager_v2/warn-patch_manager_v2.txt
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
This file lists modules PyInstaller was not able to find. This does not
|
||||||
|
necessarily mean these modules are required for running your program. Both
|
||||||
|
Python's standard library and 3rd-party Python packages often conditionally
|
||||||
|
import optional modules, some of which may be available only on certain
|
||||||
|
platforms.
|
||||||
|
|
||||||
|
Types of import:
|
||||||
|
* top-level: imported at the top-level - look at these first
|
||||||
|
* conditional: imported within an if-statement
|
||||||
|
* delayed: imported within a function
|
||||||
|
* optional: imported within a try-except-statement
|
||||||
|
|
||||||
|
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
|
||||||
|
tracking down the missing module yourself. Thanks!
|
||||||
|
|
||||||
|
missing module named pyimod02_importers - imported by C:\Users\netadmin\AppData\Local\Programs\Python\Python312\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgutil.py (delayed)
|
||||||
|
missing module named urllib.urlopen - imported by urllib (delayed, optional), lxml.html (delayed, optional)
|
||||||
|
missing module named urllib.urlencode - imported by urllib (delayed, optional), lxml.html (delayed, optional)
|
||||||
|
missing module named posix - imported by os (conditional, optional), shutil (conditional), importlib._bootstrap_external (conditional), posixpath (optional)
|
||||||
|
missing module named resource - imported by posix (top-level)
|
||||||
|
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional)
|
||||||
|
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional), getpass (delayed), netrc (delayed, conditional), http.server (delayed, optional)
|
||||||
|
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
|
||||||
|
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
|
||||||
|
missing module named 'numpy.typing' - imported by PIL._typing (conditional, optional)
|
||||||
|
missing module named _posixsubprocess - imported by subprocess (conditional)
|
||||||
|
missing module named fcntl - imported by subprocess (optional), pty (delayed, optional), invoke.runners (optional), invoke.terminals (conditional), paramiko.agent (delayed)
|
||||||
|
missing module named olefile - imported by PIL.FpxImagePlugin (top-level), PIL.MicImagePlugin (top-level)
|
||||||
|
missing module named defusedxml - imported by openpyxl.xml (delayed, optional), PIL.Image (optional)
|
||||||
|
missing module named OpenSSL - imported by pyVim.sso (top-level)
|
||||||
|
missing module named htmlentitydefs - imported by lxml.html.soupparser (optional)
|
||||||
|
missing module named BeautifulSoup - imported by lxml.html.soupparser (optional)
|
||||||
|
missing module named bs4 - imported by lxml.html.soupparser (optional)
|
||||||
|
missing module named urlparse - imported by pyVmomi.five (conditional), lxml.ElementInclude (optional), lxml.html.html5parser (optional)
|
||||||
|
missing module named urllib2 - imported by lxml.ElementInclude (optional), lxml.html.html5parser (optional)
|
||||||
|
missing module named 'html5lib.treebuilders' - imported by lxml.html.html5parser (top-level)
|
||||||
|
missing module named html5lib - imported by lxml.html._html5builder (top-level), lxml.html.html5parser (top-level)
|
||||||
|
missing module named 'cython.cimports' - imported by lxml.html.diff (optional)
|
||||||
|
missing module named cython - imported by lxml.html.diff (optional), lxml.html._difflib (optional)
|
||||||
|
missing module named lxml_html_clean - imported by lxml.html.clean (optional)
|
||||||
|
excluded module named unittest - imported by doctest (top-level)
|
||||||
|
missing module named termios - imported by getpass (optional), tty (top-level), invoke.runners (optional), invoke.terminals (conditional)
|
||||||
|
missing module named vms_lib - imported by platform (delayed, optional)
|
||||||
|
missing module named 'java.lang' - imported by platform (delayed, optional)
|
||||||
|
missing module named java - imported by platform (delayed)
|
||||||
|
missing module named _winreg - imported by platform (delayed, optional)
|
||||||
|
missing module named readline - imported by cmd (delayed, conditional, optional), code (delayed, conditional, optional), pdb (delayed, optional)
|
||||||
|
missing module named cssselect - imported by lxml.cssselect (optional)
|
||||||
|
missing module named httplib - imported by pyVmomi.five (conditional), pyVmomi.SoapAdapter (conditional)
|
||||||
|
missing module named 'defusedxml.ElementTree' - imported by openpyxl.xml.functions (conditional)
|
||||||
|
missing module named numpy - imported by openpyxl.compat.numbers (optional)
|
||||||
|
missing module named _scproxy - imported by urllib.request (conditional)
|
||||||
|
missing module named pyVmomi.vmodl - imported by pyVmomi (top-level), pyVim.connect (top-level)
|
||||||
|
missing module named Cookie - imported by pyVmomi.SoapAdapter (conditional)
|
||||||
|
missing module named StringIO - imported by pyVmomi.SoapAdapter (conditional)
|
||||||
|
missing module named elementtree - imported by pyVim.connect (optional)
|
||||||
|
missing module named openpyxl.tests - imported by openpyxl.reader.excel (optional)
|
||||||
|
missing module named yaml - imported by invoke.vendor.yaml.cyaml (top-level), invoke.util (optional)
|
||||||
|
missing module named lexicon - imported by invoke.util (optional), invoke.parser.parser (optional), invoke.parser.context (optional)
|
||||||
|
missing module named fluidity - imported by invoke.parser.parser (optional)
|
||||||
|
missing module named 'unittest.mock' - imported by invoke.context (top-level)
|
||||||
|
missing module named sspi - imported by paramiko.ssh_gss (optional)
|
||||||
|
missing module named sspicon - imported by paramiko.ssh_gss (optional)
|
||||||
|
missing module named pywintypes - imported by paramiko.ssh_gss (optional)
|
||||||
|
missing module named gssapi - imported by paramiko.ssh_gss (optional)
|
||||||
24881
build/patch_manager_v2/xref-patch_manager_v2.html
Normal file
24881
build/patch_manager_v2/xref-patch_manager_v2.html
Normal file
File diff suppressed because it is too large
Load Diff
BIN
dist/SANEF_Patch_Manager.7z
vendored
Normal file
BIN
dist/SANEF_Patch_Manager.7z
vendored
Normal file
Binary file not shown.
BIN
dist/SANEF_Patch_Manager.exe
vendored
Normal file
BIN
dist/SANEF_Patch_Manager.exe
vendored
Normal file
Binary file not shown.
@ -670,15 +670,72 @@ def build_fqdn(server, env):
|
|||||||
return [server, f"{server}.sanef.groupe"]
|
return [server, f"{server}.sanef.groupe"]
|
||||||
|
|
||||||
def load_key(keyfile):
|
def load_key(keyfile):
|
||||||
if not keyfile or not os.path.exists(keyfile.strip('"').strip("'")):
|
"""Charge une clé SSH privée. Retourne l'objet PKey ou None.
|
||||||
|
Si format PuTTY (.ppk), tente conversion auto via puttygen vers OpenSSH PEM.
|
||||||
|
Stocke le détail de l'échec dans load_key.last_error pour affichage UI."""
|
||||||
|
load_key.last_error = None
|
||||||
|
if not keyfile:
|
||||||
return None
|
return None
|
||||||
keyfile = keyfile.strip('"').strip("'")
|
keyfile = keyfile.strip('"').strip("'")
|
||||||
|
if not os.path.exists(keyfile):
|
||||||
|
load_key.last_error = f"Fichier clé introuvable : {keyfile}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Détection format PuTTY (.ppk)
|
||||||
|
try:
|
||||||
|
with open(keyfile, "r", encoding="utf-8", errors="ignore") as f:
|
||||||
|
first_line = f.readline().strip()
|
||||||
|
except Exception as ex:
|
||||||
|
load_key.last_error = f"Lecture impossible : {ex}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
is_ppk = first_line.startswith("PuTTY-User-Key-File-")
|
||||||
|
if is_ppk:
|
||||||
|
# Paramiko ne lit pas .ppk nativement → tenter conversion auto via puttygen
|
||||||
|
converted = keyfile + ".openssh"
|
||||||
|
puttygen_paths = [
|
||||||
|
r"C:\Program Files\PuTTY\puttygen.exe",
|
||||||
|
r"C:\Program Files (x86)\PuTTY\puttygen.exe",
|
||||||
|
"puttygen", # si dans PATH
|
||||||
|
"puttygen.exe",
|
||||||
|
]
|
||||||
|
import subprocess
|
||||||
|
converted_ok = False
|
||||||
|
for pg in puttygen_paths:
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[pg, keyfile, "-O", "private-openssh", "-o", converted],
|
||||||
|
check=True, capture_output=True, timeout=10,
|
||||||
|
)
|
||||||
|
converted_ok = True
|
||||||
|
break
|
||||||
|
except (FileNotFoundError, subprocess.SubprocessError, OSError):
|
||||||
|
continue
|
||||||
|
if not converted_ok:
|
||||||
|
load_key.last_error = (
|
||||||
|
f"Format PuTTY (.ppk) détecté pour {os.path.basename(keyfile)}. "
|
||||||
|
f"Paramiko ne lit pas ce format nativement et puttygen.exe est introuvable. "
|
||||||
|
f"Convertir manuellement : "
|
||||||
|
f'puttygen "{keyfile}" -O private-openssh -o "{keyfile}.openssh" '
|
||||||
|
f"puis pointer la conf vers le fichier converti."
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
keyfile = converted
|
||||||
|
|
||||||
|
# Tenter chargement PEM/OpenSSH (multi-format)
|
||||||
|
last_ex = None
|
||||||
for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey]:
|
for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey]:
|
||||||
try:
|
try:
|
||||||
return cls.from_private_key_file(keyfile)
|
return cls.from_private_key_file(keyfile)
|
||||||
except Exception:
|
except Exception as ex:
|
||||||
|
last_ex = ex
|
||||||
continue
|
continue
|
||||||
|
load_key.last_error = (
|
||||||
|
f"Aucun loader paramiko n'a pu lire {os.path.basename(keyfile)} "
|
||||||
|
f"(RSA / Ed25519 / ECDSA tentés). Dernière erreur : {last_ex}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
load_key.last_error = None
|
||||||
|
|
||||||
def ssh_connect(server, env, settings, pkey, pkey2, cyb_password=None):
|
def ssh_connect(server, env, settings, pkey, pkey2, cyb_password=None):
|
||||||
"""Connexion SSH avec fallback FQDN et clé. Retourne (client, hostname_effectif)"""
|
"""Connexion SSH avec fallback FQDN et clé. Retourne (client, hostname_effectif)"""
|
||||||
@ -2294,8 +2351,28 @@ class PatchManagerV2:
|
|||||||
self._reload_keys()
|
self._reload_keys()
|
||||||
|
|
||||||
def _reload_keys(self):
|
def _reload_keys(self):
|
||||||
self.pkey = load_key(self.settings.get("keyfile",""))
|
errors = []
|
||||||
self.pkey2 = load_key(self.settings.get("keyfile2",""))
|
kf1 = self.settings.get("keyfile", "")
|
||||||
|
if kf1:
|
||||||
|
self.pkey = load_key(kf1)
|
||||||
|
if self.pkey is None and load_key.last_error:
|
||||||
|
errors.append(f"Clé 1 ({kf1}) :\n{load_key.last_error}")
|
||||||
|
else:
|
||||||
|
self.pkey = None
|
||||||
|
kf2 = self.settings.get("keyfile2", "")
|
||||||
|
if kf2:
|
||||||
|
self.pkey2 = load_key(kf2)
|
||||||
|
if self.pkey2 is None and load_key.last_error:
|
||||||
|
errors.append(f"Clé 2 ({kf2}) :\n{load_key.last_error}")
|
||||||
|
else:
|
||||||
|
self.pkey2 = None
|
||||||
|
if errors:
|
||||||
|
try:
|
||||||
|
messagebox.showwarning("Clés SSH non chargées", "\n\n".join(errors))
|
||||||
|
except Exception:
|
||||||
|
# Pas de UI dispo (lancement CLI ?), juste print
|
||||||
|
for e in errors:
|
||||||
|
print(f"[load_key] {e}")
|
||||||
|
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
# ONGLET 1 — LOGIQUE
|
# ONGLET 1 — LOGIQUE
|
||||||
|
|||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
paramiko>=3.0
|
||||||
|
openpyxl>=3.1
|
||||||
|
requests>=2.31
|
||||||
|
pyVmomi>=8.0
|
||||||
|
ttkbootstrap>=1.10
|
||||||
|
pyinstaller>=6.0
|
||||||
Loading…
Reference in New Issue
Block a user