Files
pyssh-client/app/bastion.py
Florian a3c72ade0b feat: ajouter les Workspaces (enregistrer/restaurer l'etat des onglets)
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>
2026-09-05 02:47:07 +02:00

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}")