module

Beryl::CLI::Scan

Sous-commande beryl scan <host> : se connecte au rescue Linux, détecte les disques, propose un YAML pour le fichier host.

Forme typique : beryl scan ns3156789.ip-51-83-6.eu --domain=example.net --apply

Après l'écriture de la fiche, PROPOSE de poser les records DNS (forward via le dns_provider de la zone) + le reverse IPv4/IPv6 (question, oui par défaut), pour ne pas l'oublier (idempotent : un nouveau scan d'un host en place ne change rien). --no-dns s'en passe ; --dns reste accepté. Défaut : dry-run (affiche le YAML sans rien écrire). Avec --apply : écrit ~/.config/beryl/<domaine>/<nom>.yml (nom court demandé interactivement ou via --hostname).

Constants

EXIT_ABORTED = 16
EXIT_API_ERROR = 7
EXIT_BAD_CREDS = 4
EXIT_INSUFFICIENT = 17
EXIT_NO_DISKS = 15
EXIT_OK = 0
EXIT_SSH_FAILED = 2
EXIT_UNEXPECTED = 3
EXIT_USAGE = 1
FREEBSD_RESERVED_MOUNTPOINTS = ["bin", "boot", "dev", "etc", "home", "lib", "libexec", "media", "mnt", "net", "proc", "rescue", "sbin", "sys", "tmp", "usr", "var", "zroot"] of ::String

Noms courts qui, s'ils étaient acceptés, produiraient un pool z<nom> monté sur /<nom> — lequel écraserait un dossier système FreeBSD de premier niveau et casserait l'OS (bootloader, configuration, binaires, runtime).

Liste dérivée d'un ls -l / sur FreeBSD 15 après install fraîche. Ajouter ici tout nouveau top-level qui apparaîtrait dans une version future.

root figure à part (refusé avec un message dédié « réservé au pool boot zroot ») par ask_pool_short_name et parse_pool_spec.

KNOWN_PROVIDER_BLOCKS = ["ovh", "scaleway", "dedibox", "hetzner", "latitude", "cherry", "phoenixnap", "vultr", "leaseweb"] of ::String

Blocs provider connus : retirés de l'existant si le provider a changé (sinon un ancien bloc scaleway: resterait après bascule vers ovh).

POOL_NAME_RX = /\A[a-z][a-z0-9_]*\z/

Regex des noms de pool ZFS autorisés : commence par une lettre minuscule, suivie de lettres/chiffres/underscore. Convention standard ZFS — évite les caractères qui poseraient problème en shell ou dans un YAML (espaces, tirets, majuscules…).

STRIPE = 0

Prompt du niveau RAID sous forme numérique pour un pool donné. Convention parlante beryl : 0, 1, 5, 6, 7, 10 plutôt que stripe/mirror/raidz…

Format cohérent avec pick_disks_for : options entre (…), défaut entre […] avec espaces français autour des :. Stripe (0) : seul niveau possible sur un disque.

Class methods

default_hostname(fqdn : String) : String
Source
default_raid(count : Int32) : Int32

Niveau RAID proposé par défaut, jamais un stripe quand une redondance simple existe (un stripe de N disques multiplie par N le risque de tout perdre) : miroir (1) sur deux disques, RAID 10 (miroirs agrégés, paires formées au bootstrap) sur un nombre PAIR de disques à partir de 4 ; stripe (0) sinon — un nombre impair de 3 et plus appelle un raidz, dont le niveau dépend de l'usage et se donne explicitement. Pur.

Source
discover_next_steps(account : String, hosts : Array(String), dry_run : Bool) : String

Suite à donner après --discover, avec les VRAIS noms de hosts (pas de <host> générique). En dry-run : d'abord relancer avec --apply. Le scan des disques exige le rescue : beryl rescue vient AVANT scan.

Source
discover_short_name(display : String | Nil, service_name : String, domain : String | Nil = nil) : String

Nom court d'un serveur découvert : le displayName OVH nettoyé s'il existe, sinon un placeholder new-<id> (partie ns du serviceName) à renommer par l'opérateur. Un displayName qui est déjà le FQDN du domaine cible (web1.example.net pour le domaine example.net) donne son nom court (web1) : sinon le host deviendrait web1-example-net.example.net.

Source
disk_choice_hint(candidates : Array(Disk)) : String

Exemples de réponse construits sur les VRAIS disques (pas sda,sdb en dur) : 1,2 | nvme0n1,nvme1n1 | all. Appelé avec au moins 2 disques.

Source
disk_inventory_warning(inv : Beryl::DiskInventory, detected : Array(Disk)) : String | Nil

Croise l'inventaire disques DÉCLARÉ par le provider (API matériel) avec les disques réellement vus par l'OS. Retourne un message d'alerte si l'OS en voit MOINS (cas typique : disque défaillant non énuméré, qui fait stagner le firmware au boot), sinon nil. Pur → testable.

Source
disks_table(disks : Array(Disk)) : String
Source
merge_scan_into(existing : String, scan_yaml : String) : String

Fusionne le YAML généré par scan dans un host.yml EXISTANT, en PRÉSERVANT toutes les clés que scan ne gère pas (apply_recipes, proxy_jump, ssh_host, user, port, freebsd.users…). scan possède : provider, le bloc du provider, et freebsd.{hostname,zfs}.

Les COMMENTAIRES de l'ancien fichier sont perdus (round-trip YAML) — seules les DONNÉES sont préservées. Lève si l'existant n'est pas un mapping YAML (le caller retombe alors sur l'écrasement explicite).

