2026-04-29
This commit is contained in:
@@ -4,17 +4,17 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gitea.sunhpc.com/kelvin/sunhpc"
|
||||
"gitea.sunhpc.com/kelvin/sunhpc/internal/app/control"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
root := sunhpc.GetRootCommand()
|
||||
root := control.GetRootCommand()
|
||||
|
||||
err := root.Execute()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
|
||||
if sunhpc.DebugFlag {
|
||||
if control.DebugFlag {
|
||||
fmt.Printf("\nSTACK TRACE: %+v\n", err)
|
||||
}
|
||||
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
|
||||
|
||||
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=
|
||||
gitea.sunhpc.com/kelvin/sunhpc v0.0.0-20260428114043-53d56ec6e2ba/go.mod h1:qQ5hwUVNDDEjeZRE35FzMpsobqly7h0tRcBGCcvzgrc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
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"
|
||||
|
||||
"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/splog"
|
||||
)
|
||||
@@ -16,18 +19,19 @@ var (
|
||||
Short: "Sunhpc Control",
|
||||
Long: `Control interface to the SunHPC Cluster Provisioning System.`,
|
||||
PersistentPreRunE: rootPersistentPreRunE,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
verboseArg bool
|
||||
DebugFlag bool
|
||||
SunhpcConfArg string
|
||||
AllowEmptyConf bool
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
verboseArg bool
|
||||
DebugFlag bool
|
||||
LogLevel int
|
||||
SunhpcConfArg string
|
||||
AllowEmptyConf bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||
//rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||
rootCmd.PersistentFlags().BoolVarP(&verboseArg, "verbose", "v", false, "verbose output")
|
||||
rootCmd.PersistentFlags().BoolVarP(&DebugFlag, "debug", "d", false, "debug output")
|
||||
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().BoolVar(&AllowEmptyConf, "emptyconf", false, "Allow empty configuration")
|
||||
_ = rootCmd.PersistentFlags().MarkHidden("emptyconf")
|
||||
|
||||
rootCmd.SetUsageTemplate(help.UsageTemplate)
|
||||
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 (
|
||||
"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