Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
7646bca
fix: commit lease rows and the MAC index together
cursoragent Oct 19, 2026
8dc1db2
fix: stop concurrent lease updates from clobbering each other
cursoragent Oct 19, 2026
6a87c67
feat: add host callback auth, overview summary, and UI mode
cursoragent Oct 19, 2026
4770d2f
fix: leave the page body class unchanged
cursoragent Oct 19, 2026
3a6c7b9
feat: report install state from the stock provision templates
cursoragent Oct 19, 2026
ad1c598
fix: set UI mode on every HTML render
cursoragent Oct 19, 2026
17af5a3
Restyle the web UI with Cozy and Console themes.
cursoragent Oct 19, 2026
d00856d
Regenerate README screenshots for the restyled pages.
cursoragent Oct 19, 2026
27c3759
Capture Console screenshots for the remaining nav pages.
cursoragent Oct 19, 2026
60cc23b
Fix the review findings that belong on the frontend.
cursoragent Oct 19, 2026
4cbeef4
fix: recover offline leases without holding the pool lock
cursoragent Oct 19, 2026
8919a21
fix: validate installer fields and stop serving configs openly
cursoragent Oct 19, 2026
8a44482
fix: tighten callback input and share pool occupancy
cursoragent Oct 19, 2026
7a1ddb1
fix: redirect HTML mode posts only to a same-origin path
cursoragent Oct 19, 2026
c94a904
Merge backend phase-1 foundations into the frontend restyle.
cursoragent Oct 19, 2026
12b14b9
Regenerate README screenshots after the backend merge.
cursoragent Oct 19, 2026
83445da
fix: activate an already-downloaded syslinux version
cursoragent Oct 19, 2026
9560cb4
fix: record TFTP running state on start and stop
cursoragent Oct 19, 2026
9c8d59d
fix: return real DHCP server list from GET /dhcp/servers
cursoragent Oct 19, 2026
4b4603a
fix: fail startup when the osimages bucket cannot be created
cursoragent Oct 19, 2026
46f9263
docs: drop /status from the documented API
cursoragent Oct 19, 2026
717c2e8
fix: verify Redfish TLS and choose the system on purpose
cursoragent Oct 19, 2026
0a55403
fix: require a per-session CSRF token on admin mutations
cursoragent Oct 19, 2026
83b0093
Style the public error page and drop unused highlighters.
cursoragent Oct 19, 2026
97aa437
fix: swap boot files when activating a syslinux version
cursoragent Oct 19, 2026
d2a2d94
fix: fail startup when preference or download buckets cannot be created
cursoragent Oct 19, 2026
4d61320
fix: remove the /status page route
cursoragent Oct 19, 2026
1996d86
fix: reject state-changing requests that omit Origin
cursoragent Oct 19, 2026
fb3f9c6
fix: reject control characters in kernel_options
cursoragent Oct 19, 2026
7d2c8d4
docs: point release docs at v3.0.0
cursoragent Oct 19, 2026
9021ae6
Align DHCP actions and keep the status bar on every page.
cursoragent Oct 19, 2026
31d4265
Merge backend phase-1 foundations at 7d2c8d4.
cursoragent Oct 19, 2026
cb615b0
Wire CSRF into the shell and show action errors as text.
cursoragent Oct 19, 2026
ee52267
fix: report one DHCP listener state on every view
cursoragent Oct 19, 2026
093896f
fix: return a menu name instead of the PXE file path
cursoragent Oct 19, 2026
0076aea
fix: keep the TFTP root on a persistent path
cursoragent Oct 19, 2026
67be554
test: resolve the TFTP override inside a temp directory
cursoragent Oct 19, 2026
ab1af96
fix: keep TFTP files on a Docker volume
cursoragent Oct 19, 2026
9b097cb
fix: set cookie Secure from COOKIE_SECURE
cursoragent Oct 19, 2026
b23c475
fix: agree the attention subtitle with each item
cursoragent Oct 19, 2026
cb1f16b
fix: put overall health on every page
cursoragent Oct 19, 2026
a396356
fix: add RFC3339 timestamps beside the display text
cursoragent Oct 19, 2026
48c759b
fix: swap syslinux boot files only after a staged copy
cursoragent Oct 19, 2026
1d4dfcc
fix: stop returning the session token from login
cursoragent Oct 19, 2026
6f857a2
fix: redirect GET /status to Overview
cursoragent Oct 19, 2026
af4ced5
fix: show the configured TFTP path on the page
cursoragent Oct 19, 2026
264c1e4
fix: count in-flight and failed leases on the stat strips
cursoragent Oct 19, 2026
2d98366
fix: keep a computed overall status when page health is empty
cursoragent Oct 19, 2026
2206ee1
Align Overview columns and name why service status is partial.
cursoragent Oct 19, 2026
d051602
Unify DHCP status wording and fix the lease layout.
cursoragent Oct 19, 2026
8de3c4a
Show the DHCP server state without renaming it.
cursoragent Oct 19, 2026
ee4dda2
Map DHCP state to one pill and align the TFTP listing.
cursoragent Oct 20, 2026
73fc127
Highlight Provision and TFTP previews with DOM tokens.
cursoragent Oct 20, 2026
990d2a4
Merge backend DHCP listener state at 67be554.
cursoragent Oct 20, 2026
bd8c97c
Regenerate README screenshots for the DHCP contract.
cursoragent Oct 20, 2026
94ec1c5
Format the DHCP status helpers.
cursoragent Oct 20, 2026
1f089a3
Close the round-3 review gaps on the shell, Provision, and OS images.
cursoragent Oct 20, 2026
a3800e7
Keep layout fallbacks on the fields the merged backend already has.
cursoragent Oct 20, 2026
0b00433
Match the rev 2 stat strips and configured pill.
cursoragent Oct 20, 2026
d659fb0
Merge backend phase 1 foundations at 2d98366.
cursoragent Oct 20, 2026
ed059ab
Wire confirmed status, timestamp, and count fields.
cursoragent Oct 20, 2026
4f00631
Regenerate screenshots after the backend field wiring.
cursoragent Oct 20, 2026
29e0a06
fix: keep the database and Syslinux cache on a data volume
cursoragent Oct 22, 2026
616988d
fix: listen on TFTP_PORT and honor DHCP_ENABLED
cursoragent Oct 22, 2026
ca1c2f6
Merge backend phase 1 foundations at 616988d.
cursoragent Oct 22, 2026
bb1b0bc
Show the original flame and finish the round-4 UI fixes.
cursoragent Oct 22, 2026
4d0d569
Regenerate screenshots after the flame mark and round-4 fixes.
cursoragent Oct 22, 2026
c8cb584
Show only the Overview pair inline in the README.
cursoragent Oct 22, 2026
a928568
fix: skip BMC TLS verification unless BMC_TLS_VERIFY is set
cursoragent Oct 22, 2026
8429d0f
Recapture README screenshots from the current build.
cursoragent Oct 22, 2026
ed4ca62
Merge backend phase 1 at a928568.
cursoragent Oct 22, 2026
d6bde68
Give Overview DHCP cards the same two-column width as Services.
cursoragent Oct 23, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ vmlinuz
bin
public/tftp/*
public/node_modules
scripts/node_modules
public/http/css/tailwind.css
public/tftp/pxelinux.cfg/*
public/provision/configs/cloud-init/*
Expand Down
11 changes: 8 additions & 3 deletions Dockerfile.release
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,26 @@ WORKDIR /app

# Download the pre-built binary from GitHub release
# Use curl with follow redirects for better GitHub releases handling
RUN curl -L -o ./ignite https://github.com/chadleeshaw/ignite/releases/download/v2.0.0/ignite-linux-amd64 && \
RUN curl -L -o ./ignite https://github.com/chadleeshaw/ignite/releases/download/v3.0.0/ignite-linux-amd64 && \
chmod +x ./ignite

# Copy public files
COPY --chown=ignite:ignite ./public ./public

# Create data directory for database
# Create the data directory before the volume so a new named volume is
# owned by ignite (uid 1001). The database and Syslinux cache live here.
RUN mkdir -p /app/data && chown ignite:ignite /app/data
VOLUME /app/data

# Declared after COPY so a new named volume is seeded from ./public/tftp.
VOLUME /app/public/tftp

# Switch to non-root user
USER ignite

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/status || exit 1
CMD curl -f http://localhost:${HTTP_PORT}/ || exit 1

CMD ["./ignite"]
280 changes: 235 additions & 45 deletions README.md

Large diffs are not rendered by default.

87 changes: 83 additions & 4 deletions app/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import (
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"

"ignite/config"
"ignite/dhcp"
"ignite/handlers"
"ignite/routes"
"ignite/tftp"
Expand All @@ -25,6 +28,11 @@ type Application struct {
tftpServer *tftp.Server
staticFS embed.FS
staticHandler *handlers.StaticHandlers

// rootCancel cancels the process-wide context passed to background work.
// Stop calls it so the lease maintainer observes cancellation.
rootCancel context.CancelFunc
maintainer *dhcp.LeaseMaintainer
}

// NewApplicationWithStatic creates a new application instance with embedded static files
Expand All @@ -45,7 +53,12 @@ func NewApplicationWithStatic(staticFS embed.FS) (*Application, error) {
SyslinuxService: container.SyslinuxService,
IPXEService: container.IPXEService,
Config: container.Config,
Prefs: container.Prefs,
}
handlers.SetPreferenceStore(container.Prefs)
handlers.SetPageHealth(func(r *http.Request) string {
return handlers.NewStatusHandlers(handlerContainer).CurrentOverallStatus(r)
})

return &Application{
container: container,
Expand All @@ -58,12 +71,61 @@ func NewApplicationWithStatic(staticFS embed.FS) (*Application, error) {

// Start starts all application services including static file serving
func (a *Application) Start() error {
// Start TFTP server
a.tftpServer = tftp.NewServer(a.container.Config.TFTP.Dir)
rootCtx, rootCancel := context.WithCancel(context.Background())
a.rootCancel = rootCancel
a.maintainer = dhcp.NewLeaseMaintainer(
a.container.LeaseService,
dhcp.DefaultMaintenanceInterval,
dhcp.DefaultOfflineThreshold,
)
a.maintainer.Start(rootCtx)

// Start TFTP server. Record the outcome so status never stays "unknown"
// after a start attempt, including a failed bind.
tftpRoot, err := config.ResolveTFTPRoot(a.container.Config.TFTP.Dir)
if err != nil {
handlers.SetTFTPRunning(false)
a.stopMaintenance()
return fmt.Errorf("failed to resolve TFTP root: %w", err)
}
for _, line := range tftpRoot.StartupLines() {
log.Print(line)
}
a.container.Config.TFTP.Dir = tftpRoot.Path
handlers.SetTFTPRoot(tftpRoot.Path)
handlers.SetTFTPDisplayRoot(tftpRoot.Display)
if a.container.SyslinuxService != nil {
slCfg := a.container.SyslinuxService.GetConfig()
slCfg.TFTPDir = tftpRoot.Path
_ = a.container.SyslinuxService.UpdateConfig(rootCtx, slCfg)
log.Printf("Syslinux cache: %s", slCfg.TempDir)
}
tftpPort, err := strconv.Atoi(a.container.Config.TFTP.Port)
if err != nil {
handlers.SetTFTPRunning(false)
a.stopMaintenance()
return fmt.Errorf("invalid TFTP port %q: %w", a.container.Config.TFTP.Port, err)
}
a.tftpServer = tftp.NewServerWithPort(tftpRoot.Path, tftpPort)
if err := a.tftpServer.Start(); err != nil {
handlers.SetTFTPRunning(false)
a.stopMaintenance()
return fmt.Errorf("failed to start TFTP server: %w", err)
}
log.Printf("TFTP server started on port 69, serving from %s", a.container.Config.TFTP.Dir)
handlers.SetTFTPRunning(true)
log.Printf("TFTP server started on port %d, serving from %s", tftpPort, a.container.Config.TFTP.Dir)
if a.container.Config.DHCP.Enabled {
dhcp.SetListenersEnabled(true)
} else {
dhcp.SetListenersEnabled(false)
log.Print("DHCP is disabled")
}
log.Print(config.BMCTLSModeLog(a.container.Config.BMC.TLSVerify))

// Every page gets the same overall service word as the Overview pill.
handlers.SetPageHealth(func(r *http.Request) string {
return handlers.NewStatusHandlers(a.handlerContainer).CurrentOverallStatus(r)
})

// Create HTTP router with injected dependencies and static file handling
router := routes.SetupWithContainerAndStatic(a.handlerContainer, a.staticHandler)
Expand All @@ -90,8 +152,24 @@ func (a *Application) Start() error {
return nil
}

// Rest of the Application methods remain the same...
// stopMaintenance cancels the application context and waits for the lease
// maintainer to finish. It is safe to call more than once.
func (a *Application) stopMaintenance() {
if a.rootCancel != nil {
a.rootCancel()
a.rootCancel = nil
}
if a.maintainer != nil {
a.maintainer.Stop()
a.maintainer = nil
}
}

func (a *Application) Stop() error {
// Cancel background work first and wait for the lease maintainer to
// leave the database alone before the rest of shutdown closes it.
a.stopMaintenance()

// Stop any running DHCP servers first
if a.container.ServerService != nil {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
Expand Down Expand Up @@ -121,6 +199,7 @@ func (a *Application) Stop() error {

if a.tftpServer != nil {
a.tftpServer.Stop()
handlers.SetTFTPRunning(false)
}

if err := a.container.Close(); err != nil {
Expand Down
38 changes: 36 additions & 2 deletions app/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,53 @@ package app

import (
"embed"
"net"
"path/filepath"
"strconv"
"testing"

"ignite/handlers"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewApplicationWithStatic(t *testing.T) {
t.Setenv("DB_PATH", t.TempDir())
// Create a simple empty embedded filesystem for testing
testFS := embed.FS{}
app, err := NewApplicationWithStatic(testFS)
assert.NoError(t, err)
assert.NotNil(t, app)
require.NoError(t, err)
require.NotNil(t, app)
t.Cleanup(func() { _ = app.Stop() })
assert.NotNil(t, app.GetContainer())
assert.NotNil(t, app.GetContainer().ServerService)
assert.NotNil(t, app.GetContainer().LeaseService)
assert.NotNil(t, app.GetContainer().Config)
}

func TestStartAndStopRecordTFTPStatus(t *testing.T) {
t.Setenv("DB_PATH", t.TempDir())
t.Setenv("DB_FILE", "tftp-status.db")
t.Setenv("HTTP_PORT", "18081")
t.Setenv("TFTP_DIR", filepath.Join(t.TempDir(), "tftp"))
probe, err := net.ListenUDP("udp4", &net.UDPAddr{Port: 0})
require.NoError(t, err)
tftpPort := probe.LocalAddr().(*net.UDPAddr).Port
require.NoError(t, probe.Close())
t.Setenv("TFTP_PORT", strconv.Itoa(tftpPort))

app, err := NewApplicationWithStatic(embed.FS{})
require.NoError(t, err)
t.Cleanup(func() { _ = app.Stop() })

err = app.Start()
if err != nil {
require.Equal(t, "stopped", handlers.TFTPStatus())
return
}
require.Equal(t, ":18081", app.httpServer.Addr)
require.Equal(t, "running", handlers.TFTPStatus())
require.NoError(t, app.Stop())
require.Equal(t, "stopped", handlers.TFTPStatus())
}
27 changes: 24 additions & 3 deletions app/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"ignite/config"
"ignite/db"
"ignite/dhcp"
"ignite/handlers"
"ignite/ipxe"
"ignite/osimage"
"ignite/syslinux"
Expand All @@ -24,6 +25,7 @@ type Container struct {
SyslinuxRepo syslinux.Repository
SyslinuxService syslinux.Service
IPXEService *ipxe.Service
Prefs handlers.PreferenceStore
}

// NewContainer creates and wires up all dependencies
Expand All @@ -49,8 +51,14 @@ func NewContainer() (c *Container, err error) {
// Create repositories
serverRepo := dhcp.NewBoltServerRepository(database, cfg.DB.Bucket+"_servers")
leaseRepo := dhcp.NewBoltLeaseRepository(database, cfg.DB.Bucket+"_leases")
osImageRepo := osimage.NewOSImageRepository(database)
downloadStatusRepo := osimage.NewDownloadStatusRepository(database)
osImageRepo, err := osimage.NewOSImageRepository(database)
if err != nil {
return nil, fmt.Errorf("failed to create os image repository: %w", err)
}
downloadStatusRepo, err := osimage.NewDownloadStatusRepository(database)
if err != nil {
return nil, fmt.Errorf("failed to create download status repository: %w", err)
}
syslinuxRepo, err := syslinux.NewBoltRepository(database)
if err != nil {
return nil, fmt.Errorf("failed to create syslinux repository: %w", err)
Expand All @@ -60,8 +68,20 @@ func NewContainer() (c *Container, err error) {
serverService := dhcp.NewDHCPServerService(serverRepo, leaseRepo)
leaseService := dhcp.NewDHCPLeaseService(leaseRepo, serverRepo)
osImageService := osimage.NewOSImageService(osImageRepo, downloadStatusRepo, cfg)
syslinuxService := syslinux.NewService(syslinuxRepo, syslinux.GetDefaultConfig())
cacheDir, err := config.ResolveSyslinuxCacheDir(cfg.DB.DBPath, cfg.Syslinux.CacheDir)
if err != nil {
return nil, fmt.Errorf("failed to resolve syslinux cache: %w", err)
}
cfg.Syslinux.CacheDir = cacheDir
slCfg := syslinux.GetDefaultConfig()
slCfg.TFTPDir = cfg.TFTP.Dir
slCfg.TempDir = cacheDir
syslinuxService := syslinux.NewService(syslinuxRepo, slCfg)
ipxeService := ipxe.NewService(cfg, osImageService)
prefs, err := handlers.NewBoltPreferenceStore(database, cfg.DB.Bucket+"_prefs")
if err != nil {
return nil, fmt.Errorf("failed to create preference store: %w", err)
}

c = &Container{
Config: cfg,
Expand All @@ -76,6 +96,7 @@ func NewContainer() (c *Container, err error) {
SyslinuxRepo: syslinuxRepo,
SyslinuxService: syslinuxService,
IPXEService: ipxeService,
Prefs: prefs,
}
return c, nil
}
Expand Down
8 changes: 8 additions & 0 deletions app/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package app

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewContainer(t *testing.T) {
Expand All @@ -30,6 +32,12 @@ func TestNewContainer(t *testing.T) {
assert.NotNil(t, container.ServerService)
assert.NotNil(t, container.LeaseService)
assert.NotNil(t, container.Config)
wantCache, err := filepath.Abs(filepath.Join(tempDir, "syslinux"))
require.NoError(t, err)
assert.Equal(t, wantCache, container.SyslinuxService.GetConfig().TempDir)
info, err := os.Stat(wantCache)
require.NoError(t, err)
assert.True(t, info.IsDir())

// Clean up
container.Close()
Expand Down
File renamed without changes
Loading
Loading