Source
mirror_partner?(a : Disk, b : Disk) : Bool

Partenaire de miroir possible : même taille et même type (SSD ou disque dur) — un NVMe n'est pas une alternative pour un miroir de disques durs. Pur.

Source
mirror_warnings(pool_name : String, disks : Array(Disk), raid : Int32, all : Array(Disk)) : Array(String)

Avertissements des miroirs d'un seul modèle (RAID 1 : le pool entier ; RAID 10 : chaque paire), seulement si un autre modèle compatible existe. Pur.

Source
mix_models(pools : Array(PoolSpec), all : Array(Disk)) : Array(PoolSpec)

Deux disques du même modèle viennent souvent du même lot, avec le même firmware : ils tendent à tomber en panne ensemble. Un miroir ZFS doit donc réunir deux modèles différents quand le serveur en a (ADV-3 : 2 × Micron + 2 × Samsung). Pour un RAID 10, beryl forme les miroirs avec des paires CONSÉCUTIVES : l'ordre de lsblk (nvme0n1, nvme1n1…) donnerait Micron + Micron et Samsung + Samsung. La fiche est triée (beryl fmt) et ne garde pas d'ordre : c'est beryl bootstrap qui forme les paires (raid10_order sur les disques du rescue). mix_models annonce ces paires pour chaque pool RAID 10 et avertit de tout miroir resté d'un seul modèle alors qu'un disque d'un autre modèle, de même taille et même type, aurait pu l'accompagner (all : tous les disques détectés).

Source
other_model?(disk : Disk, all : Array(Disk)) : Bool

Le serveur a-t-il un disque d'un AUTRE modèle qui pourrait être le partenaire de disk ? Pur.

Source
parse_lsblk(output : String) : Array(Disk)
Source
parse_pool_spec(spec : String, remaining : Array(Disk)) : PoolSpec

Parse une spec --pool=SHORTNAME:DISKS:RAID et retourne un PoolSpec data (pool z<SHORTNAME> + mountpoint /<SHORTNAME>, boot: false).

  • SHORTNAME : nom court sans préfixe z (ex: data, cache). Refuse root (réservé au pool boot zroot).
  • DISKS : sda,sdb ou all (tous les disques restants).
  • RAID : 0|1|5|6|7|10.

Toutes les validations lèvent ArgumentError pour remonter un message clair à l'opérateur.

Source
raid10_order(disks : Array(Disk)) : Array(Disk)

