diff --git a/README.md b/README.md index 0c040fa..0320799 100644 --- a/README.md +++ b/README.md @@ -76,16 +76,23 @@ It is usually done via RADIUS server or similar solution. Use the config file with `gombak -c config.yaml` +## System service +Gombak can be set to run as a system service using the provided CLI commands. +Once the `gombak` binary and its configuration YAML file is set in place, system service can be interacted with: +* `gombak install -c ` - install and run the `gombak` system service +* `gombak uninstall` - uninstall `gombak` system service + ## Flags Check which flags are available with `gombak -h` ``` -b, --backup-dir string mikrotik backup export directory (default "mt-backup") +-r, --backup-retention-days days of backup file retention (default 30) + --backup-frequency-days backup frequency in days (default 5) -c, --config string configuration yaml file --log.file string write logs to the specified file --log.json output logs in json format --log.level string define log level (default "info") -m, --mode string mode of operation (default "single") --r, --retention-days int days of retention (default 5) --single.host string the ip address of the router --single.pass string the password for the username --single.ssh-port string the ssh port of the router (default "22") @@ -94,5 +101,4 @@ Check which flags are available with `gombak -h` ## TODO * Email report -* CLI command to set up a system service * More discovery modes \ No newline at end of file diff --git a/main.go b/main.go index 804c1a2..861ffb7 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "github.com/ZeljkoBenovic/gombak/internal/app" "github.com/ZeljkoBenovic/gombak/pkg/config" "github.com/ZeljkoBenovic/gombak/pkg/logger" + "github.com/ZeljkoBenovic/gombak/pkg/service" ) func main() { @@ -14,14 +15,30 @@ func main() { log, err := logger.New(conf) if err != nil { - fmt.Printf("Could not create new logger: %s", err.Error()) + fmt.Printf("could not create new logger: %s", err.Error()) os.Exit(1) } run := app.NewApp(conf, log).AppModeFactory() - if err = run(); err != nil { - log.Error(err.Error()) + srv, err := service.New(conf, []string{"run", "-c", conf.ConfigFilePath}, log) + if err != nil { + log.Info("could not init new service", "err", err) os.Exit(1) } + + err, isService := srv.HandleServiceCLICommands(run) + if err != nil { + log.Error("service error", "err", err) + + os.Exit(1) + } + + if !isService { + if err = run(); err != nil { + log.Error("run error", "err", err) + + os.Exit(1) + } + } } diff --git a/pkg/config/config.go b/pkg/config/config.go index d310e54..e0b52f1 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -32,12 +32,15 @@ var AvailableModes = map[string]Mode{ type Config struct { Mode Mode `koanf:"mode"` BackupFolder string `koanf:"backup-dir"` - BackupRetentionDays int `koanf:"retention-days"` + BackupRetentionDays int `koanf:"backup-retention-days"` + BackupFrequencyDays int `konaf:"backup-frequency-days"` Single RouterInfo `koanf:"single"` Discovery Discovery `koanf:"discovery"` Multi []RouterInfo `koanf:"multi-router"` Logger Log `koanf:"log"` + + ConfigFilePath string } type RouterInfo struct { @@ -91,7 +94,8 @@ func NewConfig() Config { f.StringVarP(&confFile, "config", "c", "", "configuration yaml file") f.StringVarP(&c.BackupFolder, "backup-dir", "b", "mt-backup", "mikrotik backup export directory") f.StringVarP(&mode, "mode", "m", "single", "mode of operation") - f.IntVarP(&c.BackupRetentionDays, "retention-days", "r", 30, "days of retention") + f.IntVarP(&c.BackupRetentionDays, "backup-retention-days", "r", 30, "days of retention") + f.IntVarP(&c.BackupFrequencyDays, "backup-frequency-days", "", 5, "backup frequency in days") f.StringVarP(&c.Single.Host, "single.host", "", "", "the ip address of the router") f.StringVarP(&c.Single.Port, "single.ssh-port", "", "22", "the ssh port of the router") @@ -139,7 +143,9 @@ func NewConfig() Config { return Config{ BackupFolder: k.String("backup-dir"), - BackupRetentionDays: k.Int("retention-days"), + BackupRetentionDays: k.Int("backup-retention-days"), + BackupFrequencyDays: k.Int("backup-frequency-days"), + ConfigFilePath: k.String("config"), Mode: AvailableModes[k.String("mode")], Single: RouterInfo{ Host: k.String("single.host"), diff --git a/pkg/service/service.go b/pkg/service/service.go new file mode 100644 index 0000000..f31db5e --- /dev/null +++ b/pkg/service/service.go @@ -0,0 +1,123 @@ +package service + +import ( + "fmt" + "log" + "os" + "time" + + "github.com/ZeljkoBenovic/gombak/pkg/config" + "github.com/ZeljkoBenovic/gombak/pkg/logger" + srv "github.com/kardianos/service" +) + +type Service struct { + svc srv.Service + log *logger.Logger + + runner *serviceRunner +} + +type serviceRunner struct { + runFn func() error + log srv.Logger + stopCh chan struct{} + conf config.Config +} + +func New(conf config.Config, args []string, log *logger.Logger) (*Service, error) { + s := &Service{ + log: log, + runner: &serviceRunner{ + stopCh: make(chan struct{}), + conf: conf, + }, + } + + srvc, err := srv.New(s.runner, &srv.Config{ + Name: "GoMBak", + DisplayName: "GoMBak", + Description: "Provides a Mikrotik router backup service. More info: https://github.com/zeljkobenovic/gombak", + Arguments: args, + }) + if err != nil { + return nil, fmt.Errorf("could not init service: %w", err) + } + + lgr, err := srvc.Logger(nil) + if err != nil { + return nil, fmt.Errorf("could not create service logger: %w", err) + } + + s.svc = srvc + s.runner.log = lgr + + return s, nil +} + +// HandleServiceCLICommands will handle "install", "uninstall" and "run" cli commands which handle gombak as a system service. +// If these cli arguments are not set, this method returns false signaling that it should be run as a console program. +func (s *Service) HandleServiceCLICommands(runFn func() error) (err error, isService bool) { + isService = true + err = nil + + switch os.Args[1] { + case "install": + if err := srv.Control(s.svc, "install"); err != nil { + return fmt.Errorf("could not install gombak service: %w", err), isService + } + + if err := srv.Control(s.svc, "start"); err != nil { + return fmt.Errorf("could not start gombak service: %w", err), isService + } + + return + case "uninstall": + if err := srv.Control(s.svc, "uninstall"); err != nil { + return fmt.Errorf("could not uninstall gombak service: %w", err), isService + } + + return + case "run": + s.runner.runFn = runFn + err = s.svc.Run() + + return + } + + return nil, false +} + +func (s *serviceRunner) Start(_ srv.Service) error { + if s.runFn == nil { + return fmt.Errorf("runFn function not initialized") + } + + go func() { + ticker := time.NewTicker(time.Hour * 24 * time.Duration(s.conf.BackupFrequencyDays)) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + _ = s.log.Info("running mikrotik backup per schedule") + if err := s.runFn(); err != nil { + if err := s.log.Error(err); err != nil { + log.Println(err) + } + } + case <-s.stopCh: + _ = s.log.Info("stopping gombak service") + + return + } + } + }() + + return nil +} + +func (s *serviceRunner) Stop(_ srv.Service) error { + s.stopCh <- struct{}{} + return nil +}