Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
484facd
feat(quackstore): implement QuackStore for persistent storage with tests
alexlovelltroy Mar 19, 2025
ac5e973
feat(bss): implement BootParams and boot script generation with HTTP …
alexlovelltroy Apr 18, 2025
74a8967
feat(bss): Add the capability for cloud-init to act as BSS
alexlovelltroy Apr 23, 2025
47b93c2
feat(quackstore): implement QuackStore for persistent storage with tests
alexlovelltroy Mar 19, 2025
cf6de8d
test(memstore): add unit tests for MemStore implementation
alexlovelltroy Mar 19, 2025
7c48b3a
feat(cloud-init-server): add support for multiple storage backends
alexlovelltroy Mar 19, 2025
aee755f
fix: correct UnmarshalJSON and add MarshalJSON for CloudConfigFile
synackd Mar 19, 2025
714e64b
chore: clarify comments in custom marshal/unmarshal funcs
synackd Mar 20, 2025
7a42fd5
test: add unit tests for CloudConfigFile JSON marshal/unmarshal
alexlovelltroy Mar 24, 2025
6232a2a
fix(tests): use base64-encoded data for base64 marshal test
synackd Mar 24, 2025
8e33ba5
refactor: Use an alternate Unmarshal implementation
alexlovelltroy Mar 24, 2025
80068be
chore: add base64 encoding import for future functionality
alexlovelltroy Apr 18, 2025
f85096b
refactor: simplify UnmarshalJSON and add MarshalJSON for CloudConfigFile
alexlovelltroy Apr 18, 2025
b1769db
feat(quackstore): implement unique instance ID generation
alexlovelltroy Apr 23, 2025
0ff3970
fix: correct UnmarshalJSON and add MarshalJSON for CloudConfigFile
synackd Mar 19, 2025
ab1e3d7
chore: clarify comments in custom marshal/unmarshal funcs
synackd Mar 20, 2025
99b3731
feat(quackstore): implement QuackStore for persistent storage with tests
alexlovelltroy Mar 19, 2025
20764e5
Merging main into bss branch
alexlovelltroy Apr 23, 2025
9cedbd4
feat(bss): add BSS API endpoints for boot parameters and scripts
alexlovelltroy Apr 24, 2025
853996a
Merge branch 'main' into alovelltroy/bss-in-cloud-init
alexlovelltroy May 1, 2025
3dbbefb
feat: Add netboot server implementation and API endpoints for boot pa…
alexlovelltroy May 2, 2025
1ee56ff
feat(bss): add CmdLine field to BootParams for kernel command line co…
alexlovelltroy May 19, 2025
8c4c2b8
feat: enhance netboot server with versioning and QuackStore integration
alexlovelltroy May 21, 2025
ba49812
fix: update archive formats in GoReleaser configuration and modify sn…
alexlovelltroy May 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,34 @@ builds:
goamd64:
- v3
env:
- CGO_ENABLED=0
- CGO_ENABLED=1
- id: netboot
main: ./cmd/netboot-server
binary: netboot-server
# export GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi)
# export BUILD_HOST=$(hostname)
# export GO_VERSION=$(go version | awk '{print $3}')
# export BUILD_USER=$(whoami)
ldflags:
- "-X 'main.GitCommit={{.Commit}}' \
-X 'main.BuildTime={{.Timestamp}}' \
-X 'main.Version={{.Version}}' \
-X 'main.GitBranch={{.Branch}}' \
-X 'main.GitTag={{.Tag}}' \
-X 'main.GitState={{ .Env.GIT_STATE }}' \
-X 'main.BuildHost={{ .Env.BUILD_HOST }}' \
-X 'main.GoVersion={{ .Env.GO_VERSION }}' \
-X 'main.BuildUser={{ .Env.BUILD_USER }}'"
goos:
- linux
- darwin
goarch:
- amd64
- arm64
goamd64:
- v3
env:
- CGO_ENABLED=1

dockers:
- image_templates:
Expand Down Expand Up @@ -96,7 +123,7 @@ docker_manifests:
- *arm64v8_linux_image

