Client mode
* Refactored so that all server code is in a single file * Added missing CLI option to send reload/quit commands to the server * Implemented client mode
This commit is contained in:
parent
cd295e51ba
commit
be6198dbed
3 changed files with 196 additions and 92 deletions
84
client.go
Normal file
84
client.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client runtime data
|
||||||
|
type TClient struct {
|
||||||
|
config tConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the client's state.
|
||||||
|
func InitClient(config tConfiguration) TClient {
|
||||||
|
return TClient{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to the UNIX socket. Terminate the program with an error if connection
|
||||||
|
// fails.
|
||||||
|
func (c *TClient) getConnection() net.Conn {
|
||||||
|
conn, err := net.Dial("unix", c.config.Socket.Path)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"error": err,
|
||||||
|
"path": c.config.Socket.Path,
|
||||||
|
}).Fatal("Could not connect to the UNIX socket")
|
||||||
|
}
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a string to the UNIX socket. Terminate the program with an error if
|
||||||
|
// some form of IO error occurs.
|
||||||
|
func (c *TClient) send(conn net.Conn, data string) {
|
||||||
|
_, err := conn.Write([]byte(data))
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"error": err,
|
||||||
|
"path": c.config.Socket.Path,
|
||||||
|
"data": data,
|
||||||
|
}).Fatal("Could not write to the UNIX socket")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a command to the server then disconnect.
|
||||||
|
func (c *TClient) SendCommand(command string) {
|
||||||
|
conn := c.getConnection()
|
||||||
|
defer conn.Close()
|
||||||
|
c.send(conn, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request an update by sending the selector and force flag to the server, then
|
||||||
|
// wait for the server to respond. Returns true if the server responded that the
|
||||||
|
// updates were executed without problem.
|
||||||
|
func (c *TClient) RequestUpdate(selector string, force bool) bool {
|
||||||
|
command := "U"
|
||||||
|
if force {
|
||||||
|
command += "!"
|
||||||
|
} else {
|
||||||
|
command += " "
|
||||||
|
}
|
||||||
|
command += selector
|
||||||
|
|
||||||
|
conn := c.getConnection()
|
||||||
|
defer conn.Close()
|
||||||
|
c.send(conn, command)
|
||||||
|
|
||||||
|
buf := make([]byte, 2)
|
||||||
|
nr, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"error": err,
|
||||||
|
"path": c.config.Socket.Path,
|
||||||
|
}).Fatal("Could not read server response from the UNIX socket")
|
||||||
|
}
|
||||||
|
if nr != 1 {
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"path": c.config.Socket.Path,
|
||||||
|
}).Fatal("Invalid response from server")
|
||||||
|
}
|
||||||
|
return buf[0] == 49
|
||||||
|
}
|
111
main.go
111
main.go
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/karrick/golf"
|
"github.com/karrick/golf"
|
||||||
|
@ -16,6 +15,11 @@ type (
|
||||||
// then quits), client (connects to the server and requests an
|
// then quits), client (connects to the server and requests an
|
||||||
// update) or server (runs the server in the foreground).
|
// update) or server (runs the server in the foreground).
|
||||||
runMode string
|
runMode string
|
||||||
|
// When running in client mode, if this is set to a string, it
|
||||||
|
// will be interpreted as a command to send to the server.
|
||||||
|
// Supported commands are 'Q' (quit) and 'R' (reload
|
||||||
|
// configuration)
|
||||||
|
command string
|
||||||
// The selector to use when running the updates. Only meaningful
|
// The selector to use when running the updates. Only meaningful
|
||||||
// if running in client or standalone mode.
|
// if running in client or standalone mode.
|
||||||
selector string
|
selector string
|
||||||
|
@ -33,16 +37,6 @@ type (
|
||||||
// Send logs to syslog.
|
// Send logs to syslog.
|
||||||
logSyslog bool
|
logSyslog bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// The state of the main server
|
|
||||||
tServerState struct {
|
|
||||||
// The path to the configuration file
|
|
||||||
cfgFile string
|
|
||||||
// The configuration
|
|
||||||
config tConfiguration
|
|
||||||
// The UNIX socket listener
|
|
||||||
listener net.Listener
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parse command line options.
|
// Parse command line options.
|
||||||
|
@ -52,6 +46,10 @@ func parseCommandLine() tCliFlags {
|
||||||
|
|
||||||
golf.StringVarP(&flags.cfgFile, 'c', "config", "/etc/fetch-certificates.yml",
|
golf.StringVarP(&flags.cfgFile, 'c', "config", "/etc/fetch-certificates.yml",
|
||||||
"Path to the configuration file.")
|
"Path to the configuration file.")
|
||||||
|
golf.StringVarP(&flags.command, 'C', "command", "",
|
||||||
|
"Send a command to the server instead of requesting an "+
|
||||||
|
"update. Only meaningful in client mode. Command may be "+
|
||||||
|
"Q (quit) or R (reload configuration).")
|
||||||
golf.BoolVarP(&flags.force, 'f', "force", false,
|
golf.BoolVarP(&flags.force, 'f', "force", false,
|
||||||
"Force update of selected certificates. Only meaningful in "+
|
"Force update of selected certificates. Only meaningful in "+
|
||||||
"client or standalone mode.")
|
"client or standalone mode.")
|
||||||
|
@ -81,65 +79,6 @@ func parseCommandLine() tCliFlags {
|
||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize server state
|
|
||||||
func initServer(cfgFile string) tServerState {
|
|
||||||
ss := tServerState{
|
|
||||||
cfgFile: cfgFile,
|
|
||||||
}
|
|
||||||
cfg, err := LoadConfiguration(ss.cfgFile)
|
|
||||||
if err != nil {
|
|
||||||
log.WithField("error", err).Fatal("Failed to load initial configuration.")
|
|
||||||
}
|
|
||||||
ss.config = cfg
|
|
||||||
listener, err := initSocket(cfg.Socket)
|
|
||||||
if err != nil {
|
|
||||||
log.WithField("error", err).Fatal("Failed to initialize socket.")
|
|
||||||
}
|
|
||||||
ss.listener = listener
|
|
||||||
return ss
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy the server
|
|
||||||
func (state *tServerState) destroy() {
|
|
||||||
state.listener.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server main loop. Processes commands received from connections. Certificate
|
|
||||||
// update requests are processed directly, but Quit/Reload commands are
|
|
||||||
// propagated back to this loop and handled here.
|
|
||||||
func (state *tServerState) mainLoop() {
|
|
||||||
for {
|
|
||||||
cmd := socketServer(&state.config, state.listener)
|
|
||||||
if cmd == CMD_QUIT {
|
|
||||||
break
|
|
||||||
} else if cmd != CMD_RELOAD {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
new_cfg, err := LoadConfiguration(state.cfgFile)
|
|
||||||
if err != nil {
|
|
||||||
log.WithField("error", err).Error("Failed to load updated configuration.")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
replace_ok := true
|
|
||||||
if new_cfg.Socket.Path != state.config.Socket.Path {
|
|
||||||
new_listener, err := initSocket(new_cfg.Socket)
|
|
||||||
if err != nil {
|
|
||||||
log.WithField("error", err).Error("Failed to initialize new server socket.")
|
|
||||||
replace_ok = false
|
|
||||||
} else {
|
|
||||||
state.listener.Close()
|
|
||||||
state.listener = new_listener
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if replace_ok {
|
|
||||||
state.config = new_cfg
|
|
||||||
log.Info("Configuration reloaded")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flags := parseCommandLine()
|
flags := parseCommandLine()
|
||||||
err := configureLogging(flags)
|
err := configureLogging(flags)
|
||||||
|
@ -147,26 +86,40 @@ func main() {
|
||||||
log.WithField("error", err).Fatal("Failed to configure logging.")
|
log.WithField("error", err).Fatal("Failed to configure logging.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if flags.runMode == "server" {
|
|
||||||
server := initServer(flags.cfgFile)
|
|
||||||
defer server.destroy()
|
|
||||||
server.mainLoop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := LoadConfiguration(flags.cfgFile)
|
cfg, err := LoadConfiguration(flags.cfgFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("error", err).Fatal("Failed to load initial configuration.")
|
log.WithField("error", err).Fatal("Failed to load initial configuration.")
|
||||||
}
|
}
|
||||||
if flags.runMode == "standalone" {
|
|
||||||
|
if flags.runMode == "server" {
|
||||||
|
server := InitServer(flags.cfgFile, cfg)
|
||||||
|
defer server.Destroy()
|
||||||
|
server.MainLoop()
|
||||||
|
|
||||||
|
} else if flags.runMode == "standalone" {
|
||||||
result := executeUpdate(&cfg, flags.selector, flags.force)
|
result := executeUpdate(&cfg, flags.selector, flags.force)
|
||||||
if result {
|
if result {
|
||||||
log.Debug("Update successful")
|
log.Debug("Update successful")
|
||||||
} else {
|
} else {
|
||||||
log.Fatal("Update failed")
|
log.Fatal("Update failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if flags.runMode == "client" {
|
} else if flags.runMode == "client" {
|
||||||
panic("CLIENT MODE NOT IMPLEMENTED") // FIXME
|
client := InitClient(cfg)
|
||||||
|
if flags.command == "Q" || flags.command == "R" {
|
||||||
|
client.SendCommand(flags.command)
|
||||||
|
} else if flags.command != "" {
|
||||||
|
log.WithField("command", flags.command).Fatal(
|
||||||
|
"Unknown server command.")
|
||||||
|
} else {
|
||||||
|
result := client.RequestUpdate(flags.selector, flags.force)
|
||||||
|
if result {
|
||||||
|
log.Debug("Update successful")
|
||||||
|
} else {
|
||||||
|
log.Fatal("Update failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
log.WithField("mode", flags.runMode).Fatal("Unknown execution mode.")
|
log.WithField("mode", flags.runMode).Fatal("Unknown execution mode.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,20 +12,32 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TCommandType int
|
type tCommandType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CMD_IGNORE TCommandType = iota
|
CMD_IGNORE tCommandType = iota
|
||||||
CMD_QUIT
|
CMD_QUIT
|
||||||
CMD_RELOAD
|
CMD_RELOAD
|
||||||
CMD_UPDATE
|
CMD_UPDATE
|
||||||
)
|
)
|
||||||
|
|
||||||
type TCommand struct {
|
type (
|
||||||
CommandType TCommandType
|
tCommand struct {
|
||||||
Force bool
|
CommandType tCommandType
|
||||||
Selector string
|
Force bool
|
||||||
}
|
Selector string
|
||||||
|
}
|
||||||
|
|
||||||
|
// The state of the main server
|
||||||
|
TServerState struct {
|
||||||
|
// The path to the configuration file
|
||||||
|
cfgFile string
|
||||||
|
// The configuration
|
||||||
|
config tConfiguration
|
||||||
|
// The UNIX socket listener
|
||||||
|
listener net.Listener
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func configureSocket(cfg tSocketConfig) error {
|
func configureSocket(cfg tSocketConfig) error {
|
||||||
if cfg.Group != "" {
|
if cfg.Group != "" {
|
||||||
|
@ -67,7 +79,7 @@ func initSocket(cfg tSocketConfig) (net.Listener, error) {
|
||||||
return listener, nil
|
return listener, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func socketServer(cfg *tConfiguration, listener net.Listener) TCommandType {
|
func socketServer(cfg *tConfiguration, listener net.Listener) tCommandType {
|
||||||
for {
|
for {
|
||||||
fd, err := listener.Accept()
|
fd, err := listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -80,7 +92,7 @@ func socketServer(cfg *tConfiguration, listener net.Listener) TCommandType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeFromSocket(cfg *tConfiguration, conn net.Conn) TCommandType {
|
func executeFromSocket(cfg *tConfiguration, conn net.Conn) tCommandType {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
log.Debug("Received connection")
|
log.Debug("Received connection")
|
||||||
|
|
||||||
|
@ -114,7 +126,7 @@ func executeFromSocket(cfg *tConfiguration, conn net.Conn) TCommandType {
|
||||||
return command.CommandType
|
return command.CommandType
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCommand(n int, buf []byte) *TCommand {
|
func parseCommand(n int, buf []byte) *tCommand {
|
||||||
if n == 512 {
|
if n == 512 {
|
||||||
log.Warn("Too much data received")
|
log.Warn("Too much data received")
|
||||||
return nil
|
return nil
|
||||||
|
@ -125,12 +137,12 @@ func parseCommand(n int, buf []byte) *TCommand {
|
||||||
}
|
}
|
||||||
if n == 1 {
|
if n == 1 {
|
||||||
if buf[0] == 'Q' {
|
if buf[0] == 'Q' {
|
||||||
return &TCommand{CommandType: CMD_QUIT}
|
return &tCommand{CommandType: CMD_QUIT}
|
||||||
} else if buf[0] == 'R' {
|
} else if buf[0] == 'R' {
|
||||||
return &TCommand{CommandType: CMD_RELOAD}
|
return &tCommand{CommandType: CMD_RELOAD}
|
||||||
}
|
}
|
||||||
} else if n > 2 && buf[0] == 'U' {
|
} else if n > 2 && buf[0] == 'U' {
|
||||||
res := &TCommand{CommandType: CMD_UPDATE}
|
res := &tCommand{CommandType: CMD_UPDATE}
|
||||||
if buf[1] == '!' {
|
if buf[1] == '!' {
|
||||||
res.Force = true
|
res.Force = true
|
||||||
}
|
}
|
||||||
|
@ -142,3 +154,58 @@ func parseCommand(n int, buf []byte) *TCommand {
|
||||||
log.Warn("Invalid command received")
|
log.Warn("Invalid command received")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize server state
|
||||||
|
func InitServer(cfgFile string, config tConfiguration) TServerState {
|
||||||
|
ss := TServerState{
|
||||||
|
cfgFile: cfgFile,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
listener, err := initSocket(ss.config.Socket)
|
||||||
|
if err != nil {
|
||||||
|
log.WithField("error", err).Fatal("Failed to initialize socket.")
|
||||||
|
}
|
||||||
|
ss.listener = listener
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy the server
|
||||||
|
func (state *TServerState) Destroy() {
|
||||||
|
state.listener.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server main loop. Processes commands received from connections. Certificate
|
||||||
|
// update requests are processed directly, but Quit/Reload commands are
|
||||||
|
// propagated back to this loop and handled here.
|
||||||
|
func (state *TServerState) MainLoop() {
|
||||||
|
for {
|
||||||
|
cmd := socketServer(&state.config, state.listener)
|
||||||
|
if cmd == CMD_QUIT {
|
||||||
|
break
|
||||||
|
} else if cmd != CMD_RELOAD {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
new_cfg, err := LoadConfiguration(state.cfgFile)
|
||||||
|
if err != nil {
|
||||||
|
log.WithField("error", err).Error("Failed to load updated configuration.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
replace_ok := true
|
||||||
|
if new_cfg.Socket.Path != state.config.Socket.Path {
|
||||||
|
new_listener, err := initSocket(new_cfg.Socket)
|
||||||
|
if err != nil {
|
||||||
|
log.WithField("error", err).Error("Failed to initialize new server socket.")
|
||||||
|
replace_ok = false
|
||||||
|
} else {
|
||||||
|
state.listener.Close()
|
||||||
|
state.listener = new_listener
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if replace_ok {
|
||||||
|
state.config = new_cfg
|
||||||
|
log.Info("Configuration reloaded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue