f-34: metadata: update metadata gen files #34
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
63fc305736
commit
883d628bf6
6 changed files with 271 additions and 59 deletions
|
|
@ -8,33 +8,37 @@ import (
|
|||
)
|
||||
|
||||
func StartMetadata(config NoCloudConfig, cfg *configuration.Config, dryrun bool) error {
|
||||
if err := WriteNoCloudFiles(config, cfg.Metadata.RunDir); err != nil {
|
||||
return fmt.Errorf("write nocloud files for %s: %w", config.Name, err)
|
||||
}
|
||||
if dryrun {
|
||||
return nil
|
||||
}
|
||||
|
||||
service, err := systemd.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to systemd: %w", err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
LoadNcCloudInDB(config, cfg.Metadata.RunDir)
|
||||
if !dryrun {
|
||||
if err := service.Start("metadata@" + config.Name + ".service"); err != nil {
|
||||
return fmt.Errorf("failed to start metadata@%s: %w", config.Name, err)
|
||||
}
|
||||
if err := service.Start("metadata@" + config.Name + ".service"); err != nil {
|
||||
return fmt.Errorf("failed to start metadata@%s: %w", config.Name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func StopMetadata(vmName string, cfg *configuration.Config, dryrun bool) error {
|
||||
service, err := systemd.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to systemd: %w", err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
UnLoadNoCloudInDB(vmName, cfg.Metadata.RunDir)
|
||||
if !dryrun {
|
||||
service, err := systemd.New()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to systemd: %w", err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
if err := service.Stop("metadata@" + vmName + ".service"); err != nil {
|
||||
return fmt.Errorf("failed to stop metadata@%s: %w", vmName, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return RemoveNoCloudFiles(vmName, cfg.Metadata.RunDir)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,21 @@ func useTestDir(t *testing.T) string {
|
|||
return t.TempDir()
|
||||
}
|
||||
|
||||
|
||||
func mustWrite(t *testing.T, cfg NoCloudConfig, dir string) {
|
||||
t.Helper()
|
||||
if err := WriteNoCloudFiles(cfg, dir); err != nil {
|
||||
t.Fatalf("WriteNoCloudFiles : %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustRemove(t *testing.T, vmName, dir string) {
|
||||
t.Helper()
|
||||
if err := RemoveNoCloudFiles(vmName, dir); err != nil {
|
||||
t.Fatalf("RemoveNoCloudFiles : %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- RenderConfig ---
|
||||
|
||||
func TestRenderConfig_MetaData(t *testing.T) {
|
||||
|
|
@ -74,14 +89,23 @@ func TestRenderConfig_NetworkConfig(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRenderConfig_UserData(t *testing.T) {
|
||||
cfg := newCfg()
|
||||
out, err := RenderConfig("templates/user-data.tmpl", cfg)
|
||||
func TestRenderConfig_UserData_DefaultIsEmpty(t *testing.T) {
|
||||
doc, err := renderDocument(DocUserData, newCfg())
|
||||
if err != nil {
|
||||
t.Fatalf("RenderConfig user-data : %v", err)
|
||||
t.Fatalf("renderDocument user-data : %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "passwd -d root") {
|
||||
t.Errorf("user-data inattendu :\n%s", out)
|
||||
if doc != "" {
|
||||
t.Errorf("le user-data par défaut doit être vide, obtenu :\n%q", doc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderConfig_UserData_NeverTouchesRootPassword(t *testing.T) {
|
||||
doc, err := renderDocument(DocUserData, newCfg())
|
||||
if err != nil {
|
||||
t.Fatalf("renderDocument user-data : %v", err)
|
||||
}
|
||||
if strings.Contains(doc, "passwd -d root") {
|
||||
t.Errorf("l'agent ne doit imposer aucune modification du compte root :\n%s", doc)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,21 +128,21 @@ func TestRenderConfig_SpecialCharsInName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- LoadNcCloudInDB / UnLoadNoCloudInDB ---
|
||||
// --- WriteNoCloudFiles / RemoveNoCloudFiles ---
|
||||
|
||||
func readTestFile(t *testing.T, dir, vmName, name string) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(filepath.Join(dir, vmName, name))
|
||||
if err != nil {
|
||||
t.Errorf("fichier %q absent après LoadNcCloudInDB : %v", name, err)
|
||||
t.Errorf("fichier %q absent après WriteNoCloudFiles : %v", name, err)
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestLoadNcCloudInDB_StoresAllFiles(t *testing.T) {
|
||||
func TestWriteNoCloudFiles_StoresAllFiles(t *testing.T) {
|
||||
dir := useTestDir(t)
|
||||
LoadNcCloudInDB(newCfg(), dir)
|
||||
mustWrite(t, newCfg(), dir)
|
||||
|
||||
files := []string{"meta-data", "user-data", "network-config", "vendor-data", "vpc", "bind_ip", "bind_port"}
|
||||
for _, f := range files {
|
||||
|
|
@ -129,9 +153,9 @@ func TestLoadNcCloudInDB_StoresAllFiles(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadNcCloudInDB_VpcAndBindValues(t *testing.T) {
|
||||
func TestWriteNoCloudFiles_VpcAndBindValues(t *testing.T) {
|
||||
dir := useTestDir(t)
|
||||
LoadNcCloudInDB(newCfg(), dir)
|
||||
mustWrite(t, newCfg(), dir)
|
||||
|
||||
if vpc := readTestFile(t, dir, "vm1", "vpc"); vpc != "vpc-test" {
|
||||
t.Errorf("vpc attendu %q, obtenu %q", "vpc-test", vpc)
|
||||
|
|
@ -144,26 +168,26 @@ func TestLoadNcCloudInDB_VpcAndBindValues(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestUnLoadNoCloudInDB_RemovesAllFiles(t *testing.T) {
|
||||
func TestRemoveNoCloudFiles_RemovesAllFiles(t *testing.T) {
|
||||
dir := useTestDir(t)
|
||||
LoadNcCloudInDB(newCfg(), dir)
|
||||
UnLoadNoCloudInDB("vm1", dir)
|
||||
mustWrite(t, newCfg(), dir)
|
||||
mustRemove(t, "vm1", dir)
|
||||
|
||||
if _, err := os.Stat(filepath.Join(dir, "vm1")); !os.IsNotExist(err) {
|
||||
t.Error("répertoire vm1 devrait être supprimé après UnLoadNoCloudInDB")
|
||||
t.Error("répertoire vm1 devrait être supprimé après RemoveNoCloudFiles")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnLoadNoCloudInDB_DoesNotAffectOtherVMs(t *testing.T) {
|
||||
func TestRemoveNoCloudFiles_DoesNotAffectOtherVMs(t *testing.T) {
|
||||
dir := useTestDir(t)
|
||||
|
||||
cfg1 := newCfg()
|
||||
cfg2 := newCfg()
|
||||
cfg2.Name = "vm2"
|
||||
LoadNcCloudInDB(cfg1, dir)
|
||||
LoadNcCloudInDB(cfg2, dir)
|
||||
mustWrite(t, cfg1, dir)
|
||||
mustWrite(t, cfg2, dir)
|
||||
|
||||
UnLoadNoCloudInDB("vm1", dir)
|
||||
mustRemove(t, "vm1", dir)
|
||||
|
||||
if _, err := os.Stat(filepath.Join(dir, "vm2", "vpc")); err != nil {
|
||||
t.Errorf("vm2 ne devrait pas être supprimée : %v", err)
|
||||
|
|
@ -264,3 +288,138 @@ func TestRootHandler_ContentType(t *testing.T) {
|
|||
t.Errorf("Content-Type attendu text/yaml, obtenu %q", ct)
|
||||
}
|
||||
}
|
||||
|
||||
// --- vendor-data : blocs conditionnels ---
|
||||
|
||||
func vendorData(t *testing.T, password, sshkey string) string {
|
||||
t.Helper()
|
||||
cfg := newCfg()
|
||||
cfg.Password = password
|
||||
cfg.SSHKEY = sshkey
|
||||
out, err := renderDocument(DocVendorData, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("renderDocument vendor-data : %v", err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestVendorData_NoCredentials_EmitsNothing(t *testing.T) {
|
||||
out := vendorData(t, "", "")
|
||||
if out != "" {
|
||||
t.Errorf("sans mot de passe ni clé, aucun compte ne doit être créé :\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVendorData_NoCredentials_NeverEmitsEmptyValues(t *testing.T) {
|
||||
out := vendorData(t, "", "")
|
||||
for _, forbidden := range []string{`passwd: ""`, `- ""`, "lock_passwd: false"} {
|
||||
if strings.Contains(out, forbidden) {
|
||||
t.Errorf("valeur vide %q émise :\n%s", forbidden, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVendorData_PasswordOnly(t *testing.T) {
|
||||
out := vendorData(t, "$6$rounds$hash", "")
|
||||
if !strings.Contains(out, `passwd: "$6$rounds$hash"`) {
|
||||
t.Errorf("hash absent :\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "lock_passwd: false") {
|
||||
t.Errorf("un compte avec mot de passe doit être déverrouillé :\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "ssh_authorized_keys") {
|
||||
t.Errorf("aucune clé fournie, le bloc ne doit pas apparaître :\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVendorData_SSHKeyOnly_LocksPassword(t *testing.T) {
|
||||
out := vendorData(t, "", "ssh-ed25519 AAAA user@host")
|
||||
if !strings.Contains(out, "ssh-ed25519 AAAA user@host") {
|
||||
t.Errorf("clé absente :\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "\n passwd:") {
|
||||
t.Errorf("aucun mot de passe fourni, le champ ne doit pas apparaître :\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "lock_passwd: true") {
|
||||
t.Errorf("sans mot de passe le compte doit rester verrouillé, sinon il devient un compte sudo sans mot de passe :\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVendorData_BothCredentials(t *testing.T) {
|
||||
out := vendorData(t, "$6$hash", "ssh-ed25519 AAAA user@host")
|
||||
for _, expected := range []string{"#cloud-config", "name: syonad", `passwd: "$6$hash"`, "lock_passwd: false", "ssh-ed25519 AAAA user@host"} {
|
||||
if !strings.Contains(out, expected) {
|
||||
t.Errorf("%q absent :\n%s", expected, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVendorData_CloudConfigHeaderIsFirstLine(t *testing.T) {
|
||||
out := vendorData(t, "$6$hash", "")
|
||||
if !strings.HasPrefix(out, "#cloud-config\n") {
|
||||
t.Errorf("cloud-init exige #cloud-config en première ligne :\n%q", out)
|
||||
}
|
||||
}
|
||||
|
||||
// --- documents fournis par l'appelant ---
|
||||
|
||||
func TestRenderDocument_VerbatimOverridesTemplate(t *testing.T) {
|
||||
cfg := newCfg()
|
||||
supplied := "#cloud-config\npackages:\n - nginx\n"
|
||||
cfg.Documents = map[string]string{DocUserData: supplied}
|
||||
|
||||
out, err := renderDocument(DocUserData, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("renderDocument : %v", err)
|
||||
}
|
||||
if out != supplied {
|
||||
t.Errorf("le document fourni doit être écrit verbatim :\nattendu %q\nobtenu %q", supplied, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderDocument_EmptySuppliedDocumentIsHonoured(t *testing.T) {
|
||||
cfg := newCfg()
|
||||
cfg.Documents = map[string]string{DocVendorData: ""}
|
||||
|
||||
out, err := renderDocument(DocVendorData, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("renderDocument : %v", err)
|
||||
}
|
||||
if out != "" {
|
||||
t.Errorf("un document explicitement vide ne doit pas retomber sur le template :\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteNoCloudFiles_WritesSuppliedDocument(t *testing.T) {
|
||||
dir := useTestDir(t)
|
||||
cfg := newCfg()
|
||||
cfg.Documents = map[string]string{DocUserData: "#cloud-config\nruncmd:\n - [ls]\n"}
|
||||
mustWrite(t, cfg, dir)
|
||||
|
||||
if got := readTestFile(t, dir, "vm1", "user-data"); got != cfg.Documents[DocUserData] {
|
||||
t.Errorf("user-data servi différent de celui fourni :\n%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// --- remontée des erreurs ---
|
||||
|
||||
func TestWriteNoCloudFiles_ReturnsErrorOnUnwritableDir(t *testing.T) {
|
||||
if os.Geteuid() == 0 {
|
||||
t.Skip("root ignore les permissions de fichiers")
|
||||
}
|
||||
dir := useTestDir(t)
|
||||
if err := os.Chmod(dir, 0500); err != nil {
|
||||
t.Fatalf("chmod : %v", err)
|
||||
}
|
||||
t.Cleanup(func() { os.Chmod(dir, 0700) })
|
||||
|
||||
if err := WriteNoCloudFiles(newCfg(), dir); err == nil {
|
||||
t.Error("une écriture impossible doit remonter une erreur : une VM ne doit jamais démarrer sans métadonnées en silence")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveNoCloudFiles_AbsentDirIsNotAnError(t *testing.T) {
|
||||
if err := RemoveNoCloudFiles("jamais-creee", useTestDir(t)); err != nil {
|
||||
t.Errorf("supprimer une VM sans fichiers ne doit pas échouer : %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,27 @@ package metadata
|
|||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//go:embed templates/*.tmpl
|
||||
var templateFS embed.FS
|
||||
|
||||
const (
|
||||
DocMetaData = "meta-data"
|
||||
DocUserData = "user-data"
|
||||
DocNetworkConfig = "network-config"
|
||||
DocVendorData = "vendor-data"
|
||||
)
|
||||
|
||||
func Documents() []string {
|
||||
return []string{DocMetaData, DocUserData, DocNetworkConfig, DocVendorData}
|
||||
}
|
||||
|
||||
func RenderConfig(path string, cfg NoCloudConfig) (string, error) {
|
||||
tpl, err := template.ParseFS(templateFS, path)
|
||||
if err != nil {
|
||||
|
|
@ -25,25 +38,55 @@ func RenderConfig(path string, cfg NoCloudConfig) (string, error) {
|
|||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func LoadNcCloudInDB(config NoCloudConfig, runDir string) {
|
||||
meta_data, _ := RenderConfig("templates/meta-data.tmpl", config)
|
||||
user_data, _ := RenderConfig("templates/user-data.tmpl", config)
|
||||
network_config, _ := RenderConfig("templates/network-config.tmpl", config)
|
||||
vendor_data, _ := RenderConfig("templates/vendor-data.tmpl", config)
|
||||
func renderDocument(name string, cfg NoCloudConfig) (string, error) {
|
||||
if doc, ok := cfg.Documents[name]; ok {
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
out, err := RenderConfig("templates/"+name+".tmpl", cfg)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("render %s: %w", name, err)
|
||||
}
|
||||
|
||||
out = strings.TrimSpace(out)
|
||||
if out == "" {
|
||||
return "", nil
|
||||
}
|
||||
return out + "\n", nil
|
||||
}
|
||||
|
||||
func WriteNoCloudFiles(config NoCloudConfig, runDir string) error {
|
||||
dir := filepath.Join(runDir, config.Name)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return
|
||||
return fmt.Errorf("create %s: %w", dir, err)
|
||||
}
|
||||
os.WriteFile(filepath.Join(dir, "meta-data"), []byte(meta_data), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "user-data"), []byte(user_data), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "network-config"), []byte(network_config), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "vendor-data"), []byte(vendor_data), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "vpc"), []byte(config.VpcName), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "bind_ip"), []byte(config.BindIP), 0644)
|
||||
os.WriteFile(filepath.Join(dir, "bind_port"), []byte(config.BindPort), 0644)
|
||||
|
||||
files := map[string]string{
|
||||
"vpc": config.VpcName,
|
||||
"bind_ip": config.BindIP,
|
||||
"bind_port": config.BindPort,
|
||||
}
|
||||
for _, name := range Documents() {
|
||||
doc, err := renderDocument(name, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files[name] = doc
|
||||
}
|
||||
|
||||
for name, content := range files {
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnLoadNoCloudInDB(vmName string, runDir string) {
|
||||
os.RemoveAll(filepath.Join(runDir, vmName))
|
||||
func RemoveNoCloudFiles(vmName string, runDir string) error {
|
||||
dir := filepath.Join(runDir, vmName)
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
return fmt.Errorf("remove %s: %w", dir, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@ type ServerConfig struct {
|
|||
}
|
||||
|
||||
type NoCloudConfig struct {
|
||||
VpcName string
|
||||
BindIP string
|
||||
BindPort string
|
||||
Name string
|
||||
Password string
|
||||
SSHKEY string
|
||||
VpcName string
|
||||
BindIP string
|
||||
BindPort string
|
||||
Name string
|
||||
Password string
|
||||
SSHKEY string
|
||||
Documents map[string]string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
passwd -d root
|
||||
|
|
@ -1,9 +1,17 @@
|
|||
{{- if or .Password .SSHKEY -}}
|
||||
#cloud-config
|
||||
users:
|
||||
- name: syonad
|
||||
lock_passwd: false
|
||||
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
|
||||
shell: /bin/bash
|
||||
{{- if .Password }}
|
||||
lock_passwd: false
|
||||
passwd: "{{ .Password }}"
|
||||
{{- else }}
|
||||
lock_passwd: true
|
||||
{{- end }}
|
||||
{{- if .SSHKEY }}
|
||||
ssh_authorized_keys:
|
||||
- "{{ .SSHKEY }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue