From 0e7208dcd5aa9190cb5642406a2138cdf512f04b Mon Sep 17 00:00:00 2001 From: ZeljkoBenovic Date: Sun, 8 Sep 2024 13:29:52 +0200 Subject: [PATCH 1/2] added tunnel name to the error output on ssh conn error --- go.mod | 2 +- go.sum | 3 +++ internal/app/app.go | 9 +++++---- pkg/config/config.go | 2 +- pkg/discovery/discovery.go | 2 +- pkg/discovery/l2tp/l2tp.go | 17 +++++++++-------- 6 files changed, 20 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index ef32540..fb190c2 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/ZeljkoBenovic/gombak go 1.21.4 require ( - github.com/davecgh/go-spew v1.1.1 github.com/go-routeros/routeros v0.0.0-20210123142807-2a44d57c6730 + github.com/kardianos/service v1.2.2 github.com/knadh/koanf/parsers/yaml v0.1.0 github.com/knadh/koanf/providers/env v0.1.0 github.com/knadh/koanf/providers/file v0.1.0 diff --git a/go.sum b/go.sum index c99e376..1996744 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/go-routeros/routeros v0.0.0-20210123142807-2a44d57c6730 h1:EuqwWLv/LP github.com/go-routeros/routeros v0.0.0-20210123142807-2a44d57c6730/go.mod h1:em1mEqFKnoeQuQP9Sg7i26yaW8o05WwcNj7yLhrXxSQ= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60= +github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= @@ -51,6 +53,7 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/app/app.go b/internal/app/app.go index dd20f4d..b83b15e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -125,21 +125,22 @@ func (a App) AppModeFactory() func() error { return err } - for _, mt := range discRouters { + for name, ip := range discRouters { a.wg.Add(1) - mt := mt + ip := ip + name := name go func() { defer a.wg.Done() if err := a.singleRouterBackup( - mt, + ip, a.conf.Discovery.SSHPort, a.conf.Discovery.Username, a.conf.Discovery.Password, ); err != nil { - a.log.Error("Could not perform backup", "err", err.Error(), "host", mt) + a.log.Error("Could not perform backup", "err", err.Error(), "host", name, "ip", ip) return } }() diff --git a/pkg/config/config.go b/pkg/config/config.go index ced2774..d310e54 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -91,7 +91,7 @@ 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", 5, "days of retention") + f.IntVarP(&c.BackupRetentionDays, "retention-days", "r", 30, "days of retention") 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") diff --git a/pkg/discovery/discovery.go b/pkg/discovery/discovery.go index 410a989..27b7db2 100644 --- a/pkg/discovery/discovery.go +++ b/pkg/discovery/discovery.go @@ -9,7 +9,7 @@ import ( type Discovery interface { // GetIPAddresses returns the list of discovered ip addresses - GetIPAddresses() ([]string, error) + GetIPAddresses() (map[string]string, error) } type Config struct { diff --git a/pkg/discovery/l2tp/l2tp.go b/pkg/discovery/l2tp/l2tp.go index e59ce5c..4428d88 100644 --- a/pkg/discovery/l2tp/l2tp.go +++ b/pkg/discovery/l2tp/l2tp.go @@ -27,12 +27,12 @@ type L2TP struct { type discoveredHosts struct { mut *sync.Mutex - hosts []string + hosts map[string]string } -func (d *discoveredHosts) add(hosts []string) { +func (d *discoveredHosts) add(hosts map[string]string) { d.mut.Lock() - d.hosts = append(d.hosts, hosts...) + d.hosts = hosts d.mut.Unlock() } @@ -65,7 +65,7 @@ func NewL2TP(hosts []string, apiPort, apiSSLPort, user, pass string, log *logger wg: &sync.WaitGroup{}, discoveredHosts: &discoveredHosts{ mut: &sync.Mutex{}, - hosts: make([]string, 0), + hosts: make(map[string]string), }, } @@ -76,7 +76,8 @@ func NewL2TP(hosts []string, apiPort, apiSSLPort, user, pass string, log *logger return l } -func (l *L2TP) GetIPAddresses() ([]string, error) { +// GetIPAddresses returns a list of host--interface name and its ip address +func (l *L2TP) GetIPAddresses() (map[string]string, error) { for _, h := range l.hosts { h := h @@ -103,10 +104,10 @@ func (l *L2TP) GetIPAddresses() ([]string, error) { return l.discoveredHosts.hosts, nil } -func (l *L2TP) fetchRouterIPs(host string) ([]string, error) { +func (l *L2TP) fetchRouterIPs(host string) (map[string]string, error) { var ( tunnelNames []string - remoteIPs []string + remoteIPs = make(map[string]string) cl *routeros.Client err error ) @@ -150,7 +151,7 @@ func (l *L2TP) fetchRouterIPs(host string) ([]string, error) { } for _, r := range res.Re { - remoteIPs = append(remoteIPs, r.Map["network"]) + remoteIPs[fmt.Sprintf("%s--%s", host, tun)] = r.Map["network"] } } From a2d74147fc4ca9bf9513f2da9f50aa5c805402dc Mon Sep 17 00:00:00 2001 From: ZeljkoBenovic Date: Mon, 9 Sep 2024 00:31:43 +0200 Subject: [PATCH 2/2] added system service cli option --- README.md | 10 +++- main.go | 23 +++++++- pkg/config/config.go | 12 +++- pkg/service/service.go | 123 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 pkg/service/service.go 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 +}