Un workspace capture les onglets ouverts (sessions SSH, terminaux locaux, splits avec leurs tailles) et les restaure plus tard. Ne reference les sessions que par id : aucun mot de passe stocke, redemande a la restauration exactement comme une ouverture normale. - app/workspace.py : Workspace/WorkspaceTab/WorkspaceItem + WorkspaceStore (meme pattern que SessionStore/KeyManager : JSON atomique, schema_version). - app/controllers/workspace_manager.py : capture() lit l'etat reel des onglets de ConnectionController (types de widgets, layout et tailles d'un SplitContainer) ; restore() rouvre chaque onglet via ConnectionController.build_session_tab()/build_sftp_tab()/new_local_terminal() (factorises hors de open_session()/open_sftp_session() pour etre reutilisables ici) puis reconstruit les splits avec split_tab(). Un element non restaurable (session supprimee depuis) est signale (compteur "ignores") plutot que de faire echouer toute la restauration. - WorkspaceDialog : creer / enregistrer (ecraser) / ouvrir / renommer / supprimer, plus la case "Restaurer automatiquement le dernier workspace ouvert au demarrage" (desactivee par defaut). - app/app_settings.py : preferences globales (theme, taille de police, dernier workspace) consolidees dans un seul module au lieu d'un mecanisme de lecture/ecriture duplique par prefernce dans theme.py. - SplitContainer expose layout_kind et get_layout_sizes()/set_layout_sizes() (parcours recursif des QSplitter imbriques) pour la capture/restauration fidele d'une grille 2x2. tests/test_workspace.py : capture (simple + split), sauvegarde/rechargement JSON, restauration complete et partielle, renommer/supprimer/re-enregistrer, restauration automatique - avec app_settings.SETTINGS_FILE redirige vers un fichier temporaire pour ne jamais toucher les vrais reglages de l'utilisateur. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
75 lines
3.0 KiB
Python
75 lines
3.0 KiB
Python
"""Resolution d'un BastionConfig (session/openssh/manuel) en parametres de
|
|
connexion concrets, independamment de la source choisie dans l'editeur.
|
|
|
|
La connexion elle-meme (ouverture du canal direct-tcpip a travers le bastion)
|
|
est geree par SSHConnection (app.ssh_connection), qui consomme un BastionTarget
|
|
sans se soucier de sa provenance.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from app.session_manager import (
|
|
SessionStore, BastionConfig,
|
|
BASTION_NONE, BASTION_SESSION, BASTION_OPENSSH, BASTION_MANUAL,
|
|
AUTH_AUTO, AUTH_KEY,
|
|
)
|
|
|
|
|
|
class BastionResolutionError(Exception):
|
|
"""Le bastion configure ne peut pas etre resolu (reference cassee, config
|
|
invalide...). Empeche de lancer une connexion vouee a l'echec."""
|
|
|
|
|
|
@dataclass
|
|
class BastionTarget:
|
|
"""Parametres de connexion resolus pour le saut a franchir avant la cible."""
|
|
host: str
|
|
port: int
|
|
username: str
|
|
auth_type: str
|
|
key_path: str = ""
|
|
|
|
@property
|
|
def label(self) -> str:
|
|
return f"{self.username}@{self.host}" if self.username else self.host
|
|
|
|
|
|
def resolve_bastion_target(
|
|
bastion: BastionConfig | None, session_store: SessionStore | None = None,
|
|
) -> BastionTarget | None:
|
|
"""Renvoie None si aucun bastion n'est configure. Leve BastionResolutionError
|
|
si le mode choisi ne peut pas etre resolu en ce moment (session supprimee,
|
|
alias OpenSSH introuvable, config manuelle incomplete...)."""
|
|
if bastion is None or bastion.mode in (BASTION_NONE, ""):
|
|
return None
|
|
|
|
if bastion.mode == BASTION_MANUAL:
|
|
if not bastion.host.strip():
|
|
raise BastionResolutionError("Bastion manuel : aucune adresse renseignee.")
|
|
return BastionTarget(bastion.host.strip(), bastion.port, bastion.username, bastion.auth_type, bastion.key_path)
|
|
|
|
if bastion.mode == BASTION_SESSION:
|
|
if session_store is None:
|
|
raise BastionResolutionError("Bastion vers une session PySSH : aucun registre de sessions disponible.")
|
|
target = session_store.get(bastion.session_id)
|
|
if target is None:
|
|
raise BastionResolutionError(
|
|
"La session utilisee comme bastion est introuvable (supprimee ou renommee)."
|
|
)
|
|
return BastionTarget(target.host, target.port, target.username, target.auth_type, target.key_path)
|
|
|
|
if bastion.mode == BASTION_OPENSSH:
|
|
from app.openssh.config import load_openssh_hosts
|
|
|
|
host = next((h for h in load_openssh_hosts() if h.alias == bastion.openssh_alias), None)
|
|
if host is None:
|
|
raise BastionResolutionError(
|
|
f"Host OpenSSH « {bastion.openssh_alias} » introuvable dans ~/.ssh/config."
|
|
)
|
|
key_path = host.identity_files[0] if host.identity_files else ""
|
|
auth_type = AUTH_KEY if key_path else AUTH_AUTO
|
|
return BastionTarget(host.hostname or host.alias, host.port, host.user, auth_type, key_path)
|
|
|
|
raise BastionResolutionError(f"Mode de bastion inconnu : {bastion.mode}")
|