Metadata-Version: 2.4
Name: ebstemplate
Version: 0.1.1
Summary: Ephemeral buffers and Microservices Templates
Author-email: Benjamin De Zordo <benjamin.de-zordo@irisa.fr>
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: System :: Distributed Computing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests
Requires-Dist: ebcommon>=1.0.0
Requires-Dist: remotemanager<=0.14.3
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"

# ebstemplate

Couche de templates au-dessus de **ebsclient**, pour lancer facilement des jobs
HPC ordonnancés (SLURM, TGCC/MSUB) sans réécrire toute la mécanique
(connexion SSH, transferts, génération de script, suivi, statuts).

Ce template-ci (`scheduler`) couvre un cas précis : **un pipeline job =
un seul job d'ordonnanceur** (un `sbatch` / `ccc_msub`). D'autres formes
(pipelines multi-étapes, job arrays, ...) doivent faire l'objet de leurs propres
templates, en suivant la même structure.

> [!note] Installation
> ```bash
> # Installation en mode developpement
> pip install -e ./ebstemplate_package/
> # Installation via PyPI
> pip install ebstemplate
> ```
>

## Deux côtés

- **Côté utilisateur** (`ClientSchedulerTemplate`) : soumettre et suivre un job
  en quelques méthodes, sans jamais toucher au HPC directement.
- **Côté mainteneur** (`JobSchedulerTemplate`) : exécute réellement le job sur
  le centre HPC. Le mainteneur ne surcharge en général que
  `get_scheduler_core()` (la commande qui tourne sur le nœud de calcul) et,
  au besoin, `preprocess()`.

## Architecture

```
ebstemplate/
├── hpc/                     : briques HPC (réutilisables par tout template)
│   ├── config.py            : HpcConfig — lecture/validation du .ini
│   ├── remote.py            : RemoteExecutor — toutes les commandes distantes,
│   │                          avec dry_run / timeout / shlex / verbose gérés
│   │                          une seule fois et de façon cohérente
│   └── profiles.py          : génération du script, une classe par centre
│                              (IdrisProfile, CinesProfile, TgccProfile,
│                               EskemProfile) + registre PROFILES
└── scheduler/               : le template "1 job SLURM par pipeline job"
    ├── shared.py            : constantes + correspondance statut SLURM↔pipeline
    ├── user_template.py     : ClientSchedulerTemplate (côté utilisateur)
    └── job_template.py      : JobSchedulerTemplate (côté mainteneur)
```

### Ajouter un profil de centre HPC

Tout se passe dans `hpc/profiles.py` : on crée une sous-classe de
`SchedulerProfile` qui implémente `build(core)` — la méthode qui rend le script
complet (header de directives + chargement des modules + commande). On
l'enregistre ensuite dans le dictionnaire `PROFILES`. Aucune autre modification
n'est nécessaire — `JobSchedulerTemplate` choisit automatiquement le bon profil
selon le champ `hpc` de la config.

Chaque centre écrit sa configuration en clair dans son propre `build()` (les
lignes communes sont volontairement répétées d'un centre à l'autre, pour qu'on
puisse lire le script complet d'un centre d'un seul coup d'œil).

```python
class NewHPCProfile(SchedulerProfile):
    name = "jeanzay"

    def build(self, core: str) -> str:
        cfg = self.cfg
        header = textwrap.dedent(f"""\
            #!/bin/bash
            #SBATCH --job-name={cfg.job_name}
            #SBATCH --partition={cfg.partition}
            #SBATCH --nodes={cfg.nodes}
            #SBATCH --ntasks={cfg.tasks}
            #SBATCH --cpus-per-task={cfg.cores}
            #SBATCH --time={cfg.time}
            #SBATCH --output={self.output_file}
            #SBATCH --error={self.error_file}
           """)

        modules_block = ""
        if cfg.modules:
            if cfg.module_purge:
                modules_block += "module purge\n"
            modules_block += f"module load {cfg.modules}\n"

        return header + "\n" + modules_block + "\nset -x\n" + core + "\n"


# puis l'ajouter au registre :
PROFILES["nexhpcprofile"] = NewHPCProfile
```

Pour un centre qui n'utilise pas la syntaxe `#SBATCH` (par exemple TGCC avec
`#MSUB`), on procède de la même façon : on hérite de `SchedulerProfile` et on
écrit le header dans la syntaxe attendue par l'ordonnanceur du centre. Voir
`TgccProfile` comme exemple.

