260 lines
7.2 KiB
Go
260 lines
7.2 KiB
Go
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
|
|
}
|