Skip to content

Commit 86bdd39

Browse files
authored
Add Syslog server to receive logs from machines during installation (#27)
Add the ability from `netbootd` to run a Syslog server on the TCP/UDP port specified by the new `-s`/`--syslog-port` flags (defaults to 514). A new `{{ .SyslogHost }}` value is available in mount content templates which contains the `<host>:<port>` to send syslog messages. This can be used, for example, to construct an `inst.syslog=<host>:<port>` kernel argument for RHEL 9 which instructs the installer to send syslogs to the given syslog server. This can make it much easier to see what is happening during an installation, and make debugging easier in case something goes wrong. Syslog messages received by `netbootd`'s syslog server are logged at `INFO` level. All syslog attributes are added to the `INFO` log emitted by `netbootd` prepended with `syslog.`. Thus a syslog message with attribute `foo=bar` is emitted by `netbootd` as `syslog.foo=bar`. The exception is `content` or `message` attributes which are used as the value for `.Msg()`.
1 parent 8e1f561 commit 86bdd39

File tree

10 files changed

+137
-6
lines changed

10 files changed

+137
-6
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# netbootd
22

33
netbootd is a lightweight network boot server, designed for maximum flexibility
4-
and with "batteries included" approach in mind, serving as a DHCP, TFTP and HTTP server.
4+
and with "batteries included" approach in mind, serving as a DHCP, TFTP, Syslog and HTTP server.
55
It includes a basic templating functionality, designed to allow generating e.g. preseed
66
files for unattended OS installation.
77

@@ -47,6 +47,10 @@ netbootd can serve local files using the `path.localDir` configuration option.
4747
netbootd also contains a bundled version of [iPXE](https://ipxe.org/), which allows
4848
downloading (typically) kernel and initrd over HTTP instead of TFTP.
4949

50+
## Syslog
51+
52+
netbootd includes a syslog server to allow collecting and displaying a machine's logs during installation.
53+
5054
## Manifests
5155

5256
A manifest represents a machine to be provisioned/served. The behavior of built-in DHCP, TFTP and HTTP server is
@@ -129,7 +133,7 @@ mounts:
129133
appendSuffix: true
130134

131135
- path: /install.ipxe
132-
# The templating context provides access to: .LocalIP, .RemoteIP, .HttpBaseUrl, .ApiBaseUrl and .Manifest.
136+
# The templating context provides access to: .LocalIP, .RemoteIP, .HttpBaseUrl, .ApiBaseUrl, .SyslogHost and .Manifest.
133137
# Sprig functions are available: masterminds.github.io/sprig
134138
content: |
135139
#!ipxe
@@ -228,6 +232,7 @@ Flags:
228232
-i, --interface string interface to listen on, e.g. eth0 (DHCP)
229233
-m, --manifests string load manifests from directory
230234
--root string if not given as an absolute path, a mount's path.localDir is relative to this directory
235+
-s, --syslog-port int Syslog port to listen on (default 514)
231236

232237
Global Flags:
233238
-d, --debug enable debug logging

cmd/server.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/DSpeichert/netbootd/dhcpd"
1111
"github.com/DSpeichert/netbootd/httpd"
1212
"github.com/DSpeichert/netbootd/store"
13+
"github.com/DSpeichert/netbootd/syslogd"
1314
"github.com/DSpeichert/netbootd/tftpd"
1415
systemd "github.com/coreos/go-systemd/v22/daemon"
1516
"github.com/rs/zerolog"
@@ -22,6 +23,7 @@ var (
2223
addr string
2324
ifname string
2425
httpPort int
26+
syslogPort int
2527
apiPort int
2628
apiTlsCert string
2729
apiTlsKey string
@@ -36,6 +38,9 @@ func init() {
3638
serverCmd.Flags().IntVarP(&httpPort, "http-port", "p", 8080, "HTTP port to listen on")
3739
viper.BindPFlag("http.port", serverCmd.Flags().Lookup("http-port"))
3840

41+
serverCmd.Flags().IntVarP(&syslogPort, "syslog-port", "s", 514, "Syslog port to listen on")
42+
viper.BindPFlag("syslog.port", serverCmd.Flags().Lookup("syslog-port"))
43+
3944
serverCmd.Flags().IntVarP(&apiPort, "api-port", "r", 8081, "HTTP API port to listen on")
4045
viper.BindPFlag("api.port", serverCmd.Flags().Lookup("api-port"))
4146

@@ -83,6 +88,7 @@ var serverCmd = &cobra.Command{
8388
}
8489
}
8590
store.GlobalHints.HttpPort = viper.GetInt("http.port")
91+
store.GlobalHints.SyslogPort = viper.GetInt("syslog.port")
8692
store.GlobalHints.ApiPort = viper.GetInt("api.port")
8793

8894
// DHCP
@@ -131,6 +137,23 @@ var serverCmd = &cobra.Command{
131137
go httpServer.Serve(connHttp)
132138
log.Info().Interface("addr", connHttp.Addr()).Msg("HTTP listening")
133139

140+
// Syslog service
141+
syslogServer, err := syslogd.NewServer(store)
142+
if err != nil {
143+
log.Fatal().Err(err).Msg("Failed to create Syslog server")
144+
}
145+
syslogAddr := net.JoinHostPort(addressIPStr, viper.GetString("syslog.port"))
146+
err = syslogServer.ListenUDP(syslogAddr)
147+
if err != nil {
148+
log.Fatal().Err(err).Msg("Failed to bind UDP Syslog server")
149+
}
150+
err = syslogServer.ListenTCP(syslogAddr)
151+
if err != nil {
152+
log.Fatal().Err(err).Msg("Failed to bind TCP Syslog server")
153+
}
154+
go syslogServer.Serve()
155+
log.Info().Interface("syslog", syslogAddr).Msg("Syslog listening...")
156+
134157
// HTTP API service
135158
apiServer, err := api.NewServer(store, viper.GetString("api.authorization"), viper.GetString("rootPath"))
136159
if err != nil {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701
1818
golang.org/x/net v0.42.0
1919
golang.org/x/sys v0.34.0
20+
gopkg.in/mcuadros/go-syslog.v2 v2.3.0
2021
gopkg.in/yaml.v2 v2.4.0
2122
)
2223

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
117117
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
118118
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
119119
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
120+
gopkg.in/mcuadros/go-syslog.v2 v2.3.0 h1:kcsiS+WsTKyIEPABJBJtoG0KkOS6yzvJ+/eZlhD79kk=
121+
gopkg.in/mcuadros/go-syslog.v2 v2.3.0/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
120122
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
121123
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
122124
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

httpd/handler.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
146146
Scheme: "http",
147147
Host: net.JoinHostPort(laddr.String(), strconv.Itoa(h.server.store.GlobalHints.ApiPort)),
148148
},
149-
Manifest: manifest,
149+
SyslogHost: net.JoinHostPort(laddr.String(), strconv.Itoa(h.server.store.GlobalHints.SyslogPort)),
150+
Manifest: manifest,
150151
})
151152
if err != nil {
152153
h.server.logger.Error().

manifest/schema.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ type ContentContext struct {
129129
HttpBaseUrl *url.URL
130130
// Base URL to the API service (IP and port)
131131
ApiBaseUrl *url.URL
132+
// Host to Syslog service (IP and port)
133+
SyslogHost string
132134
// Copy of Manifest
133135
Manifest *Manifest
134136
}

store/store.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ type Store struct {
3636

3737
// sort of global config
3838
GlobalHints struct {
39-
HttpPort int
40-
ApiPort int
39+
HttpPort int
40+
ApiPort int
41+
SyslogPort int
4142
}
4243
}
4344

syslogd/handler.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package syslogd
2+
3+
import "gopkg.in/mcuadros/go-syslog.v2/format"
4+
5+
func (server *Server) syslogHandleLog(logParts format.LogParts) {
6+
event := server.logger.Info()
7+
var msg string
8+
for key, value := range logParts {
9+
if m, ok := isMsg(key, value); ok {
10+
msg = m
11+
continue
12+
}
13+
event = event.Interface("syslog."+key, value)
14+
}
15+
if msg == "" {
16+
msg = "UNKNOWN"
17+
}
18+
event.Msg(msg)
19+
}
20+
21+
func isMsg(key string, value any) (m string, ok bool) {
22+
if key != "content" && key != "message" {
23+
return "", false
24+
}
25+
m, ok = value.(string)
26+
if !ok {
27+
return "", false
28+
}
29+
return m, true
30+
}

syslogd/server.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package syslogd
2+
3+
import (
4+
"github.com/DSpeichert/netbootd/store"
5+
"github.com/rs/zerolog"
6+
"github.com/rs/zerolog/log"
7+
"gopkg.in/mcuadros/go-syslog.v2"
8+
)
9+
10+
var defaultAddr = "0.0.0.0:514"
11+
12+
type Server struct {
13+
syslogServer *syslog.Server
14+
15+
logger zerolog.Logger
16+
store *store.Store
17+
ch syslog.LogPartsChannel
18+
}
19+
20+
func NewServer(store *store.Store) (server *Server, err error) {
21+
server = &Server{
22+
syslogServer: syslog.NewServer(),
23+
logger: log.With().Str("service", "syslog").Logger(),
24+
store: store,
25+
ch: make(syslog.LogPartsChannel),
26+
}
27+
28+
server.syslogServer.SetFormat(syslog.Automatic)
29+
server.syslogServer.SetHandler(syslog.NewChannelHandler(server.ch))
30+
31+
return server, nil
32+
}
33+
34+
func (server *Server) ListenUDP(addr string) error {
35+
if addr == "" {
36+
addr = defaultAddr
37+
}
38+
err := server.syslogServer.ListenUDP(addr)
39+
if err != nil {
40+
return err
41+
}
42+
return nil
43+
}
44+
45+
func (server *Server) ListenTCP(addr string) error {
46+
if addr == "" {
47+
addr = defaultAddr
48+
}
49+
err := server.syslogServer.ListenTCP(addr)
50+
if err != nil {
51+
return err
52+
}
53+
return nil
54+
}
55+
56+
func (server *Server) Serve() error {
57+
err := server.syslogServer.Boot()
58+
if err != nil {
59+
return err
60+
}
61+
for logParts := range server.ch {
62+
server.syslogHandleLog(logParts)
63+
}
64+
return nil
65+
}

tftpd/handler.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ func (server *Server) tftpReadHandler(filename string, rf io.ReaderFrom) error {
146146
Scheme: "http",
147147
Host: net.JoinHostPort(laddr.String(), strconv.Itoa(server.store.GlobalHints.ApiPort)),
148148
},
149-
Manifest: manifest,
149+
SyslogHost: net.JoinHostPort(laddr.String(), strconv.Itoa(server.store.GlobalHints.SyslogPort)),
150+
Manifest: manifest,
150151
})
151152
if err != nil {
152153
server.logger.Error().

0 commit comments

Comments
 (0)