## Côté utilisateur — exemple

```python
from ebsclient import EbClientAPI
from ebstemplate.scheduler import ClientSchedulerTemplate

api = EbClientAPI(ebservice_url=..., ebuffer_url=..., token="...")

client = ClientSchedulerTemplate(api, ms_uuid="...")
client.infos()                                  # entrées attendues
client.set_inputs(args=["42", "4", "100"], ebin_files=["ym1.in"])
client.submit()                                 # crée les buffers et soumet
client.monitoring()                             # bloque jusqu'à l'état terminal
client.download_data("./resultats/")            # récupère les sorties

# Annuler un job en cours :
client.cancel_job()
```

### Annulation (`cancel_job`)

`cancel_job()` passe le **pipeline job** au statut `CANCELLED` via l'API. Le côté
mainteneur (`JobSchedulerTemplate`) observe ce statut pendant son suivi et annule
alors le **job SLURM** correspondant (`scancel`). L'utilisateur n'a donc pas
besoin d'accès au HPC pour annuler.

## Côté mainteneur — exemple

```python
from ebsclient import RuntimeService
from ebstemplate.scheduler import JobSchedulerTemplate

class MonJob(JobSchedulerTemplate):
    def get_scheduler_core(self) -> str:
        return f"{self.hpc_config.runner} {self.app_dir}/mon_prog -i {self.job_dir}/input.in"

rt_service = RuntimeService(
    runtime=runtime,
    RuntimeJobType=MonJob,
    hpc_config="./.secret_idris.ini",
)
rt_service.start()
```

Voir `example/openqcd_maintainer.py` pour un exemple complet.

### Propagation des statuts

Dès qu'un état **terminal** du job SLURM est détecté (`COMPLETED`, `FAILED`,
`CANCELLED`, `TIMEOUT`, ...), le statut du pipeline job est immédiatement mis à
jour pour correspondre (voir `shared.SLURM_TO_PIPELINE_STATUS`). Un état terminal
inattendu est traité comme un échec, de sorte que le pipeline job ne reste jamais
bloqué.

## Configuration HPC (.ini)

```ini
[CONNECTION]
hpc            = idris              ; idris | cines | tgcc | eskem
hostname       = idris-host
username       = mon_user
password       = ./vault/idris.pass ; chemin vers fichier contenant le password
squeue_poll    = 120
timeout        = 5                  ; commandes courtes
submit_timeout = 60                 ; soumission / transferts (plus long)
dry_run        = false

[INSTALLATION]
app_name = openqcd-ym1
work_dir = /lustre/.../mon_user     ; sinon résolu via $CCFRWORK

[SCHEDULER]
submitter = sbatch
runner    = srun
canceller = scancel

[RESOURCES]
partition = prepost
nodes = 1
tasks = 1
cores = 4
time  = 02:00:00                    ; int (s), "MM:SS" ou "HH:MM:SS"

[MODULES]
module_purge = true
modules = gcc openmpi

[JOB]
job_name = mon_job
```

> [!important] Password HPC et clé SSH
> Si vous avez configuré une clé SSH pour la connexion avec un centre HPC, il est possible l'utiliser en : 
> * supprimant le champ `password` du fichier configuration .ini,
> * configurant le `~/.ssh/config` avec votre clé et utilisé le bon hostname ssh sur la variable `hostname du .ini,
> * utiliser la commande `ssd-add` pour ajouter les clés ssh à votre agent ssh.

## Sécurité des commandes distantes

Toutes les commandes passent par `RemoteExecutor`, qui applique systématiquement :

- un **timeout** (court pour les vérifications, long pour la soumission/transferts),
- le **dry_run** de la config,
- **`verbose=0`** (pas de sortie parasite de remotemanager),
- **`shlex.quote`** sur chaque argument interpolé (chemins, noms de variables...),
  pour éviter l'injection shell et les bugs sur les espaces/caractères spéciaux.

## Tests

Les tests **ne nécessitent aucune connexion à un centre HPC** : le
`RemoteExecutor` et l'API eb sont remplacés par des doublures qui enregistrent
les appels.

```bash
pip install pytest
pytest tests/ -v
```

Couverture : génération de script par centre, correspondance des statuts SLURM,
parsing de la config, quoting/timeout/dry_run de chaque commande distante,
propagation des statuts, annulation, et résolution des répertoires.