Ordre RAID 10 : paires consécutives de deux modèles différents tant que possible (on puise d'abord dans le modèle le plus nombreux) ; à égalité, l'ordre de détection est gardé. Pur.

Source
read_disks(conn : SSH::Connection) : Array(Disk)
Source
render_yaml(host : Beryl::Config::ResolvedHost, short : String, pools : Array(PoolSpec), provider_override : String | Nil = nil, server_id_override : String | Nil = nil, scaleway_zone_override : String | Nil = nil, ovh_service_name_override : String | Nil = nil, ovh_commercial_override : String | Nil = nil, ovh_hardware_override : Beryl::HardwareSpec | Nil = nil) : String

Rend le YAML d'un host. Le fichier ne contient QUE ce qui est spécifique (provider/service_name/hostname/disques/raid). Le reste vient du merge (_default.yml, <domaine>.yml).

Niveau RAID en notation numérique (0=stripe, 1=mirror, 5=raidz, 6=raidz2, 7=raidz3, 10=mirror_stripe) — traduit en mode ZFS par Beryl::Config::Zpool.zfs_mode au moment du bootstrap.

Source
rerun_with_write(args : Array(String), short : String, raw_host : String | Nil = nil, normalized_host : String | Nil = nil) : String

Construit la commande à proposer quand beryl scan affiche un YAML en mode suggestion (dry-run) : même args, mais en ajoutant --apply et, si l'opérateur n'a pas passé --hostname, le short résolu interactivement pour éviter le re-prompt.

Source
resolve_disk_selection(all : Array(Disk), answer : String) : Array(Disk)
Source
resolve_host_ipv4(host : String) : String | Nil

Résout une IPv4 depuis un host : déjà une IP → tel quel ; sinon DNS (A).

Source
resolve_host_ipv6(host : String) : String | Nil

Résout une IPv6 depuis un host : déjà une IPv6 (contient :) → tel quel ; sinon DNS (AAAA). nil si aucune (host sans IPv6 publique).

Source
resolve_write_target(explicit : String | Nil, auto : Bool, config_root : String, account_name : String, domain_name : String, short : String, existing : String | Nil = nil) : String | Nil

existing : fiche ACTUELLE du serveur (même nom court), là où elle est — y compris dans un groupe (<domaine>/<groupe>/) : réécrite sur place, jamais doublée à la racine du domaine.

Source
run(config_root : String, args : Array(String)) : Int32
Source
run_dns_setup_dedibox(host : Beryl::Config::ResolvedHost, hostname_flag : String | Nil, zone_flag : String | Nil, non_interactive : Bool, dry_run : Bool, server_id_str : String) : Beryl::CLI::DnsSetup::Plan

Variante Dedibox du flux --dns. Différences :

  • IPs + current_hostname viennent de l'API Dedibox (GET /server/{id}), pas OVH.
  • records A/AAAA posés via le dns_provider RÉEL de la zone (OVH, Gandi… selon host.dns_provider), via DnsApply.forward_via_dns_provider.
  • rename console : DediboxApi::Client.servers.update_hostname (API validée live 24 avril 2026).
  • reverse DNS : skip, l'API Dedibox ne l'expose pas. Warning explicite pour que l'opérateur le pose manuellement dans https://console.online.net.
Source
run_dns_setup_scaleway(host : Beryl::Config::ResolvedHost, hostname_flag : String | Nil, zone_flag : String | Nil, non_interactive : Bool, dry_run : Bool, server_id : String, zone_override : String | Nil) : Beryl::CLI::DnsSetup::Plan

Variante Scaleway de run_dns_setup. Différences vs Dedibox :

  • IPs + current_name viennent de l'API Scaleway (client.baremetal.servers.get(uuid, zone)). Si la zone n'est pas fournie explicitement (cas d'un UUID brut venu du shortcut), on la retrouve via find_any_zone.
  • Records A/AAAA posés via le dns_provider RÉEL de la zone (OVH, Gandi…). Même code que Dedibox (forward_via_dns_provider).
  • Rename console : client.baremetal.servers.update(name:). Scaleway n'a pas de séparation « nom système / nom console » comme Dedibox : le name sert de nom d'affichage dans la console et dans les logs d'install.
  • Reverse DNS : EXPOSÉ par l'API Scaleway, client.baremetal.servers.update(reverse:). Contrairement à Dedibox (qui force l'opérateur à passer par la console web), on peut le poser automatiquement.
Source
stripe_warning(pool_name : String, count : Int32, level : Int32) : String | Nil

Garde-fou : un zroot en stripe (RAID 0) sur plusieurs disques n'a AUCUNE redondance — un seul disque perdu = système perdu. Piège vu in vivo après un passage JBOD → ZFS : le défaut 0 accepté d'un [Entrée] a donné un zroot de 4 disques sans redondance, annulant l'intérêt du JBOD. Le défaut reste 0 (convention beryl) ; on exige seulement une confirmation explicite en interactif. Limité au zroot : un pool de données en stripe (cache…) peut être voulu. Renvoie le message d'avertissement, ou nil si rien à signaler.

Source
validate_pool_name!(name : String) : Nil

Valide le nom court d'un pool ZFS (sans préfixe z). Lève ArgumentError avec un message explicite sur quoi l'opérateur aurait dû taper. Utilisé à la fois côté interactif (ask_pool_short_name) et côté CLI (parse_pool_spec).

Deux checks :

  1. Format regex (lettre minuscule + [a-z0-9_]*).
  2. Noms réservés qui écraseraient un dossier système FreeBSD de premier niveau (/bin, /etc, /usr, /var, /tmp, /boot…) et casseraient l'OS. Sécurité : mieux vaut refuser que laisser l'opérateur saboter son FreeBSD au prochain boot.
Source
validate_raid!(answer : String) : Int32

Valide une chaîne RAID. Lève ArgumentError sur entrée non-numérique ou niveau inconnu.

Source
validate_raid_for!(answer : String, count : Int32) : Int32

validate_raid! + compatibilité avec le nombre de disques (règles de Zpool.validate!, appliquées dès le scan au lieu du bootstrap). Lève ArgumentError (re-prompt en interactif) en citant les niveaux possibles.

Source

Nested types