archives:
- format: tar.gz
- format : "tar.gz"
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_
Expand All @@ -115,6 +142,7 @@ checksum:
name_template: 'checksums.txt'
snapshot:
version_template: "{{ incpatch .Version }}-next"

changelog:
sort: asc
filters:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export BUILD_USER=$(whoami)
2. Run in snapshot mode to build locally without releasing:

```bash
goreleaser release --snapshot --clean
goreleaser release --snapshot --clean --single-target
```
3. Check the `dist/` directory for compiled binaries, which will include the injected metadata.

Expand Down
2 changes: 1 addition & 1 deletion cmd/cloud-init-server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (

// Import to run swag.Register() to generated docs
_ "github.com/OpenCHAMI/cloud-init/docs"
"github.com/OpenCHAMI/cloud-init/internal/smdclient"
"github.com/OpenCHAMI/cloud-init/pkg/cistore"
"github.com/OpenCHAMI/cloud-init/pkg/smdclient"
"github.com/OpenCHAMI/cloud-init/pkg/wgtunnel"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
Expand Down
30 changes: 26 additions & 4 deletions cmd/cloud-init-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import (
"time"

"github.com/OpenCHAMI/cloud-init/internal/memstore"
"github.com/OpenCHAMI/cloud-init/internal/smdclient"
"github.com/OpenCHAMI/cloud-init/internal/quackstore"
"github.com/OpenCHAMI/cloud-init/pkg/cistore"
"github.com/OpenCHAMI/cloud-init/pkg/smdclient"
"github.com/OpenCHAMI/cloud-init/pkg/wgtunnel"
"github.com/OpenCHAMI/jwtauth/v5"
"github.com/go-chi/chi/v5"
Expand Down Expand Up @@ -50,6 +51,8 @@ var (
wireguardOnly = false
debug = true
wireGuardMiddleware func(http.Handler) http.Handler
storageBackend = "mem" // Default to memstore
dbPath = "cloud-init.db" // Default database path for quackstore
)

func main() {
Expand All @@ -69,6 +72,8 @@ func main() {
flag.StringVar(&wireguardServer, "wireguard-server", wireguardServer, "Wireguard server IP address and network (e.g. 100.97.0.1/16)")
flag.BoolVar(&wireguardOnly, "wireguard-only", wireguardOnly, "Only allow access to the cloud-init functions from the WireGuard subnet")
flag.BoolVar(&debug, "debug", debug, "Enable debug logging")
flag.StringVar(&storageBackend, "storage-backend", storageBackend, "Storage backend to use (mem or quack)")
flag.StringVar(&dbPath, "db-path", dbPath, "Path to the database file for quackstore backend")
flag.Parse()

if debug {
Expand Down Expand Up @@ -115,15 +120,32 @@ func main() {
}
}

// Set up our DataStore
store = memstore.NewMemStore()
store.SetClusterDefaults(cistore.ClusterDefaults{
// Set up our DataStore based on the selected backend
var err error
switch storageBackend {
case "mem":
store = memstore.NewMemStore()
case "quack":
store, err = quackstore.NewQuackStore(dbPath)
if err != nil {
log.Fatal().Err(err).Msg("Failed to initialize QuackStore")
}
defer store.(*quackstore.QuackStore).Close()
default:
log.Fatal().Msgf("Unsupported storage backend: %s", storageBackend)
}

// Set cluster defaults
err = store.SetClusterDefaults(cistore.ClusterDefaults{
ClusterName: clusterName,
Region: region,
AvailabilityZone: availabilityZone,
CloudProvider: cloudProvider,
BaseUrl: baseUrl,
})
if err != nil {
log.Fatal().Err(err).Msg("Failed to set cluster defaults")
}

// Initialize the cloud-init handler with the datastore and SMD client
ciHandler := NewCiHandler(store, sm, clusterName)
Expand Down
2 changes: 1 addition & 1 deletion cmd/cloud-init-server/metadata_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"net/http"
"strings"

"github.com/OpenCHAMI/cloud-init/internal/smdclient"
"github.com/OpenCHAMI/cloud-init/pkg/cistore"
"github.com/OpenCHAMI/cloud-init/pkg/smdclient"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
yaml "gopkg.in/yaml.v2"
Expand Down
2 changes: 1 addition & 1 deletion cmd/cloud-init-server/userdata_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package main
import (
"net/http"

"github.com/OpenCHAMI/cloud-init/internal/smdclient"
"github.com/OpenCHAMI/cloud-init/pkg/cistore"
"github.com/OpenCHAMI/cloud-init/pkg/smdclient"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/cloud-init-server/vendordata_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"fmt"
"net/http"

"github.com/OpenCHAMI/cloud-init/internal/smdclient"
"github.com/OpenCHAMI/cloud-init/pkg/cistore"
"github.com/OpenCHAMI/cloud-init/pkg/smdclient"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
)
Expand Down
184 changes: 184 additions & 0 deletions cmd/netboot-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package main

import (
"errors"
"fmt"
"net/http"
"os"
"time"

"github.com/OpenCHAMI/cloud-init/pkg/bss"
"github.com/OpenCHAMI/cloud-init/pkg/smdclient"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
openchami_logger "github.com/openchami/chi-middleware/log"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/pkgerrors"

"github.com/spf13/cobra"
)

var (
clusterName string
jwksUrl string
smdEndpoint string
tokenEndpoint string
accessToken string
certPath string
insecure bool
fakeSMDEnabled bool
storageBackend string
dbPath string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should look at turning the server into a struct that main() instantiates from the args rather than using package vars.

For dbPath in particular, that struct should have a quackConfig field, whose value is a struct with Quack-specific parameters.

)

func main() {
// Create root command
rootCmd := &cobra.Command{
Use: "netboot-server",
Short: "OpenCHAMI netboot server",
Long: `OpenCHAMI netboot server provides boot parameter management and boot script generation.
It supports both in-memory and persistent storage backends, and can integrate with SMD for inventory details.`,
}

// Create serve command
serveCmd := &cobra.Command{
Use: "serve",
Short: "Start the netboot server",
Long: `Start the netboot server with the specified configuration.`,
PreRunE: func(cmd *cobra.Command, args []string) error {
if smdEndpoint == "" && !fakeSMDEnabled {
return errors.New("either --smd-endpoint or --fake-smd must be set for inventory details")
}
if clusterName == "" {
return errors.New("cluster name must be set via --cluster-name or CLUSTER_NAME")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return startServer()
},
}

// Add flags to serve command
serveCmd.Flags().StringVar(&clusterName, "cluster-name", os.Getenv("CLUSTER_NAME"), "Cluster name for SMD (default from CLUSTER_NAME env)")
serveCmd.Flags().StringVar(&jwksUrl, "jwks-url", os.Getenv("JWKS_URL"), "JWKS URL for JWT validation (default from JWKS_URL env)")
serveCmd.Flags().StringVar(&smdEndpoint, "smd-endpoint", os.Getenv("SMD_ENDPOINT"), "SMD endpoint (default from SMD_ENDPOINT env)")
serveCmd.Flags().StringVar(&tokenEndpoint, "token-endpoint", os.Getenv("TOKEN_ENDPOINT"), "Token endpoint (default from TOKEN_ENDPOINT env)")
serveCmd.Flags().StringVar(&accessToken, "access-token", os.Getenv("ACCESS_TOKEN"), "Access token (default from ACCESS_TOKEN env)")
serveCmd.Flags().StringVar(&certPath, "cert-path", os.Getenv("CERT_PATH"), "Path to certificate for secure connections (default from CERT_PATH env)")
serveCmd.Flags().BoolVar(&insecure, "insecure", os.Getenv("INSECURE") == "true", "Disable TLS verification (default from INSECURE env)")
serveCmd.Flags().BoolVar(&fakeSMDEnabled, "fake-smd", os.Getenv("CLOUD_INIT_SMD_SIMULATOR") == "true", "Enable FakeSMDClient simulation (default from CLOUD_INIT_SMD_SIMULATOR env)")
serveCmd.Flags().StringVar(&storageBackend, "storage-backend", "mem", "Storage backend to use (mem or quack)")
serveCmd.Flags().StringVar(&dbPath, "db-path", "netboot.db", "Path to the database file for quackstore backend")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, storage backend-specific flags should use some common prefix to scope them, so quack-db-path probably.


// Create version command
versionCmd := &cobra.Command{
Use: "version",
Short: "Print version information",
Run: func(cmd *cobra.Command, args []string) {
PrintVersionInfo()
},
}

// Add commands to root
rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(versionCmd)

// Execute root command
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

// startServer encapsulates the existing main logic
func startServer() error {
// Setup logger
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})

// Create SMD client
var sm smdclient.SMDClientInterface
if fakeSMDEnabled {
fmt.Printf("\n\n**********\n\n\tCLOUD_INIT_SMD_SIMULATOR is set to true.\n\tUsing the FakeSMDClient\n\n**********\n\n\n")
fakeSm := smdclient.NewFakeSMDClient(clusterName, 500)
fakeSm.Summary()
sm = fakeSm
} else {
var err error
sm, err = smdclient.NewSMDClient(clusterName, smdEndpoint, tokenEndpoint, accessToken, certPath, insecure)
if err != nil {
log.Fatal().Err(err)
}
}

// Initialize BSS storage based on the selected backend
var bssStorage bss.Store
var err error
switch storageBackend {
case "mem":
bssStorage = bss.NewMemoryStore()
case "quack":
bssStorage, err = bss.NewQuackStore(dbPath)
if err != nil {
log.Fatal().Err(err).Msg("Failed to initialize QuackStore")
}
defer bssStorage.(*bss.QuackStore).Close()
default:
log.Fatal().Msgf("Unsupported storage backend: %s", storageBackend)
}

// Set up router
router := chi.NewRouter()
router.Use(
middleware.RequestID,
middleware.RealIP,
middleware.Logger,
middleware.Recoverer,
middleware.StripSlashes,
middleware.Timeout(60*time.Second),
openchami_logger.OpenCHAMILogger(log.Logger),
)

initBssRouter(router, sm, bssStorage)

// Start the HTTP server
log.Info().
Str("cluster", clusterName).
Str("smdEndpoint", smdEndpoint).
Str("fakeSMDEnabled", fmt.Sprintf("%t", fakeSMDEnabled)).
Str("tokenEndpoint", tokenEndpoint).
Str("jwksUrl", jwksUrl).
Str("accessToken", accessToken).
Str("certPath", certPath).
Bool("insecure", insecure).
Str("storageBackend", storageBackend).
Str("dbPath", dbPath).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this to leave room for other stores, we could either:

  • Add a func (s *Store) Describe() string "identify yourself" method to bss.Store, and then have Str("storage", bssStorage.Describe())
  • Add a log argument to NewQuackStore() and have it log whatever setup info it chooses.

Str("version", Version).
Str("commit", GitCommit).
Msgf("Starting netboot server on port 8080")

if err := http.ListenAndServe(":8080", router); err != nil {
log.Fatal().Msgf("Failed to start server: %v", err)
}
return nil
}

// initBssRouter sets up bootparam routes
func initBssRouter(router chi.Router, sm smdclient.SMDClientInterface, bssStorage bss.Store) {
router.Get("/version", VersionHandler)
router.Route("/boot/v1/", func(r chi.Router) {
r.Post("/bootparams", bss.CreateBootParamsHandler(bssStorage))
r.Get("/bootparams/{id}", bss.GetBootParamsHandler(bssStorage))
r.Put("/bootparams/{id}", bss.UpdateBootParamsHandler(bssStorage))
r.Get("/bootparams/{id}", bss.GetBootParamsHandler(bssStorage))
r.Get("/bootscript", bss.GenerateBootScriptHandler(bssStorage, sm))
})
router.Route("/boot/v2/", func(r chi.Router) {
r.Get("/bootparams/{id}", bss.GetBootParamsHandler(bssStorage))
r.Put("/bootparams/{id}", bss.UpdateBootParamsHandler(bssStorage))
r.Get("/bootparams/{id}", bss.GetBootParamsHandler(bssStorage))
r.Get("/bootscript", bss.GenerateBootScriptHandler(bssStorage, sm))
})
}
Loading