2026-04-29
This commit is contained in:
@@ -4,17 +4,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"gitea.sunhpc.com/kelvin/sunhpc"
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/app/control"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
root := sunhpc.GetRootCommand()
|
root := control.GetRootCommand()
|
||||||
|
|
||||||
err := root.Execute()
|
err := root.Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
|
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
|
||||||
if sunhpc.DebugFlag {
|
if control.DebugFlag {
|
||||||
fmt.Printf("\nSTACK TRACE: %+v\n", err)
|
fmt.Printf("\nSTACK TRACE: %+v\n", err)
|
||||||
}
|
}
|
||||||
os.Exit(255)
|
os.Exit(255)
|
||||||
|
|||||||
11
go.mod
11
go.mod
@@ -1,5 +1,12 @@
|
|||||||
module gitea.sunhpc.com/kelvin/sunhpc.git
|
module gitea.sunhpc.com/kelvin/sunhpc
|
||||||
|
|
||||||
go 1.25.8
|
go 1.25.8
|
||||||
|
|
||||||
require gitea.sunhpc.com/kelvin/sunhpc v0.0.0-20260428114043-53d56ec6e2ba // indirect
|
require github.com/spf13/cobra v1.10.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/creasty/defaults v1.8.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -1,2 +1,14 @@
|
|||||||
gitea.sunhpc.com/kelvin/sunhpc v0.0.0-20260428114043-53d56ec6e2ba h1:NKGusycgJgYD/ljVamRqkLm8+jqT2h09xVjZf7NBAqc=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
gitea.sunhpc.com/kelvin/sunhpc v0.0.0-20260428114043-53d56ec6e2ba/go.mod h1:qQ5hwUVNDDEjeZRE35FzMpsobqly7h0tRcBGCcvzgrc=
|
github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk=
|
||||||
|
github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
19
internal/app/control/clean/main.go
Normal file
19
internal/app/control/clean/main.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package clean
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/clean"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) {
|
||||||
|
return func(cmd *cobra.Command, args []string) (err error) {
|
||||||
|
if err = clean.CleanOciBlobCacheDir(); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err = clean.CleanOverlays(); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
internal/app/control/clean/root.go
Normal file
23
internal/app/control/clean/root.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package clean
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/app/control/completions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type variables struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCommand() *cobra.Command {
|
||||||
|
vars := variables{}
|
||||||
|
baseCmd := &cobra.Command{
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Use: "clean",
|
||||||
|
Short: "Clean up",
|
||||||
|
Long: "This command cleans the OCI cache and removes leftovers from deleted nodes",
|
||||||
|
RunE: CobraRunE(&vars),
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
ValidArgsFunction: completions.None,
|
||||||
|
}
|
||||||
|
return baseCmd
|
||||||
|
}
|
||||||
9
internal/app/control/completions/completions.go
Normal file
9
internal/app/control/completions/completions.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package completions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func None(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
10
internal/app/control/configure/dhcp/main.go
Normal file
10
internal/app/control/configure/dhcp/main.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package dhcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/configure"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CobraRunE(cmd *cobra.Command, args []string) error {
|
||||||
|
return configure.DHCP()
|
||||||
|
}
|
||||||
24
internal/app/control/configure/dhcp/root.go
Normal file
24
internal/app/control/configure/dhcp/root.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package dhcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/app/control/completions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseCmd = &cobra.Command{
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Use: "dhcp [OPTIONS]",
|
||||||
|
Short: "Manage and initialize DHCP",
|
||||||
|
Long: "DHCP is a dependent service to Warewulf. This command will configure DHCP as defined\n" +
|
||||||
|
"in the warewulf.conf file.",
|
||||||
|
RunE: CobraRunE,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
ValidArgsFunction: completions.None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRootCommand returns the root cobra.Command for the application.
|
||||||
|
func GetCommand() *cobra.Command {
|
||||||
|
return baseCmd
|
||||||
|
}
|
||||||
32
internal/app/control/configure/main.go
Normal file
32
internal/app/control/configure/main.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package configure
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
sunhpcconf "gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/config"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/configure"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/splog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CobraRunE(cmd *cobra.Command, args []string) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
conf := sunhpcconf.Get()
|
||||||
|
if conf.Autodetected() && conf.InitializedFromFile() {
|
||||||
|
if err = conf.PersistToFile(conf.GetSunhpcConf()); err != nil {
|
||||||
|
splog.Warn("error when persisting auto-detected settings: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allFunctions {
|
||||||
|
if _, err = configure.TLS(false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = configure.SUNHPCD()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
30
internal/app/control/configure/root.go
Normal file
30
internal/app/control/configure/root.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package configure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/app/control/configure/dhcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseCmd = &cobra.Command{
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Use: "config [options]",
|
||||||
|
Short: "Manage sunhpc configuration",
|
||||||
|
Long: "This application allows you to manage and initialize Sunhpc configuration\n" +
|
||||||
|
"services based on the configuration in the sunhpc.conf file.",
|
||||||
|
RunE: CobraRunE,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
allFunctions bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
baseCmd.AddCommand(dhcp.GetCommand())
|
||||||
|
baseCmd.Flags().BoolVarP(&allFunctions, "all", "a", false, "Configure all services")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRootCommand returns the root cobra.Command for the application.
|
||||||
|
func GetCommand() *cobra.Command {
|
||||||
|
return baseCmd
|
||||||
|
}
|
||||||
@@ -4,7 +4,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gitea.sunhpc.com/kelvin/sunhpc/internal/app/control/version"
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/app/control/clean"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/app/control/configure"
|
||||||
|
|
||||||
|
sunhpcconf "gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/config"
|
||||||
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/help"
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/help"
|
||||||
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/splog"
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/splog"
|
||||||
)
|
)
|
||||||
@@ -16,18 +19,19 @@ var (
|
|||||||
Short: "Sunhpc Control",
|
Short: "Sunhpc Control",
|
||||||
Long: `Control interface to the SunHPC Cluster Provisioning System.`,
|
Long: `Control interface to the SunHPC Cluster Provisioning System.`,
|
||||||
PersistentPreRunE: rootPersistentPreRunE,
|
PersistentPreRunE: rootPersistentPreRunE,
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
}
|
}
|
||||||
verboseArg bool
|
verboseArg bool
|
||||||
DebugFlag bool
|
DebugFlag bool
|
||||||
SunhpcConfArg string
|
LogLevel int
|
||||||
AllowEmptyConf bool
|
SunhpcConfArg string
|
||||||
|
AllowEmptyConf bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
//rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||||
rootCmd.PersistentFlags().BoolVarP(&verboseArg, "verbose", "v", false, "verbose output")
|
rootCmd.PersistentFlags().BoolVarP(&verboseArg, "verbose", "v", false, "verbose output")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&DebugFlag, "debug", "d", false, "debug output")
|
rootCmd.PersistentFlags().BoolVarP(&DebugFlag, "debug", "d", false, "debug output")
|
||||||
rootCmd.PersistentFlags().IntVar(&LogLevel, "loglevel", splog.INFO, "Set log level to given string")
|
rootCmd.PersistentFlags().IntVar(&LogLevel, "loglevel", splog.INFO, "Set log level to given string")
|
||||||
@@ -35,6 +39,42 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringVar(&SunhpcConfArg, "sunhpcconf", "", "Set the sunhpc configuration file")
|
rootCmd.PersistentFlags().StringVar(&SunhpcConfArg, "sunhpcconf", "", "Set the sunhpc configuration file")
|
||||||
rootCmd.PersistentFlags().BoolVar(&AllowEmptyConf, "emptyconf", false, "Allow empty configuration")
|
rootCmd.PersistentFlags().BoolVar(&AllowEmptyConf, "emptyconf", false, "Allow empty configuration")
|
||||||
_ = rootCmd.PersistentFlags().MarkHidden("emptyconf")
|
_ = rootCmd.PersistentFlags().MarkHidden("emptyconf")
|
||||||
|
|
||||||
rootCmd.SetUsageTemplate(help.UsageTemplate)
|
rootCmd.SetUsageTemplate(help.UsageTemplate)
|
||||||
rootCmd.SetHelpTemplate(help.HelpTemplate)
|
rootCmd.SetHelpTemplate(help.HelpTemplate)
|
||||||
}
|
|
||||||
|
rootCmd.AddCommand(clean.GetCommand())
|
||||||
|
rootCmd.AddCommand(configure.GetCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRootCommand() *cobra.Command {
|
||||||
|
return rootCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func rootPersistentPreRunE(cmd *cobra.Command, args []string) (err error) {
|
||||||
|
if DebugFlag {
|
||||||
|
splog.SetLogLevel(splog.DEBUG)
|
||||||
|
} else if verboseArg {
|
||||||
|
splog.SetLogLevel(splog.VERBOSE)
|
||||||
|
} else {
|
||||||
|
splog.SetLogLevel(splog.INFO)
|
||||||
|
}
|
||||||
|
if LogLevel != splog.INFO {
|
||||||
|
splog.SetLogLevel(LogLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if SunhpcConfArg != "" {
|
||||||
|
sunhpcconf.ConfigFile = SunhpcConfArg
|
||||||
|
} else if os.Getenv("SUNHPC_CONF") != "" {
|
||||||
|
sunhpcconf.ConfigFile = os.Getenv("SUNHPC_CONF")
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := sunhpcconf.Get()
|
||||||
|
if !AllowEmptyConf && !conf.InitializedFromFile() {
|
||||||
|
if err = conf.Read(sunhpcconf.ConfigFile, true); err != nil {
|
||||||
|
splog.Error("error reading configuration file: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
9
internal/pkg/clean/clean.go
Normal file
9
internal/pkg/clean/clean.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package clean
|
||||||
|
|
||||||
|
func CleanOciBlobCacheDir() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanOverlays() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
56
internal/pkg/config/api.go
Normal file
56
internal/pkg/config/api.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/creasty/defaults"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IPNet net.IPNet
|
||||||
|
|
||||||
|
func (n IPNet) MarshalText() ([]byte, error) {
|
||||||
|
ipnet := n.IPNet()
|
||||||
|
return []byte((&ipnet).String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *IPNet) UnmarshalText(text []byte) error {
|
||||||
|
_, network, err := net.ParseCIDR(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*n = IPNet(*network)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n IPNet) IPNet() net.IPNet {
|
||||||
|
return net.IPNet(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIConf struct {
|
||||||
|
EnabledP *bool `yaml:"enabled,omitempty" default:"false"`
|
||||||
|
TLSEnabledP *bool `yaml:"tls,omitempty"`
|
||||||
|
AllowedNets []IPNet `yaml:"allowed subnets,omitempty" default:"[\"127.0.0.0/8\", \"::1/128\"]"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf *APIConf) AllowedIPNets() (allowedIPNets []net.IPNet) {
|
||||||
|
for _, allowedIPNet := range conf.AllowedNets {
|
||||||
|
allowedIPNets = append(allowedIPNets, allowedIPNet.IPNet())
|
||||||
|
}
|
||||||
|
return allowedIPNets
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf *APIConf) Unmarshal(unmarshal func(interface{}) error) error {
|
||||||
|
if err := defaults.Set(conf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf APIConf) Enabled() bool {
|
||||||
|
return util.BoolP(conf.EnabledP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf APIConf) TLSEnabled() bool {
|
||||||
|
return util.BoolP(conf.TLSEnabledP)
|
||||||
|
}
|
||||||
97
internal/pkg/config/buildconfig.go
Normal file
97
internal/pkg/config/buildconfig.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ConfigFile = "/etc/sunhpc/sunhpc.conf"
|
||||||
|
|
||||||
|
type BuildConfig struct {
|
||||||
|
Bindir string `yaml:"bindir,omitempty" default:"@BINDIR@"`
|
||||||
|
Sysconfdir string `yaml:"sysconfdir,omitempty" default:"@SYSCONFDIR@"`
|
||||||
|
Localstatedir string `yaml:"localstatedir,omitempty" default:"@LOCALSTATEDIR@"`
|
||||||
|
Cachedir string `yaml:"cachedir,omitempty" default:"@CACHEDIR@"`
|
||||||
|
Ipxesource string `yaml:"ipxesource,omitempty" default:"@IPXESOURCE@"`
|
||||||
|
Srvdir string `yaml:"srvdir,omitempty" default:"@SRVDIR@"`
|
||||||
|
Firewallddir string `yaml:"firewallddir,omitempty" default:"@FIREWALLDDIR@"`
|
||||||
|
Systemddir string `yaml:"systemddir,omitempty" default:"@SYSTEMDDIR@"`
|
||||||
|
Datadir string `yaml:"datadir,omitempty" default:"@DATADIR@"`
|
||||||
|
WWOverlaydir string `yaml:"wwoverlaydir,omitempty" default:"@WWOVERLAYDIR@"`
|
||||||
|
WWChrootdir string `yaml:"wwchrootdir,omitempty" default:"@WWCHROOTDIR@"`
|
||||||
|
WWProvisiondir string `yaml:"wwprovisiondir,omitempty" default:"@WWPROVISIONDIR@"`
|
||||||
|
WWClientdir string `yaml:"wwclientdir,omitempty" default:"@WWCLIENTDIR@"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const Version = "1.0.0"
|
||||||
|
const Release = "1"
|
||||||
|
|
||||||
|
type TFTPConf struct {
|
||||||
|
EnabledP *bool `yaml:"enabled" default:"true"`
|
||||||
|
TftpRoot string `yaml:"tftproot,omitempty" default:"@TFTPDIR@"`
|
||||||
|
SystemdName string `yaml:"systemd name,omitempty" default:"tftp"`
|
||||||
|
|
||||||
|
IpxeBinaries map[string]string `yaml:"ipxe,omitempty" default:"{\"00:09\": \"ipxe-snponly-x86_64.efi\",\"00:00\": \"undionly.kpxe\",\"00:0B\": \"arm64-efi/snponly.efi\",\"00:07\": \"ipxe-snponly-x86_64.efi\"}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf TFTPConf) Enabled() bool {
|
||||||
|
return util.BoolP(conf.EnabledP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SunhpcConf adds additional Sunhpc-specific configuration to
|
||||||
|
// BaseConf.
|
||||||
|
type SunhpcConf struct {
|
||||||
|
Port int `yaml:"port,omitempty" default:"9873"`
|
||||||
|
TLSPort int `yaml:"tls port,omitempty" default:"9874"`
|
||||||
|
SecureP *bool `yaml:"secure,omitempty" default:"true"`
|
||||||
|
TLSEnabledP *bool `yaml:"tls,omitempty"`
|
||||||
|
UpdateInterval int `yaml:"update interval,omitempty" default:"60"`
|
||||||
|
AutobuildOverlaysP *bool `yaml:"autobuild overlays,omitempty" default:"true"`
|
||||||
|
EnableHostOverlayP *bool `yaml:"host overlay,omitempty" default:"true"`
|
||||||
|
GrubBootP *bool `yaml:"grubboot,omitempty" default:"false"`
|
||||||
|
SystemdName string `yaml:"systemd name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf SunhpcConf) Secure() bool {
|
||||||
|
return util.BoolP(conf.SecureP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf SunhpcConf) TLSEnabled() bool {
|
||||||
|
return util.BoolP(conf.TLSEnabledP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf SunhpcConf) AutobuildOverlays() bool {
|
||||||
|
return util.BoolP(conf.AutobuildOverlaysP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf SunhpcConf) EnableHostOverlay() bool {
|
||||||
|
return util.BoolP(conf.EnableHostOverlayP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf SunhpcConf) GrubBoot() bool {
|
||||||
|
return util.BoolP(conf.GrubBootP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (paths BuildConfig) NodesConf() string {
|
||||||
|
return path.Join(paths.Sysconfdir, "sunhpc", "nodes.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (paths BuildConfig) AuthenticationConf() string {
|
||||||
|
return path.Join(paths.Sysconfdir, "sunhpc", "auth.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (paths BuildConfig) OciBlobCachedir() string {
|
||||||
|
return path.Join(paths.Cachedir, "sunhpc")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (paths BuildConfig) SiteOverlaydir() string {
|
||||||
|
return paths.WWOverlaydir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (paths BuildConfig) DistributionOverlaydir() string {
|
||||||
|
return path.Join(paths.Datadir, "sunhpc", "overlays")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (paths BuildConfig) OverlayProvisiondir() string {
|
||||||
|
return path.Join(paths.WWProvisiondir, "overlays")
|
||||||
|
}
|
||||||
5
internal/pkg/config/client.go
Normal file
5
internal/pkg/config/client.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type SPClientConf struct {
|
||||||
|
Port uint16 `yaml:"port,omitempty" default:"0"`
|
||||||
|
}
|
||||||
21
internal/pkg/config/dhcp.go
Normal file
21
internal/pkg/config/dhcp.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DHCPConf represents the configuration for the DHCP service that
|
||||||
|
// Sunhpc will configure.
|
||||||
|
type DHCPConf struct {
|
||||||
|
EnabledP *bool `yaml:"enabled,omitempty" default:"true"`
|
||||||
|
Template string `yaml:"template,omitempty" default:"default"`
|
||||||
|
RangeStart string `yaml:"range start,omitempty"`
|
||||||
|
RangeEnd string `yaml:"range end,omitempty"`
|
||||||
|
Range6Start string `yaml:"range6 start,omitempty"`
|
||||||
|
Range6End string `yaml:"range6 end,omitempty"`
|
||||||
|
SystemdName string `yaml:"systemd name,omitempty" default:"dhcpd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf DHCPConf) Enabled() bool {
|
||||||
|
return util.BoolP(conf.EnabledP)
|
||||||
|
}
|
||||||
21
internal/pkg/config/dnsmasq.go
Normal file
21
internal/pkg/config/dnsmasq.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSmasqConf represents the configuration for the DNSmasq service that
|
||||||
|
// Sunhpc will configure.
|
||||||
|
type DNSMASQConf struct {
|
||||||
|
EnabledP *bool `yaml:"enabled,omitempty" default:"true"`
|
||||||
|
Template string `yaml:"template,omitempty" default:"default"`
|
||||||
|
RangeStart string `yaml:"range start,omitempty"`
|
||||||
|
RangeEnd string `yaml:"range end,omitempty"`
|
||||||
|
Range6Start string `yaml:"range6 start,omitempty"`
|
||||||
|
Range6End string `yaml:"range6 end,omitempty"`
|
||||||
|
SystemdName string `yaml:"systemd name,omitempty" default:"dhcpd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf DNSMASQConf) Enabled() bool {
|
||||||
|
return util.BoolP(conf.EnabledP)
|
||||||
|
}
|
||||||
23
internal/pkg/config/mounts.go
Normal file
23
internal/pkg/config/mounts.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A MountEntry represents a bind mount that is applied to an image
|
||||||
|
// during exec and shell.
|
||||||
|
type MountEntry struct {
|
||||||
|
Source string `yaml:"source"`
|
||||||
|
Dest string `yaml:"dest,omitempty"`
|
||||||
|
ReadOnlyP *bool `yaml:"readonly,omitempty"`
|
||||||
|
Options string `yaml:"options,omitempty"` // ignored at the moment
|
||||||
|
CopyP *bool `yaml:"copy,omitempty"` // temporarily copy the file into the image
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mount MountEntry) ReadOnly() bool {
|
||||||
|
return util.BoolP(mount.ReadOnlyP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mount MountEntry) Copy() bool {
|
||||||
|
return util.BoolP(mount.CopyP)
|
||||||
|
}
|
||||||
34
internal/pkg/config/nfs.go
Normal file
34
internal/pkg/config/nfs.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/creasty/defaults"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NFSConf represents the NFS configuration that will be used by
|
||||||
|
// Sunhpc to generate exports on the server and mounts on compute
|
||||||
|
// nodes.
|
||||||
|
type NFSConf struct {
|
||||||
|
EnabledP *bool `yaml:"enabled,omitempty" default:"true"`
|
||||||
|
ExportsExtended []*NFSExportConf `yaml:"export paths,omitempty" default:"[]"`
|
||||||
|
SystemdName string `yaml:"systemd name,omitempty" default:"nfsd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf NFSConf) Enabled() bool {
|
||||||
|
return util.BoolP(conf.EnabledP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An NFSExportConf reprents a single NFS export / mount.
|
||||||
|
type NFSExportConf struct {
|
||||||
|
Path string `yaml:"path" default:"/dev/null"`
|
||||||
|
ExportOptions string `yaml:"export options,omitempty" default:"rw,sync,no_subtree_check"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements the Unmarshal interface for NFSConf to set default
|
||||||
|
// values.
|
||||||
|
func (conf *NFSConf) Unmarshal(unmarshal func(interface{}) error) error {
|
||||||
|
if err := defaults.Set(conf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
262
internal/pkg/config/root.go
Normal file
262
internal/pkg/config/root.go
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
// Package config reads, parses, and represents the sunhpc.conf
|
||||||
|
// config file.
|
||||||
|
//
|
||||||
|
// sunhpc.conf is a yaml-formatted configuration file that includes
|
||||||
|
// configuration for the sunhpc daemon and commands, as well as the
|
||||||
|
// DHCP, TFTP and NFS services that sunhpc manages.
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/creasty/defaults"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/splog"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cachedConf SunhpcYaml
|
||||||
|
|
||||||
|
// SunhpcYaml is the main Sunhpc configuration structure. It stores
|
||||||
|
// some information about the Sunhpc server locally, and has
|
||||||
|
// [SunhpcConf], [DHCPConf], [TFTPConf], and [NFSConf] sub-sections.
|
||||||
|
type SunhpcYaml struct {
|
||||||
|
Comment string `yaml:"comment,omitempty"`
|
||||||
|
Ipaddr string `yaml:"ipaddr,omitempty"`
|
||||||
|
Netmask string `yaml:"netmask,omitempty"`
|
||||||
|
Network string `yaml:"network,omitempty"`
|
||||||
|
Fqdn string `yaml:"fqdn,omitempty"`
|
||||||
|
Ipaddr6 string `yaml:"ipaddr6,omitempty"`
|
||||||
|
PrefixLen6 string `yaml:"prefixlen6,omitempty"`
|
||||||
|
Sunhpc *SunhpcConf `yaml:"sunhpc,omitempty"`
|
||||||
|
API *APIConf `yaml:"api,omitempty"`
|
||||||
|
DNSMASQ *DNSMASQConf `yaml:"dhcp,omitempty"`
|
||||||
|
DHCP *DHCPConf `yaml:"dhcp,omitempty"`
|
||||||
|
TFTP *TFTPConf `yaml:"tftp,omitempty"`
|
||||||
|
NFS *NFSConf `yaml:"nfs,omitempty"`
|
||||||
|
SSH *SSHConf `yaml:"ssh,omitempty"`
|
||||||
|
MountsImage []*MountEntry `yaml:"image mounts,omitempty" default:"[{\"source\": \"/etc/resolv.conf\", \"dest\": \"/etc/resolv.conf\"}]"`
|
||||||
|
Paths *BuildConfig `yaml:"paths,omitempty"`
|
||||||
|
//Client *SPClientConf `yaml:"client,omitempty"`
|
||||||
|
|
||||||
|
sunhpcconf string
|
||||||
|
autodetected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// New caches and returns a new [SunhpcYaml] initialized with empty
|
||||||
|
// values, clearing replacing any previously cached value.
|
||||||
|
func New() *SunhpcYaml {
|
||||||
|
cachedConf = SunhpcYaml{}
|
||||||
|
cachedConf.sunhpcconf = ""
|
||||||
|
|
||||||
|
cachedConf.Sunhpc = new(SunhpcConf)
|
||||||
|
cachedConf.DNSMASQ = new(DNSMASQConf)
|
||||||
|
cachedConf.Paths = new(BuildConfig)
|
||||||
|
cachedConf.DHCP = new(DHCPConf)
|
||||||
|
cachedConf.TFTP = new(TFTPConf)
|
||||||
|
cachedConf.NFS = new(NFSConf)
|
||||||
|
cachedConf.SSH = new(SSHConf)
|
||||||
|
cachedConf.API = new(APIConf)
|
||||||
|
if err := defaults.Set(&cachedConf); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return &cachedConf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a previously cached [SunhpcYaml] if it exists, or returns
|
||||||
|
// a new SunhpcYaml.
|
||||||
|
func Get() *SunhpcYaml {
|
||||||
|
// NOTE: This function can be called before any log level is set
|
||||||
|
// so using splog.Verbose or splog.Debug won't work
|
||||||
|
if reflect.ValueOf(cachedConf).IsZero() {
|
||||||
|
cachedConf = *New()
|
||||||
|
}
|
||||||
|
return &cachedConf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read populates [SunhpcYaml] with the values from a configuration
|
||||||
|
// file.
|
||||||
|
func (conf *SunhpcYaml) Read(confFileName string, autodetect bool) error {
|
||||||
|
splog.Debug("Reading sunhpc.conf from: %s", confFileName)
|
||||||
|
conf.sunhpcconf = confFileName
|
||||||
|
if data, err := os.ReadFile(confFileName); err != nil {
|
||||||
|
return err
|
||||||
|
} else if err := conf.Parse(data, autodetect); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse populates [SunhpcYaml] with the values from a yaml document.
|
||||||
|
func (conf *SunhpcYaml) Parse(data []byte, autodetect bool) error {
|
||||||
|
// ipxe binaries are merged not overwritten, store defaults separate
|
||||||
|
defIpxe := make(map[string]string)
|
||||||
|
for k, v := range conf.TFTP.IpxeBinaries {
|
||||||
|
defIpxe[k] = v
|
||||||
|
delete(conf.TFTP.IpxeBinaries, k)
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(data, &conf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(conf.TFTP.IpxeBinaries) == 0 {
|
||||||
|
conf.TFTP.IpxeBinaries = defIpxe
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip, network, err := net.ParseCIDR(conf.Ipaddr); err == nil {
|
||||||
|
conf.Ipaddr = ip.String()
|
||||||
|
if conf.Network == "" {
|
||||||
|
conf.Network = network.IP.String()
|
||||||
|
}
|
||||||
|
if conf.Netmask == "" {
|
||||||
|
conf.Netmask = net.IP(network.Mask).String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if autodetect {
|
||||||
|
if conf.Ipaddr == "" {
|
||||||
|
if ip := GetOutboundIP(); ip != nil {
|
||||||
|
conf.Ipaddr = ip.String()
|
||||||
|
conf.autodetected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Netmask == "" {
|
||||||
|
if ip := net.ParseIP(conf.Ipaddr); ip != nil {
|
||||||
|
if network, err := GetIPNetForIP(ip); err == nil {
|
||||||
|
conf.Netmask = net.IP(network.Mask).String()
|
||||||
|
conf.autodetected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Network == "" {
|
||||||
|
if ip := net.ParseIP(conf.Ipaddr); ip != nil {
|
||||||
|
if mask := net.IPMask(net.ParseIP(conf.Netmask)); mask != nil {
|
||||||
|
conf.Network = ip.Mask(mask).String()
|
||||||
|
conf.autodetected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := net.ParseIP(conf.Ipaddr6); ip != nil {
|
||||||
|
if prefix, err := strconv.Atoi(conf.PrefixLen6); err == nil {
|
||||||
|
conf.Ipaddr6 = ip.String()
|
||||||
|
conf.PrefixLen6 = strconv.Itoa(prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip, net, err := net.ParseCIDR(conf.Ipaddr6); err == nil {
|
||||||
|
conf.Ipaddr6 = ip.String()
|
||||||
|
prefixLen6, _ := net.Mask.Size()
|
||||||
|
conf.PrefixLen6 = strconv.Itoa(prefixLen6)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *SunhpcYaml) Network6() string {
|
||||||
|
cidr := fmt.Sprintf("%s/%s", config.Ipaddr6, config.PrefixLen6)
|
||||||
|
_, ipnet, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ipnet.IP.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *SunhpcYaml) NetworkCIDR() string {
|
||||||
|
if config.Network == "" || config.Netmask == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
cidr := net.IPNet{
|
||||||
|
IP: net.ParseIP(config.Network),
|
||||||
|
Mask: net.IPMask(net.ParseIP(config.Netmask)),
|
||||||
|
}
|
||||||
|
if cidr.IP == nil || cidr.Mask == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return cidr.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *SunhpcYaml) NetworkCIDR6() string {
|
||||||
|
cidr := fmt.Sprintf("%s/%s", config.Ipaddr6, config.PrefixLen6)
|
||||||
|
_, ipnet, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ipnet.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *SunhpcYaml) IpCIDR() string {
|
||||||
|
if config.Ipaddr == "" || config.Netmask == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
cidr := net.IPNet{
|
||||||
|
IP: net.ParseIP(config.Ipaddr),
|
||||||
|
Mask: net.IPMask(net.ParseIP(config.Netmask)),
|
||||||
|
}
|
||||||
|
if cidr.IP == nil || cidr.Mask == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return cidr.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *SunhpcYaml) IpCIDR6() string {
|
||||||
|
cidr := fmt.Sprintf("%s/%s", config.Ipaddr6, config.PrefixLen6)
|
||||||
|
ip, _, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil || ip == nil || ip.To4() != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return cidr
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitializedFromFile returns true if [SunhpcYaml] memory was read from
|
||||||
|
// a file, or false otherwise.
|
||||||
|
func (conf *SunhpcYaml) InitializedFromFile() bool {
|
||||||
|
return conf.sunhpcconf != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf *SunhpcYaml) GetSunhpcConf() string {
|
||||||
|
return conf.sunhpcconf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conf *SunhpcYaml) Autodetected() bool {
|
||||||
|
return conf.autodetected
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *SunhpcYaml) Dump() ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
yamlEncoder := yaml.NewEncoder(&buf)
|
||||||
|
yamlEncoder.SetIndent(2)
|
||||||
|
err := yamlEncoder.Encode(config)
|
||||||
|
return buf.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *SunhpcYaml) PersistToFile(configFile string) (err error) {
|
||||||
|
out, dumpErr := config.Dump()
|
||||||
|
if dumpErr != nil {
|
||||||
|
splog.Error("%s", dumpErr)
|
||||||
|
return dumpErr
|
||||||
|
}
|
||||||
|
file, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
splog.Error("%s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := file.Close(); cerr != nil && err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, err = file.WriteString(string(out))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
splog.Debug("persisted: %s", configFile)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
5
internal/pkg/config/ssh.go
Normal file
5
internal/pkg/config/ssh.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type SSHConf struct {
|
||||||
|
KeyTypes []string `yaml:"key types,omitempty" default:"[\"ed25519\",\"ecdsa\",\"rsa\",\"dsa\"]"`
|
||||||
|
}
|
||||||
58
internal/pkg/config/util.go
Normal file
58
internal/pkg/config/util.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetOutboundIP() net.IP {
|
||||||
|
conn, err := net.Dial("udp", "192.0.2.1:80")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer func() { _ = conn.Close() }()
|
||||||
|
localAddr := conn.LocalAddr().(*net.UDPAddr)
|
||||||
|
return localAddr.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIPNetForIP(ip net.IP) (*net.IPNet, error) {
|
||||||
|
interfaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get network interfaces: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
// skip interfaces that are down or not relevant
|
||||||
|
if iface.Flags&net.FlagUp == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
continue // try next interface
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
// We expect addr to be of type *net.IPNet.
|
||||||
|
var ipNet *net.IPNet
|
||||||
|
switch v := addr.(type) {
|
||||||
|
case *net.IPNet:
|
||||||
|
ipNet = v
|
||||||
|
case *net.IPAddr:
|
||||||
|
// Wrap net.IPAddr in an IPNet with its default mask.
|
||||||
|
ipNet = &net.IPNet{IP: v.IP, Mask: v.IP.DefaultMask()}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipNet == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the IP matches (for IPv4, Equal works well)
|
||||||
|
if ipNet.IP.Equal(ip) {
|
||||||
|
return ipNet, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("could not find IPNet for IP %v", ip)
|
||||||
|
}
|
||||||
22
internal/pkg/configure/dhcp.go
Normal file
22
internal/pkg/configure/dhcp.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package configure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sunhpccconf "gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/config"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/splog"
|
||||||
|
//"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func DHCP() (err error) {
|
||||||
|
controller := sunhpccconf.Get()
|
||||||
|
|
||||||
|
if !controller.DHCP.Enabled() {
|
||||||
|
splog.Warn("This system is not configured as a Sunhpc DHCP controller.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Enabling and restarting the DHCP services\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
22
internal/pkg/configure/sunhpcd.go
Normal file
22
internal/pkg/configure/sunhpcd.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package configure
|
||||||
|
|
||||||
|
import (
|
||||||
|
sunhpcconf "gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/config"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/util"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/splog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SUNHPCD() (err error) {
|
||||||
|
controller := sunhpcconf.Get()
|
||||||
|
if controller.Sunhpc.SystemdName != "" {
|
||||||
|
splog.Info("Enabling and restarting the Sunhpc server")
|
||||||
|
err = util.SystemdStart(controller.Sunhpc.SystemdName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
splog.Warn("Not (re)starting Sunhpc server: no systemd name configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
118
internal/pkg/configure/tls.go
Normal file
118
internal/pkg/configure/tls.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package configure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
sunhpcconf "gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/config"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/util"
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/splog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TLS ensures TLS keys exist if TLS is enabled.
|
||||||
|
// If force is true, regenerates even if keys already exist.
|
||||||
|
// Returns true if new keys were generated.
|
||||||
|
func TLS(force bool) (bool, error) {
|
||||||
|
conf := sunhpcconf.Get()
|
||||||
|
if !conf.Sunhpc.TLSEnabled() {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
keystore := path.Join(conf.Paths.Sysconfdir, "sunhpc", "tls")
|
||||||
|
keyFile := path.Join(keystore, "sunhpc.key")
|
||||||
|
certFile := path.Join(keystore, "sunhpc.crt")
|
||||||
|
|
||||||
|
if !force && util.IsFile(keyFile) && util.IsFile(certFile) {
|
||||||
|
splog.Info("TLS keys already exist in %s", keystore)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := GenTLSKeys(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
splog.Info("TLS keys generated in %s", keystore)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenTLSKeys generates new TLS keys and certificate unconditionally.
|
||||||
|
func GenTLSKeys() error {
|
||||||
|
conf := sunhpcconf.Get()
|
||||||
|
keystore := path.Join(conf.Paths.Sysconfdir, "sunhpc", "tls")
|
||||||
|
|
||||||
|
if err := os.MkdirAll(keystore, 0755); err != nil {
|
||||||
|
return fmt.Errorf("could not create keystore directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyFile := path.Join(keystore, "sunhpc.key")
|
||||||
|
certFile := path.Join(keystore, "sunhpc.crt")
|
||||||
|
splog.Verbose("Generating new x509 keys in %s", keystore)
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate rsa key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate serial number: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: "Sunhpc Server",
|
||||||
|
Organization: []string{"Sunhpc"},
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(time.Hour * 24 * 365 * 10),
|
||||||
|
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := net.ParseIP(conf.Ipaddr); ip != nil {
|
||||||
|
template.IPAddresses = append(template.IPAddresses, ip)
|
||||||
|
}
|
||||||
|
if ip := net.ParseIP(conf.Ipaddr6); ip != nil {
|
||||||
|
template.IPAddresses = append(template.IPAddresses, ip)
|
||||||
|
}
|
||||||
|
if conf.Fqdn != "" {
|
||||||
|
template.DNSNames = append(template.DNSNames, conf.Fqdn)
|
||||||
|
}
|
||||||
|
template.DNSNames = append(template.DNSNames, "sunhpc")
|
||||||
|
|
||||||
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open key file for writing: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = keyOut.Close() }()
|
||||||
|
if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
|
||||||
|
return fmt.Errorf("failed to write data to key file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certOut, err := os.OpenFile(certFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open cert file for writing: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = certOut.Close() }()
|
||||||
|
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||||
|
return fmt.Errorf("failed to write data to cert file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package wwlog
|
package splog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|||||||
464
internal/pkg/util/util.go
Normal file
464
internal/pkg/util/util.go
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
//"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
//"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"gitea.sunhpc.com/kelvin/sunhpc/internal/pkg/splog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsDir(path string) bool {
|
||||||
|
splog.Debug("Checking if path exists as a directory: %s", path)
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if stat, err := os.Stat(path); err == nil && stat.IsDir() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsFile(path string) bool {
|
||||||
|
splog.Debug("Checking if path exists as a file: %s", path)
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat, err := os.Lstat(path); err == nil && !stat.IsDir() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadFile(path string) ([]string, error) {
|
||||||
|
lines := []string{}
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
lines = append(lines, scanner.Text())
|
||||||
|
}
|
||||||
|
_ = f.Close()
|
||||||
|
return lines, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidString(pattern string, s string) bool {
|
||||||
|
if b, _ := regexp.MatchString(pattern, s); b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindFiles(path string) []string {
|
||||||
|
var ret []string
|
||||||
|
|
||||||
|
splog.Debug("Finding files in path: %s", path)
|
||||||
|
|
||||||
|
err := filepath.Walk(path, func(location string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
splog.Warn("Error walking path %s: %v", location, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the relative path from the base directory
|
||||||
|
relPath, relErr := filepath.Rel(path, location)
|
||||||
|
if relErr != nil {
|
||||||
|
splog.Warn("Error computing relative path for %s: %v", location, relErr)
|
||||||
|
return relErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if relPath == "." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
splog.Debug("FindFiles() found directory: %s", relPath)
|
||||||
|
ret = append(ret, relPath+"/")
|
||||||
|
} else {
|
||||||
|
splog.Debug("FindFiles() found file: %s", relPath)
|
||||||
|
ret = append(ret, relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
splog.Warn("Error during file walk: %v", err)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Finds all files under a given directory with tar like include and ignore patterns.
|
||||||
|
/foo/*
|
||||||
|
will match /foo/baar/ and /foo/baar/sibling
|
||||||
|
*/
|
||||||
|
func FindFilterFiles(
|
||||||
|
path string,
|
||||||
|
includePattern []string,
|
||||||
|
ignorePattern []string,
|
||||||
|
ignore_xdev bool,
|
||||||
|
) (ofiles []string, err error) {
|
||||||
|
splog.Debug("Finding files: %s include: %s ignore: %s", path, includePattern, ignorePattern)
|
||||||
|
|
||||||
|
// Preprocess patterns to remove leading (and trailing) /, as we are handling relative paths
|
||||||
|
for i, pattern := range ignorePattern {
|
||||||
|
ignorePattern[i] = strings.Trim(pattern, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the base path to an absolute path
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return ofiles, fmt.Errorf("failed to resolve absolute path: %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand the include list
|
||||||
|
var globedInclude []string
|
||||||
|
for _, include := range includePattern {
|
||||||
|
globed, err := filepath.Glob(filepath.Join(absPath, include))
|
||||||
|
if err != nil {
|
||||||
|
return ofiles, fmt.Errorf("failed to glob pattern %s: %w", include, err)
|
||||||
|
}
|
||||||
|
globedInclude = append(globedInclude, globed...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dev uint64
|
||||||
|
if ignore_xdev {
|
||||||
|
splog.Debug("Ignoring cross-device (xdev) files")
|
||||||
|
pathStat, err := os.Stat(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return ofiles, fmt.Errorf("failed to stat base path: %s: %w", absPath, err)
|
||||||
|
}
|
||||||
|
dev = pathStat.Sys().(*syscall.Stat_t).Dev
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, inc := range globedInclude {
|
||||||
|
splog.Debug("Processing include pattern: %s", inc)
|
||||||
|
stat, err := os.Lstat(inc)
|
||||||
|
if err != nil {
|
||||||
|
return ofiles, fmt.Errorf("failed to stat include: %s: %w", inc, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat.IsDir() {
|
||||||
|
// Walk the directory
|
||||||
|
err = filepath.WalkDir(inc, func(location string, info fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, relErr := filepath.Rel(absPath, location)
|
||||||
|
if relErr != nil {
|
||||||
|
splog.Warn("Error computing relative path for %s: %v", location, relErr)
|
||||||
|
return relErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if relPath == "." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fsInfo, err := info.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ignore_xdev && fsInfo.Sys().(*syscall.Stat_t).Dev != dev {
|
||||||
|
splog.Debug("Ignored (cross-device): %s", location)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ignoredPattern := range ignorePattern {
|
||||||
|
if ignored, _ := filepath.Match(ignoredPattern, relPath); ignored {
|
||||||
|
splog.Debug("Ignored %s due to pattern %s", relPath, ignoredPattern)
|
||||||
|
if info.IsDir() {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ofiles = append(ofiles, relPath)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ofiles, fmt.Errorf("error walking directory %s: %w", inc, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add the file directly
|
||||||
|
relPath, relErr := filepath.Rel(absPath, inc)
|
||||||
|
if relErr != nil {
|
||||||
|
splog.Warn("Error computing relative path for %s: %v", inc, relErr)
|
||||||
|
return ofiles, relErr
|
||||||
|
}
|
||||||
|
ofiles = append(ofiles, relPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ofiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ******************************************************************************
|
||||||
|
func ExecInteractive(command string, a ...string) error {
|
||||||
|
splog.Debug("ExecInteractive(%s, %s)", command, a)
|
||||||
|
c := exec.Command(command, a...)
|
||||||
|
c.Stdin = os.Stdin
|
||||||
|
c.Stdout = os.Stdout
|
||||||
|
c.Stderr = os.Stderr
|
||||||
|
err := c.Run()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SystemdStart(systemdName string) error {
|
||||||
|
startCmd := fmt.Sprintf("systemctl restart %s", systemdName)
|
||||||
|
enableCmd := fmt.Sprintf("systemctl enable %s", systemdName)
|
||||||
|
|
||||||
|
splog.Debug("Setting up Systemd service: %s", systemdName)
|
||||||
|
err := ExecInteractive("/bin/sh", "-c", startCmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run start cmd: %w", err)
|
||||||
|
}
|
||||||
|
err = ExecInteractive("/bin/sh", "-c", enableCmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run enable cmd: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyUIDGID(source string, dest string) error {
|
||||||
|
info, err := os.Stat(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// root is always good, if we failt to get UID/GID of a file
|
||||||
|
var UID = 0
|
||||||
|
var GID = 0
|
||||||
|
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||||
|
UID = int(stat.Uid)
|
||||||
|
GID = int(stat.Gid)
|
||||||
|
}
|
||||||
|
splog.Debug("Chown %d:%d '%s'", UID, GID, dest)
|
||||||
|
err = os.Chown(dest, UID, GID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func IncrementIPv4(start net.IP, inc uint) net.IP {
|
||||||
|
ipv4 := start.To4()
|
||||||
|
v4_int := uint(ipv4[0])<<24 + uint(ipv4[1])<<16 + uint(ipv4[2])<<8 + uint(ipv4[3])
|
||||||
|
v4_int += inc
|
||||||
|
v4_o3 := byte(v4_int & 0xFF)
|
||||||
|
v4_o2 := byte((v4_int >> 8) & 0xFF)
|
||||||
|
v4_o1 := byte((v4_int >> 16) & 0xFF)
|
||||||
|
v4_o0 := byte((v4_int >> 24) & 0xFF)
|
||||||
|
ipv4_new := net.IPv4(v4_o0, v4_o1, v4_o2, v4_o3)
|
||||||
|
return ipv4_new
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Appending the lines to the given file
|
||||||
|
*/
|
||||||
|
func AppendLines(fileName string, lines []string) (err error) {
|
||||||
|
splog.Verbose("appending %v lines to %s", len(lines), fileName)
|
||||||
|
file, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't open file: %s: %w", fileName, err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := file.Close(); cerr != nil && err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for _, line := range lines {
|
||||||
|
splog.Debug("Appending '%s' to %s", line, fileName)
|
||||||
|
if _, err := fmt.Fprintf(file, "%s\n", line); err != nil {
|
||||||
|
return fmt.Errorf("can't write to file: %s: %w", fileName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
******************************************************************************
|
||||||
|
|
||||||
|
Compress a file using gzip or pigz
|
||||||
|
*/
|
||||||
|
func FileGz(
|
||||||
|
file string,
|
||||||
|
) (err error) {
|
||||||
|
file_gz := file + ".gz"
|
||||||
|
|
||||||
|
if IsFile(file_gz) {
|
||||||
|
err := os.Remove(file_gz)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not remove existing file: %s: %w", file_gz, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compressor, err := exec.LookPath("pigz")
|
||||||
|
if err != nil {
|
||||||
|
splog.Verbose("Could not locate PIGZ")
|
||||||
|
compressor, err = exec.LookPath("gzip")
|
||||||
|
if err != nil {
|
||||||
|
splog.Verbose("Could not locate GZIP")
|
||||||
|
return fmt.Errorf("no compressor program for image file: %s: %w", file_gz, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
splog.Verbose("Using compressor program: %s", compressor)
|
||||||
|
|
||||||
|
proc := exec.Command(
|
||||||
|
compressor,
|
||||||
|
"--keep",
|
||||||
|
file)
|
||||||
|
|
||||||
|
out, err := proc.CombinedOutput()
|
||||||
|
if len(out) > 0 {
|
||||||
|
outStr := string(out[:])
|
||||||
|
if err != nil && strings.HasSuffix(compressor, "gzip") && strings.Contains(outStr, "unrecognized option") {
|
||||||
|
var gzippedFile *os.File
|
||||||
|
var gzipStderr io.ReadCloser
|
||||||
|
|
||||||
|
/* Older version of gzip, try it another way: */
|
||||||
|
splog.Verbose("%s does not recognize the --keep flag, trying redirected stdout", compressor)
|
||||||
|
|
||||||
|
/* Open the output file for writing: */
|
||||||
|
gzippedFile, err = os.Create(file_gz)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to open compressed image file for writing: %s: %w", file_gz, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We'll execute gzip with output to stdout and attach stdout to the compressed file we just
|
||||||
|
created:
|
||||||
|
*/
|
||||||
|
proc = exec.Command(
|
||||||
|
compressor,
|
||||||
|
"--stdout",
|
||||||
|
file)
|
||||||
|
proc.Stdout = gzippedFile
|
||||||
|
gzipStderr, err = proc.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to open stderr pipe for compression program: %s: %w", compressor, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Execute the command: */
|
||||||
|
err = proc.Start()
|
||||||
|
if err != nil {
|
||||||
|
_ = proc.Wait()
|
||||||
|
_ = gzippedFile.Close()
|
||||||
|
_ = os.Remove(file_gz)
|
||||||
|
err = fmt.Errorf("unable to successfully execute compression program: %s: %w", compressor, err)
|
||||||
|
} else {
|
||||||
|
err = proc.Wait()
|
||||||
|
if cerr := gzippedFile.Close(); cerr != nil {
|
||||||
|
splog.Warn("failed to close compressed image file %s: %s", file_gz, cerr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
_ = os.Remove(file_gz)
|
||||||
|
err = fmt.Errorf("unable to successfully create compressed image file: %s: %w", file_gz, err)
|
||||||
|
} else {
|
||||||
|
splog.Verbose("Successfully compressed image file: %s", file_gz)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out, _ = io.ReadAll(gzipStderr)
|
||||||
|
}
|
||||||
|
splog.Debug(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get size of given directory in bytes
|
||||||
|
*/
|
||||||
|
func DirSize(path string) (int64, error) {
|
||||||
|
var size int64
|
||||||
|
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
size += info.Size()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return size, err
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convert bytes to human friendly format
|
||||||
|
*/
|
||||||
|
func ByteToString(b int64) string {
|
||||||
|
const base = 1024
|
||||||
|
if b < base {
|
||||||
|
return fmt.Sprintf("%d B", b)
|
||||||
|
}
|
||||||
|
div, exp := int64(base), 0
|
||||||
|
for n := b / base; n >= base; n /= base {
|
||||||
|
div *= base
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
|
|
||||||
|
func HashFile(file *os.File) (string, error) {
|
||||||
|
if prevOffset, err := file.Seek(0, 0); err != nil {
|
||||||
|
return "", err
|
||||||
|
} else {
|
||||||
|
hasher := sha256.New()
|
||||||
|
if _, err := io.Copy(hasher, file); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if _, err := file.Seek(prevOffset, 0); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(hasher.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeYaml(data interface{}) ([]byte, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
encoder := yaml.NewEncoder(buf)
|
||||||
|
encoder.SetIndent(2)
|
||||||
|
err := encoder.Encode(data)
|
||||||
|
return buf.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func EqualYaml(a interface{}, b interface{}) (bool, error) {
|
||||||
|
aYaml, err := EncodeYaml(a)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bYaml, err := EncodeYaml(b)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.Equal(aYaml, bYaml), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BoolP(p *bool) bool {
|
||||||
|
return p != nil && *p
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user