package main import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "gopkg.in/yaml.v3" ) type DebianBlueprint struct { Name string `yaml:"name" json:"name"` Description string `yaml:"description" json:"description"` Version string `yaml:"version" json:"version"` Variant string `yaml:"variant" json:"variant"` Architecture string `yaml:"architecture" json:"architecture"` Packages BlueprintPackages `yaml:"packages" json:"packages"` Users []BlueprintUser `yaml:"users" json:"users"` Groups []BlueprintGroup `yaml:"groups" json:"groups"` Services []BlueprintService `yaml:"services" json:"services"` Files []BlueprintFile `yaml:"files" json:"files"` Customizations BlueprintCustomizations `yaml:"customizations" json:"customizations"` Created time.Time `yaml:"created" json:"created"` Modified time.Time `yaml:"modified" json:"modified"` } type BlueprintPackages struct { Include []string `yaml:"include" json:"include"` Exclude []string `yaml:"exclude" json:"exclude"` Groups []string `yaml:"groups" json:"groups"` } type BlueprintUser struct { Name string `yaml:"name" json:"name"` Description string `yaml:"description" json:"description"` Password string `yaml:"password" json:"password"` Key string `yaml:"key" json:"key"` Home string `yaml:"home" json:"home"` Shell string `yaml:"shell" json:"shell"` Groups []string `yaml:"groups" json:"groups"` UID int `yaml:"uid" json:"uid"` GID int `yaml:"gid" json:"gid"` } type BlueprintGroup struct { Name string `yaml:"name" json:"name"` Description string `yaml:"description" json:"description"` GID int `yaml:"gid" json:"gid"` } type BlueprintService struct { Name string `yaml:"name" json:"name"` Enabled bool `yaml:"enabled" json:"enabled"` Masked bool `yaml:"masked" json:"masked"` } type BlueprintFile struct { Path string `yaml:"path" json:"path"` User string `yaml:"user" json:"user"` Group string `yaml:"group" json:"group"` Mode string `yaml:"mode" json:"mode"` Data string `yaml:"data" json:"data"` EnsureParents bool `yaml:"ensure_parents" json:"ensure_parents"` } type BlueprintCustomizations struct { Hostname string `yaml:"hostname" json:"hostname"` Kernel BlueprintKernel `yaml:"kernel" json:"kernel"` Timezone string `yaml:"timezone" json:"timezone"` Locale string `yaml:"locale" json:"locale"` Firewall BlueprintFirewall `yaml:"firewall" json:"firewall"` SSH BlueprintSSH `yaml:"ssh" json:"ssh"` } type BlueprintKernel struct { Name string `yaml:"name" json:"name"` Append string `yaml:"append" json:"append"` Remove string `yaml:"remove" json:"remove"` } type BlueprintFirewall struct { Services []string `yaml:"services" json:"services"` Ports []string `yaml:"ports" json:"ports"` } type BlueprintSSH struct { KeyFile string `yaml:"key_file" json:"key_file"` User string `yaml:"user" json:"user"` } func loadBlueprint(path string) (*DebianBlueprint, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("cannot read blueprint file: %w", err) } var blueprint DebianBlueprint // Try YAML first if err := yaml.Unmarshal(data, &blueprint); err != nil { // Try JSON if YAML fails if err := json.Unmarshal(data, &blueprint); err != nil { return nil, fmt.Errorf("cannot parse blueprint file (neither YAML nor JSON): %w", err) } } // Set timestamps if not present if blueprint.Created.IsZero() { blueprint.Created = time.Now() } blueprint.Modified = time.Now() return &blueprint, nil } func saveBlueprint(blueprint *DebianBlueprint, path string, format string) error { var data []byte var err error switch strings.ToLower(format) { case "yaml", "yml": data, err = yaml.Marshal(blueprint) case "json": data, err = json.MarshalIndent(blueprint, "", " ") default: return fmt.Errorf("unsupported format: %s", format) } if err != nil { return fmt.Errorf("cannot marshal blueprint: %w", err) } // Ensure directory exists dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("cannot create directory: %w", err) } if err := os.WriteFile(path, data, 0644); err != nil { return fmt.Errorf("cannot write blueprint file: %w", err) } return nil } func validateBlueprintStructure(blueprint *DebianBlueprint) error { var errors []string if blueprint.Name == "" { errors = append(errors, "name is required") } if blueprint.Variant == "" { errors = append(errors, "variant is required") } if blueprint.Architecture == "" { errors = append(errors, "architecture is required") } // Validate variant validVariants := []string{"bookworm", "sid", "testing", "backports"} validVariant := false for _, v := range validVariants { if blueprint.Variant == v { validVariant = true break } } if !validVariant { errors = append(errors, fmt.Sprintf("invalid variant: %s (valid: %v)", blueprint.Variant, validVariants)) } // Validate architecture validArchs := []string{"amd64", "arm64", "armel", "armhf", "i386", "mips64el", "mipsel", "ppc64el", "s390x"} validArch := false for _, a := range validArchs { if blueprint.Architecture == a { validArch = true break } } if !validArch { errors = append(errors, fmt.Sprintf("invalid architecture: %s (valid: %v)", blueprint.Architecture, validArchs)) } if len(errors) > 0 { return fmt.Errorf("blueprint validation failed: %s", strings.Join(errors, "; ")) } return nil } func createDefaultBlueprint(name, variant, architecture string) *DebianBlueprint { return &DebianBlueprint{ Name: name, Description: fmt.Sprintf("Debian %s %s blueprint", variant, architecture), Version: "1.0.0", Variant: variant, Architecture: architecture, Packages: BlueprintPackages{ Include: []string{"task-minimal"}, Exclude: []string{}, Groups: []string{}, }, Users: []BlueprintUser{ { Name: "debian", Description: "Default user", Home: "/home/debian", Shell: "/bin/bash", Groups: []string{"users"}, }, }, Groups: []BlueprintGroup{ { Name: "users", Description: "Default users group", }, }, Services: []BlueprintService{ { Name: "ssh", Enabled: true, }, }, Customizations: BlueprintCustomizations{ Hostname: fmt.Sprintf("%s-%s", name, variant), Timezone: "UTC", Locale: "en_US.UTF-8", Kernel: BlueprintKernel{ Name: "linux-image-amd64", }, }, Created: time.Now(), Modified: time.Now(), } } func listBlueprints(directory string) ([]string, error) { files, err := os.ReadDir(directory) if err != nil { return nil, fmt.Errorf("cannot read directory: %w", err) } var blueprints []string for _, file := range files { if !file.IsDir() { ext := strings.ToLower(filepath.Ext(file.Name())) if ext == ".yaml" || ext == ".yml" || ext == ".json" { blueprints = append(blueprints, file.Name()) } } } return blueprints, nil }