From 059f4c0097a8a7b70423ab79db0135ab23df2901 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 1 Feb 2023 23:45:23 -0600 Subject: [PATCH] added some features - Added `cron` command to run lists with `cron` time specifed - Changed `-c` flag to `-f` flag for passing config file - Modified some config keys - cmdArgs -> Args - Got rid of `hosts.config` - better SSH handling - respects values in config file --- .goreleaser.yaml | 2 +- README.md | 36 +- cmd/backup.go | 17 +- cmd/cron.go | 24 ++ cmd/exec.go | 13 +- cmd/root.go | 33 +- examples/backy.yaml | 27 +- go.mod | 16 +- go.sum | 38 +- pkg/backy/backy.go | 646 +++++++------------------------ pkg/backy/config.go | 276 +++++++++++++ pkg/backy/cron.go | 28 ++ pkg/backy/mongo.go | 96 +++++ pkg/backy/notification.go | 91 +++++ pkg/backy/ssh.go | 317 ++++++++++++--- pkg/backy/types.go | 100 +++-- pkg/backy/utils.go | 209 ++++++++++ pkg/logging/logging.go | 55 +++ pkg/notification/notification.go | 101 ----- 19 files changed, 1349 insertions(+), 776 deletions(-) create mode 100644 cmd/cron.go create mode 100644 pkg/backy/config.go create mode 100644 pkg/backy/cron.go create mode 100644 pkg/backy/mongo.go create mode 100644 pkg/backy/notification.go create mode 100644 pkg/backy/utils.go delete mode 100644 pkg/notification/notification.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 5ae1505..2a0be51 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -14,7 +14,7 @@ archives: - format: tar.gz # this name template makes the OS and Arch compatible with the results of uname. name_template: >- - {{ .ProjectName }}_ + {{ .ProjectName }}_{{ .Version }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 diff --git a/README.md b/README.md index 2863edc..aa704c0 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ To run a config: `backy backup` Or to use a specific file: -```backy backup -c /path/to/file``` +```backy backup -f /path/to/file``` If you leave the config path blank, the following paths will be searched in order: @@ -38,28 +38,26 @@ Create a file at `~/.config/backy.yaml`: commands: stop-docker-container: cmd: docker - cmdArgs: + Args: - compose - -f /some/path/to/docker-compose.yaml - down - # if host is not defined, + # if host is not defined, cmd will be run locally host: some-host - env: ~/path/to/env/file backup-docker-container-script: cmd: /path/to/script + # The host has to be defined in the config file host: some-host - env: ~/path/to/env/file shell-cmd: cmd: rsync shell: bash - cmdArgs: + Args: - -av some-host:/path/to/data ~/Docker/Backups/docker-data hostname: cmd: hostname cmd-configs: - # this can be any name you want - cmds-to-run: + cmds-to-run: # this can be any name you want # all commands have to be defined order: - stop-docker-container @@ -68,7 +66,9 @@ cmd-configs: - hostname notifications: - matrix + name: backup-some-server hostname: + name: hostname order: - hostname notifications: @@ -76,22 +76,26 @@ cmd-configs: hosts: some-host: - config: - usefile: true - user: root - private-key-path: + hostname: some-hostname + config: ~/.ssh/config + user: user + privatekeypath: /path/to/private/key + port: 22 + password: + logging: verbose: true file: /path/to/logs/commands.log + console: false + cmd-std-out: false notifications: prod-email: id: prod-email type: mail - host: yourhost.tld - port: 587 + host: yourhost.tld:port senderAddress: email@domain.tld to: - admin@domain.tld @@ -100,11 +104,9 @@ notifications: matrix: id: matrix type: matrix - homeserver: your-home-server.tld + home-server: your-home-server.tld room-id: room-id access-token: your-access-token user-id: your-user-id ``` - -Note, let me know if a path lookup fails due to using Go's STDLib `os` diff --git a/cmd/backup.go b/cmd/backup.go index f95e527..e715580 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -1,8 +1,11 @@ +// backup.go +// Copyright (C) Andrew Woodlee 2023 +// License: Apache-2.0 + package cmd import ( "git.andrewnw.xyz/CyberShell/backy/pkg/backy" - "git.andrewnw.xyz/CyberShell/backy/pkg/notification" "github.com/spf13/cobra" ) @@ -18,17 +21,17 @@ var ( ) // Holds command list to run -var cmdList []string +var cmdLists []string func init() { - backupCmd.Flags().StringSliceVarP(&cmdList, "lists", "l", nil, "Accepts a comma-separated names of command lists to execute.") + backupCmd.Flags().StringSliceVarP(&cmdLists, "lists", "l", nil, "Accepts a comma-separated names of command lists to execute.") } func Backup(cmd *cobra.Command, args []string) { - - config := backy.ReadAndParseConfigFile(cfgFile, cmdList) - notification.SetupNotify(*config) - config.RunBackyConfig() + backyConfOpts := backy.NewOpts(cfgFile, backy.AddCommandLists(cmdLists)) + backyConfOpts.InitConfig() + config := backy.ReadConfig(backyConfOpts) + config.RunBackyConfig("") } diff --git a/cmd/cron.go b/cmd/cron.go new file mode 100644 index 0000000..143f3e8 --- /dev/null +++ b/cmd/cron.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "git.andrewnw.xyz/CyberShell/backy/pkg/backy" + + "github.com/spf13/cobra" +) + +var ( + cronCmd = &cobra.Command{ + Use: "cron command ...", + Short: "Runs commands defined in config file.", + Long: `Cron executes commands at the time defined in config file.`, + Run: cron, + } +) + +func cron(cmd *cobra.Command, args []string) { + + opts := backy.NewOpts(cfgFile, backy.UseCron()) + opts.InitConfig() + + backy.ReadConfig(opts).Cron() +} diff --git a/cmd/exec.go b/cmd/exec.go index aa244ef..49ea8c8 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -1,3 +1,7 @@ +// exec.go +// Copyright (C) Andrew Woodlee 2023 +// License: Apache-2.0 + package cmd import ( @@ -9,7 +13,7 @@ import ( var ( execCmd = &cobra.Command{ - Use: "exec command1 command2", + Use: "exec command ...", Short: "Runs commands defined in config file.", Long: `Exec executes commands defined in config file.`, Run: execute, @@ -23,9 +27,8 @@ func execute(cmd *cobra.Command, args []string) { } opts := backy.NewOpts(cfgFile, backy.AddCommands(args)) - - commands := opts.GetCmdsInConfigFile() - - commands.ExecuteCmds() + opts.InitConfig() + // opts.InitMongo() + backy.ReadConfig(opts).ExecuteCmds() } diff --git a/cmd/root.go b/cmd/root.go index 2dcbdcd..fbca535 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,11 +7,8 @@ package cmd import ( "fmt" "os" - "path" - "strings" "github.com/spf13/cobra" - "github.com/spf13/viper" ) var ( @@ -35,35 +32,9 @@ func Execute() { } func init() { - cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file to read from") + rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file to read from") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level") - rootCmd.AddCommand(backupCmd) - rootCmd.AddCommand(execCmd) -} - -func initConfig() { - backyConfig := viper.New() - if cfgFile != strings.TrimSpace("") { - // Use config file from the flag. - backyConfig.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := os.UserHomeDir() - cobra.CheckErr(err) - - configPath := path.Join(home, ".config", "backy") - // Search config in config directory with name "backy" (without extension). - backyConfig.AddConfigPath(configPath) - backyConfig.SetConfigType("yaml") - backyConfig.SetConfigName("backy") - } - - backyConfig.AutomaticEnv() - - if err := backyConfig.ReadInConfig(); err == nil { - // fmt.Println("Using config file:", backyConfig.ConfigFileUsed()) - } + rootCmd.AddCommand(backupCmd, execCmd, cronCmd) } diff --git a/examples/backy.yaml b/examples/backy.yaml index bc0629d..e1bb24e 100644 --- a/examples/backy.yaml +++ b/examples/backy.yaml @@ -1,28 +1,26 @@ commands: stop-docker-container: cmd: docker - cmdArgs: + Args: - compose - -f /some/path/to/docker-compose.yaml - down - # if host is not defined, + # if host is not defined, cmd will be run locally host: some-host - env: ~/path/to/env/file backup-docker-container-script: cmd: /path/to/script + # The host has to be defined in the config file host: some-host - env: ~/path/to/env/file shell-cmd: cmd: rsync shell: bash - cmdArgs: + Args: - -av some-host:/path/to/data ~/Docker/Backups/docker-data hostname: cmd: hostname cmd-configs: - # this can be any name you want - cmds-to-run: + cmds-to-run: # this can be any name you want # all commands have to be defined order: - stop-docker-container @@ -31,7 +29,9 @@ cmd-configs: - hostname notifications: - matrix + name: backup-some-server hostname: + name: hostname order: - hostname notifications: @@ -39,14 +39,19 @@ cmd-configs: hosts: some-host: - config: - usefile: true - user: root - private-key-path: + hostname: some-hostname + config: ~/.ssh/config + user: user + privatekeypath: /path/to/private/key + port: 22 + password: + logging: verbose: true file: /path/to/logs/commands.log + console: false + cmd-std-out: false notifications: diff --git a/go.mod b/go.mod index a96cf0d..c6418df 100644 --- a/go.mod +++ b/go.mod @@ -3,30 +3,38 @@ module git.andrewnw.xyz/CyberShell/backy go 1.19 require ( + github.com/go-co-op/gocron v1.18.0 + github.com/joho/godotenv v1.4.0 github.com/kevinburke/ssh_config v1.2.0 + github.com/mattn/go-isatty v0.0.17 github.com/nikoksr/notify v0.36.0 + github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.28.0 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.14.0 + go.mongodb.org/mongo-driver v1.11.1 golang.org/x/crypto v0.5.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 maunium.net/go/mautrix v0.13.0 + mvdan.cc/sh/v3 v3.6.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/golang/snappy v0.0.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect + github.com/klauspost/compress v1.13.6 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect @@ -38,6 +46,10 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.1 // indirect + github.com/xdg-go/stringprep v1.0.3 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect golang.org/x/net v0.5.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.4.0 // indirect diff --git a/go.sum b/go.sum index d074898..4ba7623 100644 --- a/go.sum +++ b/go.sum @@ -58,9 +58,11 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-co-op/gocron v1.18.0 h1:SxTyJ5xnSN4byCq7b10LmmszFdxQlSQJod8s3gbnXxA= +github.com/go-co-op/gocron v1.18.0/go.mod h1:sD/a0Aadtw5CpflUJ/lpP9Vfdk979Wl1Sg33HPHg0FY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -90,6 +92,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -129,6 +133,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -136,9 +142,11 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -153,6 +161,8 @@ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPn github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/nikoksr/notify v0.36.0 h1:OeO/COtxZYLjtFuxBhpeVLfCFdGt48KKgOHKu43w8H0= github.com/nikoksr/notify v0.36.0/go.mod h1:U5h6rVleLTcAJASy7kRdD4vtsFBBxirWQKYX8NJ4jcw= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= @@ -165,8 +175,10 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= @@ -190,6 +202,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -202,14 +215,25 @@ github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8= +go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -223,6 +247,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -289,6 +314,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -310,6 +336,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -344,6 +371,7 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -360,6 +388,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -527,6 +557,8 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= maunium.net/go/mautrix v0.13.0 h1:CRdpMFc1kDSNnCZMcqahR9/pkDy/vgRbd+fHnSCl6Yg= maunium.net/go/mautrix v0.13.0/go.mod h1:gYMQPsZ9lQpyKlVp+DGwOuc9LIcE/c8GZW2CvKHISgM= +mvdan.cc/sh/v3 v3.6.0 h1:gtva4EXJ0dFNvl5bHjcUEvws+KRcDslT8VKheTYkbGU= +mvdan.cc/sh/v3 v3.6.0/go.mod h1:U4mhtBLZ32iWhif5/lD+ygy1zrgaQhUu+XFy7C8+TTA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/pkg/backy/backy.go b/pkg/backy/backy.go index 161bc13..0d4bb4c 100644 --- a/pkg/backy/backy.go +++ b/pkg/backy/backy.go @@ -6,187 +6,210 @@ package backy import ( "bufio" "bytes" - "errors" + "context" "fmt" "io" "os" "os/exec" - "os/user" - "path/filepath" - "strings" - "time" - "git.andrewnw.xyz/CyberShell/backy/pkg/logging" "github.com/rs/zerolog" - "github.com/spf13/viper" - "golang.org/x/crypto/ssh" - "gopkg.in/natefinch/lumberjack.v2" ) -var requiredKeys = []string{"commands", "cmd-configs"} +var requiredKeys = []string{"commands", "cmd-configs", "logging"} var Sprintf = fmt.Sprintf -func (c *BackyConfigOpts) LogLvl(level string) BackyOptionFunc { - - return func(bco *BackyConfigOpts) { - c.BackyLogLvl = &level - } -} - -func AddCommands(commands []string) BackyOptionFunc { - return func(bco *BackyConfigOpts) { - bco.executeCmds = append(bco.executeCmds, commands...) - } -} - -func NewOpts(configFilePath string, opts ...BackyOptionFunc) *BackyConfigOpts { - b := &BackyConfigOpts{} - b.ConfigFilePath = configFilePath - for _, opt := range opts { - if opt != nil { - opt(b) - } - } - return b -} - -/* -NewConfig initializes new config that holds information from the config file -*/ -func NewConfig() *BackyConfigFile { - return &BackyConfigFile{ - Cmds: make(map[string]*Command), - CmdConfigLists: make(map[string]*CmdConfig), - Hosts: make(map[string]Host), - Notifications: make(map[string]*NotificationsConfig), - } -} - -type environmentVars struct { - file string - env []string -} - // RunCmd runs a Command. // The environment of local commands will be the machine's environment plus any extra // variables specified in the Env file or Environment. -// -// If host is specifed, the command will call ConnectToSSHHost, -// returning a client that is used to run the command. -func (command *Command) RunCmd(log *zerolog.Logger) { +// Dir can also be specified for local commands. +func (command *Command) RunCmd(log *zerolog.Logger) error { - var envVars = environmentVars{ - file: command.Env, - env: command.Environment, - } + var ( + ArgsStr string + cmdOutBuf bytes.Buffer + cmdOutWriters io.Writer + + envVars = environmentVars{ + file: command.Env, + env: command.Environment, + } + ) envVars.env = append(envVars.env, os.Environ()...) - var cmdArgsStr string - for _, v := range command.CmdArgs { - cmdArgsStr += fmt.Sprintf(" %s", v) - } - var hostStr string - if command.Host != nil { - hostStr = *command.Host + for _, v := range command.Args { + ArgsStr += fmt.Sprintf(" %s", v) } - log.Info().Str("Command", fmt.Sprintf("Running command: %s %s on host %s", command.Cmd, cmdArgsStr, hostStr)).Send() if command.Host != nil { - command.RemoteHost.Host = *command.Host - command.RemoteHost.Port = 22 + log.Info().Str("Command", fmt.Sprintf("Running command: %s %s on host %s", command.Cmd, ArgsStr, *command.Host)).Send() + sshc, err := command.RemoteHost.ConnectToSSHHost(log) if err != nil { - log.Err(fmt.Errorf("ssh dial: %w", err)).Send() + return err } defer sshc.Close() commandSession, err := sshc.NewSession() if err != nil { log.Err(fmt.Errorf("new ssh session: %w", err)).Send() + return err } defer commandSession.Close() injectEnvIntoSSH(envVars, commandSession, log) cmd := command.Cmd - for _, a := range command.CmdArgs { + for _, a := range command.Args { cmd += " " + a } + cmdOutWriters = io.MultiWriter(&cmdOutBuf) - var stdoutBuf, stderrBuf bytes.Buffer - commandSession.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) - commandSession.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) + if IsCmdStdOutEnabled() { + cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf) + } + + commandSession.Stdout = cmdOutWriters + commandSession.Stderr = cmdOutWriters err = commandSession.Run(cmd) - log.Info().Bytes(fmt.Sprintf("%s stdout", command.Cmd), stdoutBuf.Bytes()).Send() - log.Info().Bytes(fmt.Sprintf("%s stderr", command.Cmd), stderrBuf.Bytes()).Send() + outScanner := bufio.NewScanner(&cmdOutBuf) + for outScanner.Scan() { + outMap := make(map[string]interface{}) + outMap["cmd"] = cmd + outMap["output"] = outScanner.Text() + log.Info().Fields(outMap).Send() + } if err != nil { log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() + return err } } else { cmdExists := command.checkCmdExists() if !cmdExists { - log.Error().Str(command.Cmd, "not found").Send() + log.Info().Str(command.Cmd, "not found").Send() } - // shell := "/bin/bash" + var err error if command.Shell != "" { - cmdArgsStr = fmt.Sprintf("%s %s", command.Cmd, cmdArgsStr) - localCMD := exec.Command(command.Shell, "-c", cmdArgsStr) + log.Info().Str("Command", fmt.Sprintf("Running command: %s %s on local machine in %s", command.Cmd, ArgsStr, command.Shell)).Send() + ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr) + localCMD := exec.Command(command.Shell, "-c", ArgsStr) if command.Dir != nil { localCMD.Dir = *command.Dir } - - var stdoutBuf, stderrBuf bytes.Buffer - localCMD.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) - localCMD.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) injectEnvIntoLocalCMD(envVars, localCMD, log) + + cmdOutWriters = io.MultiWriter(&cmdOutBuf) + + if IsCmdStdOutEnabled() { + cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf) + } + + localCMD.Stdout = cmdOutWriters + localCMD.Stderr = cmdOutWriters err = localCMD.Run() - log.Info().Bytes(fmt.Sprintf("%s stdout", command.Cmd), stdoutBuf.Bytes()).Send() - log.Info().Bytes(fmt.Sprintf("%s stderr", command.Cmd), stderrBuf.Bytes()).Send() + outScanner := bufio.NewScanner(&cmdOutBuf) + for outScanner.Scan() { + outMap := make(map[string]interface{}) + outMap["cmd"] = command.Cmd + outMap["output"] = outScanner.Text() + log.Info().Fields(outMap).Send() + } if err != nil { log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() + return err } - return + return nil } - localCMD := exec.Command(command.Cmd, command.CmdArgs...) + log.Info().Str("Command", fmt.Sprintf("Running command: %s %s on local machine", command.Cmd, ArgsStr)).Send() + + localCMD := exec.Command(command.Cmd, command.Args...) if command.Dir != nil { localCMD.Dir = *command.Dir } - var stdoutBuf, stderrBuf bytes.Buffer - localCMD.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) - localCMD.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) injectEnvIntoLocalCMD(envVars, localCMD, log) + cmdOutWriters = io.MultiWriter(&cmdOutBuf) + + if IsCmdStdOutEnabled() { + cmdOutWriters = io.MultiWriter(os.Stdout, &cmdOutBuf) + } + localCMD.Stdout = cmdOutWriters + localCMD.Stderr = cmdOutWriters err = localCMD.Run() - log.Info().Bytes(fmt.Sprintf("%s stdout", command.Cmd), stdoutBuf.Bytes()).Send() - log.Info().Bytes(fmt.Sprintf("%s stderr", command.Cmd), stderrBuf.Bytes()).Send() + outScanner := bufio.NewScanner(&cmdOutBuf) + for outScanner.Scan() { + outMap := make(map[string]interface{}) + outMap["cmd"] = command.Cmd + outMap["output"] = outScanner.Text() + log.Info().Fields(outMap).Send() + } if err != nil { log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() + return err } } + return nil } -func cmdListWorker(id int, jobs <-chan *CmdConfig, config *BackyConfigFile, results chan<- string) { - for j := range jobs { - for _, cmd := range j.Order { +func cmdListWorker(id int, jobs <-chan *CmdList, config *BackyConfigFile, results chan<- string) { + for list := range jobs { + var currentCmd string + fieldsMap := make(map[string]interface{}) + fieldsMap["list"] = list.Name + cmdLog := config.Logger.Info() + var count int + var Msg string + for _, cmd := range list.Order { + currentCmd = config.Cmds[cmd].Cmd + fieldsMap["cmd"] = config.Cmds[cmd].Cmd + cmdLog.Fields(fieldsMap).Send() cmdToRun := config.Cmds[cmd] - cmdToRun.RunCmd(&config.Logger) + cmdLogger := config.Logger.With(). + Str("backy-cmd", cmd). + Logger() + runOutErr := cmdToRun.RunCmd(&cmdLogger) + count++ + if runOutErr != nil { + if list.NotifyConfig != nil { + notifySendErr := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s failed on command %s ", list.Name, cmd), + fmt.Sprintf("List %s failed on command %s running command %s. \n Error: %v", list.Name, cmd, currentCmd, runOutErr)) + if notifySendErr != nil { + config.Logger.Err(notifySendErr).Send() + } + } + config.Logger.Err(runOutErr).Send() + break + } else { + + if count == len(list.Order) { + Msg += fmt.Sprintf("%s ", cmd) + if list.NotifyConfig != nil { + err := list.NotifyConfig.Send(context.Background(), fmt.Sprintf("List %s succeded", list.Name), + fmt.Sprintf("Command list %s was completed successfully. The following commands ran:\n %s", list.Name, Msg)) + if err != nil { + config.Logger.Err(err).Send() + } + } + } else { + Msg += fmt.Sprintf("%s, ", cmd) + } + } } + results <- "done" } } // RunBackyConfig runs a command list from the BackyConfigFile. -func (config *BackyConfigFile) RunBackyConfig() { +func (config *BackyConfigFile) RunBackyConfig(cron string) { configListsLen := len(config.CmdConfigLists) - jobs := make(chan *CmdConfig, configListsLen) + listChan := make(chan *CmdList, configListsLen) results := make(chan string) - // configChan := make(chan map[string]Command) // This starts up 3 workers, initially blocked // because there are no jobs yet. for w := 1; w <= 3; w++ { - go cmdListWorker(w, jobs, config, results) + go cmdListWorker(w, listChan, config, results) } @@ -194,10 +217,15 @@ func (config *BackyConfigFile) RunBackyConfig() { // channel to indicate that's all the work we have. // configChan <- config.Cmds for _, cmdConfig := range config.CmdConfigLists { - jobs <- cmdConfig - // fmt.Println("sent job", config.Order) + if cron != "" { + if cron == cmdConfig.Cron { + listChan <- cmdConfig + } + } else { + listChan <- cmdConfig + } } - close(jobs) + close(listChan) for a := 1; a <= configListsLen; a++ { <-results @@ -210,413 +238,3 @@ func (config *BackyConfigFile) ExecuteCmds() { cmd.RunCmd(&config.Logger) } } - -// ReadAndParseConfigFile validates and reads the config file. -func ReadAndParseConfigFile(configFile string, lists []string) *BackyConfigFile { - - backyConfigFile := NewConfig() - - backyViper := viper.New() - - if configFile != "" { - backyViper.SetConfigFile(configFile) - } else { - backyViper.SetConfigName("backy.yaml") // name of config file (with extension) - backyViper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name - backyViper.AddConfigPath(".") // optionally look for config in the working directory - backyViper.AddConfigPath("$HOME/.config/backy") // call multiple times to add many search paths - } - err := backyViper.ReadInConfig() // Find and read the config file - if err != nil { // Handle errors reading the config file - panic(fmt.Errorf("fatal error reading config file %s: %w", backyViper.ConfigFileUsed(), err)) - } - - CheckConfigValues(backyViper) - - for _, l := range lists { - if !backyViper.IsSet(getCmdListFromConfig(l)) { - logging.ExitWithMSG(Sprintf("list %s not found", l), 1, nil) - } - } - - var backyLoggingOpts *viper.Viper - backyLoggingOptsSet := backyViper.IsSet("logging") - if backyLoggingOptsSet { - backyLoggingOpts = backyViper.Sub("logging") - } - verbose := backyLoggingOpts.GetBool("verbose") - - logFile := backyLoggingOpts.GetString("file") - if verbose { - zerolog.SetGlobalLevel(zerolog.InfoLevel) - globalLvl := zerolog.GlobalLevel().String() - os.Setenv("BACKY_LOGLEVEL", globalLvl) - } - output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC1123} - output.FormatLevel = func(i interface{}) string { - return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) - } - output.FormatMessage = func(i interface{}) string { - return fmt.Sprintf("%s", i) - } - output.FormatFieldName = func(i interface{}) string { - return fmt.Sprintf("%s: ", i) - } - output.FormatFieldValue = func(i interface{}) string { - return strings.ToUpper(fmt.Sprintf("%s", i)) - } - - fileLogger := &lumberjack.Logger{ - MaxSize: 500, // megabytes - MaxBackups: 3, - MaxAge: 28, //days - Compress: true, // disabled by default - } - if strings.TrimSpace(logFile) != "" { - fileLogger.Filename = logFile - } else { - fileLogger.Filename = "./backy.log" - } - - // UNIX Time is faster and smaller than most timestamps - zerolog.TimeFieldFormat = zerolog.TimeFormatUnix - // zerolog.TimeFieldFormat = time.RFC1123 - writers := zerolog.MultiLevelWriter(os.Stdout, fileLogger) - log := zerolog.New(writers).With().Timestamp().Logger() - - backyConfigFile.Logger = log - - commandsMap := backyViper.GetStringMapString("commands") - commandsMapViper := backyViper.Sub("commands") - unmarshalErr := commandsMapViper.Unmarshal(&backyConfigFile.Cmds) - if unmarshalErr != nil { - panic(fmt.Errorf("error unmarshalling cmds struct: %w", unmarshalErr)) - } - - var cmdNames []string - for k := range commandsMap { - cmdNames = append(cmdNames, k) - } - hostConfigsMap := make(map[string]*viper.Viper) - - for _, cmdName := range cmdNames { - subCmd := backyViper.Sub(getNestedConfig("commands", cmdName)) - - hostSet := subCmd.IsSet("host") - host := subCmd.GetString("host") - - if hostSet { - log.Debug().Timestamp().Str(cmdName, "host is set").Str("host", host).Send() - if backyViper.IsSet(getNestedConfig("hosts", host)) { - hostconfig := backyViper.Sub(getNestedConfig("hosts", host)) - hostConfigsMap[host] = hostconfig - } - } else { - log.Debug().Timestamp().Str(cmdName, "host is not set").Send() - } - - } - - cmdListCfg := backyViper.Sub("cmd-configs") - unmarshalErr = cmdListCfg.Unmarshal(&backyConfigFile.CmdConfigLists) - if unmarshalErr != nil { - panic(fmt.Errorf("error unmarshalling cmd list struct: %w", unmarshalErr)) - } - var cmdNotFoundSliceErr []error - for cmdListName, cmdList := range backyConfigFile.CmdConfigLists { - for _, cmdInList := range cmdList.Order { - _, cmdNameFound := backyConfigFile.Cmds[cmdInList] - if !cmdNameFound { - cmdNotFoundStr := fmt.Sprintf("command %s is not defined in config file", cmdInList) - cmdNotFoundErr := errors.New(cmdNotFoundStr) - cmdNotFoundSliceErr = append(cmdNotFoundSliceErr, cmdNotFoundErr) - } else { - log.Info().Str(cmdInList, "found in "+cmdListName).Send() - } - } - for _, notificationID := range cmdList.Notifications { - - cmdList.NotificationsConfig = make(map[string]*NotificationsConfig) - notifConfig := backyViper.Sub(getNestedConfig("notifications", notificationID)) - config := &NotificationsConfig{ - Config: notifConfig, - Enabled: true, - } - cmdList.NotificationsConfig[notificationID] = config - // First we get a "copy" of the entry - if entry, ok := cmdList.NotificationsConfig[notificationID]; ok { - - // Then we modify the copy - entry.Config = notifConfig - entry.Enabled = true - - // Then we reassign the copy - cmdList.NotificationsConfig[notificationID] = entry - } - backyConfigFile.CmdConfigLists[cmdListName].NotificationsConfig[notificationID] = config - - } - } - - if len(lists) > 0 { - for l := range backyConfigFile.CmdConfigLists { - if !contains(lists, l) { - delete(backyConfigFile.CmdConfigLists, l) - } - } - } - - if len(cmdNotFoundSliceErr) > 0 { - var cmdNotFoundErrorLog = log.Fatal() - for _, err := range cmdNotFoundSliceErr { - if err != nil { - cmdNotFoundErrorLog.Err(err) - } - } - cmdNotFoundErrorLog.Send() - } - - var notificationsMap = make(map[string]interface{}) - if backyViper.IsSet("notifications") { - notificationsMap = backyViper.GetStringMap("notifications") - for id := range notificationsMap { - notifConfig := backyViper.Sub(getNestedConfig("notifications", id)) - config := &NotificationsConfig{ - Config: notifConfig, - Enabled: true, - } - backyConfigFile.Notifications[id] = config - } - } - - return backyConfigFile -} - -// GetCmdsInConfigFile validates and reads the config file for commands. -func (opts *BackyConfigOpts) GetCmdsInConfigFile() *BackyConfigFile { - - backyConfigFile := NewConfig() - - backyViper := viper.New() - - if opts.ConfigFilePath != strings.TrimSpace("") { - backyViper.SetConfigFile(opts.ConfigFilePath) - } else { - backyViper.SetConfigName("backy.yaml") // name of config file (with extension) - backyViper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name - backyViper.AddConfigPath(".") // optionally look for config in the working directory - backyViper.AddConfigPath("$HOME/.config/backy") // call multiple times to add many search paths - } - err := backyViper.ReadInConfig() // Find and read the config file - if err != nil { // Handle errors reading the config file - panic(fmt.Errorf("fatal error reading config file %s: %w", backyViper.ConfigFileUsed(), err)) - } - - CheckConfigValues(backyViper) - for _, c := range opts.executeCmds { - if !backyViper.IsSet(getCmdFromConfig(c)) { - logging.ExitWithMSG(Sprintf("command %s is not in config file %s", c, backyViper.ConfigFileUsed()), 1, nil) - } - } - var backyLoggingOpts *viper.Viper - backyLoggingOptsSet := backyViper.IsSet("logging") - if backyLoggingOptsSet { - backyLoggingOpts = backyViper.Sub("logging") - } - verbose := backyLoggingOpts.GetBool("verbose") - - logFile := backyLoggingOpts.GetString("file") - if verbose { - zerolog.SetGlobalLevel(zerolog.InfoLevel) - globalLvl := zerolog.GlobalLevel().String() - os.Setenv("BACKY_LOGLEVEL", globalLvl) - } - output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC1123} - output.FormatLevel = func(i interface{}) string { - return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) - } - output.FormatMessage = func(i interface{}) string { - return fmt.Sprintf("%s", i) - } - output.FormatFieldName = func(i interface{}) string { - return fmt.Sprintf("%s: ", i) - } - output.FormatFieldValue = func(i interface{}) string { - return strings.ToUpper(fmt.Sprintf("%s", i)) - } - - fileLogger := &lumberjack.Logger{ - MaxSize: 500, // megabytes - MaxBackups: 3, - MaxAge: 28, //days - Compress: true, // disabled by default - } - if strings.TrimSpace(logFile) != "" { - fileLogger.Filename = logFile - } else { - fileLogger.Filename = "./backy.log" - } - - // UNIX Time is faster and smaller than most timestamps - zerolog.TimeFieldFormat = zerolog.TimeFormatUnix - // zerolog.TimeFieldFormat = time.RFC1123 - writers := zerolog.MultiLevelWriter(os.Stdout, fileLogger) - log := zerolog.New(writers).With().Timestamp().Logger() - - backyConfigFile.Logger = log - - commandsMap := backyViper.GetStringMapString("commands") - commandsMapViper := backyViper.Sub("commands") - unmarshalErr := commandsMapViper.Unmarshal(&backyConfigFile.Cmds) - if unmarshalErr != nil { - panic(fmt.Errorf("error unmarshalling cmds struct: %w", unmarshalErr)) - } - - var cmdNames []string - for c := range commandsMap { - if contains(opts.executeCmds, c) { - cmdNames = append(cmdNames, c) - } - if !contains(opts.executeCmds, c) { - delete(backyConfigFile.Cmds, c) - } - } - - hostConfigsMap := make(map[string]*viper.Viper) - - for _, cmdName := range cmdNames { - subCmd := backyViper.Sub(getNestedConfig("commands", cmdName)) - - hostSet := subCmd.IsSet("host") - host := subCmd.GetString("host") - - if hostSet { - log.Debug().Timestamp().Str(cmdName, "host is set").Str("host", host).Send() - if backyViper.IsSet(getNestedConfig("hosts", host)) { - hostconfig := backyViper.Sub(getNestedConfig("hosts", host)) - hostConfigsMap[host] = hostconfig - } - } else { - log.Debug().Timestamp().Str(cmdName, "host is not set").Send() - } - - } - - return backyConfigFile -} - -func getNestedConfig(nestedConfig, key string) string { - return fmt.Sprintf("%s.%s", nestedConfig, key) -} - -func getCmdFromConfig(key string) string { - return fmt.Sprintf("commands.%s", key) -} -func getCmdListFromConfig(list string) string { - return fmt.Sprintf("cmd-configs.%s", list) -} - -func resolveDir(path string) (string, error) { - usr, err := user.Current() - if err != nil { - return path, err - } - dir := usr.HomeDir - if path == "~" { - // In case of "~", which won't be caught by the "else if" - path = dir - } else if strings.HasPrefix(path, "~/") { - // Use strings.HasPrefix so we don't match paths like - // "/something/~/something/" - path = filepath.Join(dir, path[2:]) - } - return path, nil -} - -func injectEnvIntoSSH(envVarsToInject environmentVars, process *ssh.Session, log *zerolog.Logger) { - if envVarsToInject.file != "" { - envPath, envPathErr := resolveDir(envVarsToInject.file) - if envPathErr != nil { - log.Err(envPathErr).Send() - } - file, err := os.Open(envPath) - if err != nil { - log.Err(err).Send() - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - envVar := scanner.Text() - envVarArr := strings.Split(envVar, "=") - process.Setenv(envVarArr[0], envVarArr[1]) - } - if err := scanner.Err(); err != nil { - log.Err(err).Send() - } - } - if len(envVarsToInject.env) > 0 { - for _, envVal := range envVarsToInject.env { - if strings.Contains(envVal, "=") { - envVarArr := strings.Split(envVal, "=") - process.Setenv(strings.ToUpper(envVarArr[0]), envVarArr[1]) - } - } - } -} - -func injectEnvIntoLocalCMD(envVarsToInject environmentVars, process *exec.Cmd, log *zerolog.Logger) { - if envVarsToInject.file != "" { - envPath, envPathErr := resolveDir(envVarsToInject.file) - if envPathErr != nil { - log.Error().Err(envPathErr).Send() - } - file, err := os.Open(envPath) - if err != nil { - log.Err(err).Send() - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - envVar := scanner.Text() - process.Env = append(process.Env, envVar) - } - if err := scanner.Err(); err != nil { - log.Err(err).Send() - } - } - if len(envVarsToInject.env) > 0 { - for _, envVal := range envVarsToInject.env { - if strings.Contains(envVal, "=") { - process.Env = append(process.Env, envVal) - } - } - } - envVarsToInject.env = append(envVarsToInject.env, os.Environ()...) -} - -func (cmd *Command) checkCmdExists() bool { - _, err := exec.LookPath(cmd.Cmd) - return err == nil -} - -func contains(s []string, e string) bool { - for _, a := range s { - if a == e { - return true - } - } - return false -} - -func CheckConfigValues(config *viper.Viper) { - - for _, key := range requiredKeys { - isKeySet := config.IsSet(key) - if !isKeySet { - logging.ExitWithMSG(Sprintf("Config key %s is not defined in %s", key, config.ConfigFileUsed()), 1, nil) - } - - } -} diff --git a/pkg/backy/config.go b/pkg/backy/config.go new file mode 100644 index 0000000..daa56ac --- /dev/null +++ b/pkg/backy/config.go @@ -0,0 +1,276 @@ +package backy + +import ( + "errors" + "fmt" + "os" + "path" + "strings" + + "git.andrewnw.xyz/CyberShell/backy/pkg/logging" + "github.com/joho/godotenv" + "github.com/mattn/go-isatty" + "github.com/rs/zerolog" + "github.com/spf13/viper" + "mvdan.cc/sh/v3/shell" +) + +// ReadConfig validates and reads the config file. +func ReadConfig(opts *BackyConfigOpts) *BackyConfigFile { + + if isatty.IsTerminal(os.Stdout.Fd()) { + os.Setenv("BACKY_TERM", "enabled") + } else if isatty.IsCygwinTerminal(os.Stdout.Fd()) { + os.Setenv("BACKY_TERM", "enabled") + } else { + os.Setenv("BACKY_TERM", "disabled") + } + + backyConfigFile := NewConfig() + backyViper := opts.viper + // loadEnv(backyViper) + envFileInConfigDir := fmt.Sprintf("%s/.env", path.Dir(backyViper.ConfigFileUsed())) + + envFileErr := godotenv.Load() + if envFileErr != nil { + _ = godotenv.Load(envFileInConfigDir) + } + if backyViper.GetBool(getNestedConfig("logging", "cmd-std-out")) { + os.Setenv("BACKY_STDOUT", "enabled") + } + + CheckConfigValues(backyViper) + for _, c := range opts.executeCmds { + if !backyViper.IsSet(getCmdFromConfig(c)) { + logging.ExitWithMSG(Sprintf("command %s is not in config file %s", c, backyViper.ConfigFileUsed()), 1, nil) + } + } + + for _, l := range opts.executeLists { + if !backyViper.IsSet(getCmdListFromConfig(l)) { + logging.ExitWithMSG(Sprintf("list %s not found", l), 1, nil) + } + } + + var backyLoggingOpts *viper.Viper + isBackyLoggingOptsSet := backyViper.IsSet("logging") + if isBackyLoggingOptsSet { + backyLoggingOpts = backyViper.Sub("logging") + } + verbose := backyLoggingOpts.GetBool("verbose") + + logFile := backyLoggingOpts.GetString("file") + + zerolog.SetGlobalLevel(zerolog.InfoLevel) + + if verbose { + zerolog.SetGlobalLevel(zerolog.DebugLevel) + globalLvl := zerolog.GlobalLevel() + os.Setenv("BACKY_LOGLEVEL", Sprintf("%x", globalLvl)) + } + + consoleLoggingEnabled := backyLoggingOpts.GetBool("console") + + // Other qualifiers can go here as well + if consoleLoggingEnabled { + os.Setenv("BACKY_CONSOLE_LOGGING", "enabled") + } else { + os.Setenv("BACKY_CONSOLE_LOGGING", "") + } + + writers := logging.SetLoggingWriters(backyLoggingOpts, logFile) + + log := zerolog.New(writers).With().Timestamp().Logger() + + backyConfigFile.Logger = log + + commandsMap := backyViper.GetStringMapString("commands") + commandsMapViper := backyViper.Sub("commands") + unmarshalErr := commandsMapViper.Unmarshal(&backyConfigFile.Cmds) + if unmarshalErr != nil { + panic(fmt.Errorf("error unmarshalling cmds struct: %w", unmarshalErr)) + } + + hostConfigsMap := make(map[string]*viper.Viper) + + for cmdName, cmdConf := range backyConfigFile.Cmds { + envFileErr := testFile(cmdConf.Env) + if envFileErr != nil { + backyConfigFile.Logger.Info().Str("cmd", cmdName).Err(envFileErr).Send() + os.Exit(1) + } + + host := cmdConf.Host + if host != nil { + if backyViper.IsSet(getNestedConfig("hosts", *host)) { + hostconfig := backyViper.Sub(getNestedConfig("hosts", *host)) + hostConfigsMap[*host] = hostconfig + } + } + } + + hostsMapViper := backyViper.Sub("hosts") + unmarshalErr = hostsMapViper.Unmarshal(&backyConfigFile.Hosts) + if unmarshalErr != nil { + panic(fmt.Errorf("error unmarshalling hosts struct: %w", unmarshalErr)) + } + for _, v := range backyConfigFile.Hosts { + + if v.JumpHost != "" { + proxyHost, defined := backyConfigFile.Hosts[v.JumpHost] + if defined { + v.ProxyHost = proxyHost + } + } + } + cmdListCfg := backyViper.Sub("cmd-configs") + unmarshalErr = cmdListCfg.Unmarshal(&backyConfigFile.CmdConfigLists) + if unmarshalErr != nil { + panic(fmt.Errorf("error unmarshalling cmd list struct: %w", unmarshalErr)) + } + + var cmdNotFoundSliceErr []error + for cmdListName, cmdList := range backyConfigFile.CmdConfigLists { + if opts.useCron { + cron := strings.TrimSpace(cmdList.Cron) + if cron == "" { + delete(backyConfigFile.CmdConfigLists, cmdListName) + } + } + for _, cmdInList := range cmdList.Order { + _, cmdNameFound := backyConfigFile.Cmds[cmdInList] + if !cmdNameFound { + cmdNotFoundStr := fmt.Sprintf("command %s in list %s is not defined in config file", cmdInList, cmdListName) + cmdNotFoundErr := errors.New(cmdNotFoundStr) + cmdNotFoundSliceErr = append(cmdNotFoundSliceErr, cmdNotFoundErr) + } + } + for _, notificationID := range cmdList.Notifications { + if !backyViper.IsSet(getNestedConfig("notifications", notificationID)) { + logging.ExitWithMSG(fmt.Sprintf("%s in list %s not found in notifications", notificationID, cmdListName), 1, nil) + } + } + } + + if len(cmdNotFoundSliceErr) > 0 { + var cmdNotFoundErrorLog = log.Fatal() + cmdNotFoundErrorLog.Errs("commands not found", cmdNotFoundSliceErr).Send() + } + + if opts.useCron && len(backyConfigFile.CmdConfigLists) > 0 { + log.Info().Msg("Starting cron mode...") + + } else if opts.useCron && (len(backyConfigFile.CmdConfigLists) == 0) { + logging.ExitWithMSG("No cron fields detected in any command lists", 1, nil) + } + + for c := range commandsMap { + if opts.executeCmds != nil && !contains(opts.executeCmds, c) { + delete(backyConfigFile.Cmds, c) + } + } + + if len(opts.executeLists) > 0 { + for l := range backyConfigFile.CmdConfigLists { + if !contains(opts.executeLists, l) { + delete(backyConfigFile.CmdConfigLists, l) + } + } + } + + var notificationsMap = make(map[string]interface{}) + if backyViper.IsSet("notifications") { + notificationsMap = backyViper.GetStringMap("notifications") + for id := range notificationsMap { + notifConfig := backyViper.Sub(getNestedConfig("notifications", id)) + config := &NotificationsConfig{ + Config: notifConfig, + Enabled: true, + } + backyConfigFile.Notifications[id] = config + } + } + + for _, cmd := range backyConfigFile.Cmds { + if cmd.Host != nil { + host, hostFound := backyConfigFile.Hosts[*cmd.Host] + if hostFound { + cmd.RemoteHost = host + cmd.RemoteHost.Host = host.Host + if host.HostName != "" { + cmd.RemoteHost.HostName = host.HostName + } + } else { + cmd.RemoteHost = &Host{Host: *cmd.Host} + } + } + + } + backyConfigFile.SetupNotify() + return backyConfigFile +} + +func getNestedConfig(nestedConfig, key string) string { + return fmt.Sprintf("%s.%s", nestedConfig, key) +} + +func getCmdFromConfig(key string) string { + return fmt.Sprintf("commands.%s", key) +} +func getCmdListFromConfig(list string) string { + return fmt.Sprintf("cmd-configs.%s", list) +} + +func (opts *BackyConfigOpts) InitConfig() { + if opts.viper != nil { + return + } + backyViper := viper.New() + + if strings.TrimSpace(opts.ConfigFilePath) != "" { + backyViper.SetConfigFile(opts.ConfigFilePath) + } else { + backyViper.SetConfigName("backy.yaml") // name of config file (with extension) + backyViper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name + backyViper.AddConfigPath(".") // optionally look for config in the working directory + backyViper.AddConfigPath("$HOME/.config/backy") // call multiple times to add many search paths + } + err := backyViper.ReadInConfig() // Find and read the config file + if err != nil { // Handle errors reading the config file + panic(fmt.Errorf("fatal error reading config file %s: %w", backyViper.ConfigFileUsed(), err)) + } + opts.viper = backyViper +} + +func loadEnv(backyViper *viper.Viper) { + envFileInConfigDir := fmt.Sprintf("%s/.env", path.Dir(backyViper.ConfigFileUsed())) + var backyEnv map[string]string + backyEnv, envFileErr := godotenv.Read() + + // envFile, envFileErr := os.Open(".env") + if envFileErr != nil { + backyEnv, _ = godotenv.Read(envFileInConfigDir) + } + envFileErr = godotenv.Load() + if envFileErr != nil { + _ = godotenv.Load(envFileInConfigDir) + } + + env := func(name string) string { + name = strings.ToUpper(name) + envVar, found := backyEnv[name] + if found { + return envVar + } + return "" + } + envVars := []string{"APP=${BACKY_APP}"} + + for indx, v := range envVars { + if strings.Contains(v, "$") || (strings.Contains(v, "${") && strings.Contains(v, "}")) { + out, _ := shell.Expand(v, env) + envVars[indx] = out + // println(out) + } + } +} diff --git a/pkg/backy/cron.go b/pkg/backy/cron.go new file mode 100644 index 0000000..b601b4b --- /dev/null +++ b/pkg/backy/cron.go @@ -0,0 +1,28 @@ +// cron.go +// Copyright (C) Andrew Woodlee 2023 +// License: Apache-2.0 + +package backy + +import ( + "strings" + "time" + + "github.com/go-co-op/gocron" +) + +func (conf *BackyConfigFile) Cron() { + s := gocron.NewScheduler(time.Local) + s.TagsUnique() + for _, config := range conf.CmdConfigLists { + if strings.TrimSpace(config.Cron) != "" { + _, err := s.CronWithSeconds(config.Cron).Tag(config.Name).Do(func(cron string) { + conf.RunBackyConfig(cron) + }, config.Cron) + if err != nil { + panic(err) + } + } + } + s.StartBlocking() +} diff --git a/pkg/backy/mongo.go b/pkg/backy/mongo.go new file mode 100644 index 0000000..8eaf195 --- /dev/null +++ b/pkg/backy/mongo.go @@ -0,0 +1,96 @@ +package backy + +import ( + "context" + "errors" + "fmt" + "os" + "time" + + "github.com/joho/godotenv" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readpref" +) + +const mongoConfigKey = "global.mongo" + +func (opts *BackyConfigOpts) InitMongo() { + + if !opts.viper.GetBool(getMongoConfigKey("enabled")) { + return + } + var ( + err error + client *mongo.Client + ) + + // TODO: Get uri and creditials from config + host := opts.viper.GetString(getMongoConfigKey("host")) + port := opts.viper.GetInt32(getMongoConfigKey("port")) + + client, err = mongo.NewClient(options.Client().ApplyURI(fmt.Sprintf("mongo://%s:%d", host, port))) + if opts.viper.GetBool(getMongoConfigKey("prod")) { + mongoEnvFileSet := opts.viper.IsSet(getMongoConfigKey("env")) + if mongoEnvFileSet { + getMongoConfigFromEnv(opts) + } + auth := options.Credential{} + auth.Password = opts.viper.GetString("global.mongo.password") + auth.Username = opts.viper.GetString("global.mongo.username") + client, err = mongo.NewClient(options.Client().SetAuth(auth).ApplyURI("mongodb://localhost:27017")) + + } + if err != nil { + opts.ConfigFile.Logger.Fatal().Err(err).Send() + } + ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer ctxCancel() + err = client.Connect(ctx) + if err != nil { + opts.ConfigFile.Logger.Fatal().Err(err).Send() + } + defer client.Disconnect(ctx) + err = client.Ping(ctx, readpref.Primary()) + if err != nil { + opts.ConfigFile.Logger.Fatal().Err(err).Send() + } + databases, err := client.ListDatabaseNames(ctx, bson.M{}) + if err != nil { + opts.ConfigFile.Logger.Fatal().Err(err).Send() + } + fmt.Println(databases) + backyDB := client.Database("backy") + backyDB.CreateCollection(context.Background(), "cmds") + backyDB.CreateCollection(context.Background(), "cmd-lists") + backyDB.CreateCollection(context.Background(), "logs") + opts.DB = backyDB +} + +func getMongoConfigFromEnv(opts *BackyConfigOpts) error { + mongoEnvFile, err := os.Open(opts.viper.GetString("global.mongo.env")) + if err != nil { + return err + } + mongoMap, mongoErr := godotenv.Parse(mongoEnvFile) + if mongoErr != nil { + return err + } + mongoPW, mongoPWFound := mongoMap["MONGO_PASSWORD"] + if !mongoPWFound { + return errors.New("MONGO_PASSWORD not set in " + mongoEnvFile.Name()) + } + mongoUser, mongoUserFound := mongoMap["MONGO_USER"] + if !mongoUserFound { + return errors.New("MONGO_PASSWORD not set in " + mongoEnvFile.Name()) + } + opts.viper.Set(mongoConfigKey+".password", mongoPW) + opts.viper.Set(mongoConfigKey+".username", mongoUser) + + return nil +} + +func getMongoConfigKey(key string) string { + return fmt.Sprintf("global.mongo.%s", key) +} diff --git a/pkg/backy/notification.go b/pkg/backy/notification.go new file mode 100644 index 0000000..d255335 --- /dev/null +++ b/pkg/backy/notification.go @@ -0,0 +1,91 @@ +// notification.go +// Copyright (C) Andrew Woodlee 2023 +// License: Apache-2.0 +package backy + +import ( + "fmt" + + "github.com/nikoksr/notify" + "github.com/nikoksr/notify/service/mail" + "github.com/nikoksr/notify/service/matrix" + "maunium.net/go/mautrix/id" +) + +type matrixStruct struct { + homeserver string + roomid id.RoomID + accessToken string + userId id.UserID +} + +type mailConfig struct { + senderaddress string + host string + to []string + username string + password string + port string +} + +func SetupCommandsNotifiers(backyConfig BackyConfigFile, ids ...string) { + +} + +// SetupNotify sets up notify instances for each command list. + +func (backyConfig *BackyConfigFile) SetupNotify() { + + for _, cmdConfig := range backyConfig.CmdConfigLists { + var services []notify.Notifier + for notifyID := range backyConfig.Notifications { + if contains(cmdConfig.Notifications, notifyID) { + + if backyConfig.Notifications[notifyID].Enabled { + config := backyConfig.Notifications[notifyID].Config + switch config.GetString("type") { + case "matrix": + mtrx := matrixStruct{ + userId: id.UserID(config.GetString("user-id")), + roomid: id.RoomID(config.GetString("room-id")), + accessToken: config.GetString("access-token"), + homeserver: config.GetString("homeserver"), + } + mtrxClient, _ := setupMatrix(mtrx) + services = append(services, mtrxClient) + case "mail": + mailCfg := mailConfig{ + senderaddress: config.GetString("senderaddress"), + password: config.GetString("password"), + username: config.GetString("username"), + to: config.GetStringSlice("to"), + host: config.GetString("host"), + port: fmt.Sprint(config.GetUint16("port")), + } + mailClient := setupMail(mailCfg) + services = append(services, mailClient) + } + } + } + } + cmdConfig.NotifyConfig = notify.NewWithServices(services...) + } + + // logging.ExitWithMSG("This was a test of notifications", 0, nil) +} + +func setupMatrix(config matrixStruct) (*matrix.Matrix, error) { + matrixClient, matrixErr := matrix.New(config.userId, config.roomid, config.homeserver, config.accessToken) + if matrixErr != nil { + panic(matrixErr) + } + return matrixClient, nil + +} + +func setupMail(config mailConfig) *mail.Mail { + mailClient := mail.New(config.senderaddress, config.host+":"+config.port) + mailClient.AuthenticateSMTP("", config.username, config.password, config.host) + mailClient.AddReceivers(config.to...) + return mailClient +} diff --git a/pkg/backy/ssh.go b/pkg/backy/ssh.go index 403ed12..eefc695 100644 --- a/pkg/backy/ssh.go +++ b/pkg/backy/ssh.go @@ -5,95 +5,288 @@ package backy import ( + "bufio" + "fmt" + "log" "os" "os/user" - "path/filepath" "strings" + "time" "github.com/kevinburke/ssh_config" + "github.com/pkg/errors" "github.com/rs/zerolog" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/knownhosts" ) +var ErrPrivateKeyFileFailedToOpen = errors.New("Private key file failed to open.") +var TS = strings.TrimSpace + // ConnectToSSHHost connects to a host by looking up the config values in the directory ~/.ssh/config -// Other than host, it does not yet respect other config values set in the backy config file. +// It uses any set values and looks up an unset values in the config files // It returns an ssh.Client used to run commands against. func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger) (*ssh.Client, error) { var sshClient *ssh.Client var connectErr error - khPath := filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts") - f, _ := os.Open(filepath.Join(os.Getenv("HOME"), ".ssh", "config")) - cfg, _ := ssh_config.Decode(f) - for _, host := range cfg.Hosts { - // var hostKey ssh.PublicKey - if host.Matches(remoteConfig.Host) { - var identityFile string - if remoteConfig.PrivateKeyPath == "" { - identityFile, _ = cfg.Get(remoteConfig.Host, "IdentityFile") - usr, _ := user.Current() - dir := usr.HomeDir - if identityFile == "~" { - // In case of "~", which won't be caught by the "else if" - identityFile = dir - } else if strings.HasPrefix(identityFile, "~/") { - // Use strings.HasPrefix so we don't match paths like - // "/something/~/something/" - identityFile = filepath.Join(dir, identityFile[2:]) - } - remoteConfig.PrivateKeyPath = filepath.Join(identityFile) - log.Debug().Str("Private key path", remoteConfig.PrivateKeyPath).Send() - } - remoteConfig.HostName, _ = cfg.Get(remoteConfig.Host, "HostName") - remoteConfig.User, _ = cfg.Get(remoteConfig.Host, "User") - if remoteConfig.HostName == "" { - port, _ := cfg.Get(remoteConfig.Host, "Port") - if port == "" { - port = "22" - } - // remoteConfig.HostName[0] = remoteConfig.Host + ":" + port - } else { - // for index, hostName := range remoteConfig.HostName { - port, _ := cfg.Get(remoteConfig.Host, "Port") - if port == "" { - port = "22" - } - remoteConfig.HostName = remoteConfig.HostName + ":" + port - // remoteConfig.HostName[index] = hostName + ":" + port - } + // TODO: add JumpHost config check - // TODO: Add value/option to config for host key and add bool to check for host key - hostKeyCallback, err := knownhosts.New(khPath) - if err != nil { - log.Fatal().Err(err).Msg("could not create hostkeycallback function") + // if !remoteConfig.UseConfigFiles { + // log.Info().Msg("Not using config files") + // } + if TS(remoteConfig.ConfigFilePath) == "" { + remoteConfig.useDefaultConfig = true + } + + khPath, khPathErr := GetKnownHosts(remoteConfig.KnownHostsFile) + + if khPathErr != nil { + return nil, khPathErr + } + if remoteConfig.ClientConfig == nil { + remoteConfig.ClientConfig = &ssh.ClientConfig{} + } + var sshConfigFile *os.File + var sshConfigFileOpenErr error + if !remoteConfig.useDefaultConfig { + + sshConfigFile, sshConfigFileOpenErr = os.Open(remoteConfig.ConfigFilePath) + if sshConfigFileOpenErr != nil { + return nil, sshConfigFileOpenErr + } + } else { + defaultConfig, _ := resolveDir("~/.ssh/config") + sshConfigFile, sshConfigFileOpenErr = os.Open(defaultConfig) + if sshConfigFileOpenErr != nil { + return nil, sshConfigFileOpenErr + } + } + remoteConfig.SSHConfigFile.DefaultUserSettings = ssh_config.DefaultUserSettings + + cfg, decodeErr := ssh_config.Decode(sshConfigFile) + if decodeErr != nil { + return nil, decodeErr + } + remoteConfig.SSHConfigFile.SshConfigFile = cfg + remoteConfig.GetPrivateKeyFromConfig() + remoteConfig.GetHostNameWithPort() + remoteConfig.GetSshUserFromConfig() + + if remoteConfig.HostName == "" { + return nil, errors.New("No hostname found or specified") + } + err := remoteConfig.GetAuthMethods() + if err != nil { + return nil, err + } + + // TODO: Add value/option to config for host key and add bool to check for host key + hostKeyCallback, err := knownhosts.New(khPath) + if err != nil { + return nil, errors.Wrap(err, "could not create hostkeycallback function") + } + remoteConfig.ClientConfig.HostKeyCallback = hostKeyCallback + log.Info().Str("user", remoteConfig.ClientConfig.User).Send() + + log.Info().Msgf("Connecting to host %s", remoteConfig.HostName) + remoteConfig.ClientConfig.Timeout = time.Second * 30 + sshClient, connectErr = ssh.Dial("tcp", remoteConfig.HostName, remoteConfig.ClientConfig) + if connectErr != nil { + return nil, connectErr + } + return sshClient, nil +} + +func (remoteHost *Host) GetSshUserFromConfig() { + if TS(remoteHost.User) == "" { + remoteHost.User, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "User") + if TS(remoteHost.User) == "" { + remoteHost.User = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "User") + if TS(remoteHost.User) == "" { + currentUser, _ := user.Current() + remoteHost.User = currentUser.Username } - privateKey, err := os.ReadFile(remoteConfig.PrivateKeyPath) + } + } + remoteHost.ClientConfig.User = remoteHost.User +} +func (remoteHost *Host) GetAuthMethods() error { + var signer ssh.Signer + var err error + var privateKey []byte + remoteHost.Password = strings.TrimSpace(remoteHost.Password) + remoteHost.PrivateKeyPassword = strings.TrimSpace(remoteHost.PrivateKeyPassword) + remoteHost.PrivateKeyPath = strings.TrimSpace(remoteHost.PrivateKeyPath) + if remoteHost.PrivateKeyPath != "" { + privateKey, err = os.ReadFile(remoteHost.PrivateKeyPath) + if err != nil { + return err + } + remoteHost.PrivateKeyPassword, err = GetPrivateKeyPassword(remoteHost.PrivateKeyPassword) + if err != nil { + return err + } + if remoteHost.PrivateKeyPassword == "" { + signer, err = ssh.ParsePrivateKey(privateKey) if err != nil { - log.Fatal().Err(err).Msg("read private key error") + return ErrPrivateKeyFileFailedToOpen } - signer, err := ssh.ParsePrivateKey(privateKey) + remoteHost.ClientConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} + } else { + signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(remoteHost.PrivateKeyPassword)) if err != nil { - log.Fatal().Err(err).Msg("parse private key error") + return err } - sshConfig := &ssh.ClientConfig{ - User: remoteConfig.User, - Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, - HostKeyCallback: hostKeyCallback, - // HostKeyAlgorithms: []string{ssh.KeyAlgoECDSA256}, + remoteHost.ClientConfig.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)} + } + } + if remoteHost.Password == "" { + remoteHost.Password, err = GetPassword(remoteHost.Password) + if err != nil { + return err + } + remoteHost.ClientConfig.Auth = append(remoteHost.ClientConfig.Auth, ssh.Password(remoteHost.Password)) + } + return nil +} + +// GetPrivateKeyFromConfig checks to see if the privateKeyPath is empty. +// If not, it keeps the value. +// If empty, the key is looked for in the specified config file. +// If that path is empty, the default config file is searched +// If not found in the default file, the privateKeyPath is set to ~/.ssh/id_rsa +func (remoteHost *Host) GetPrivateKeyFromConfig() { + var identityFile string + if remoteHost.PrivateKeyPath == "" { + identityFile, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "IdentityFile") + if identityFile == "" { + identityFile, _ = remoteHost.SSHConfigFile.DefaultUserSettings.GetStrict(remoteHost.Host, "IdentityFile") + if identityFile == "" { + identityFile = "~/.ssh/id_rsa" } - // for _, host := range remoteConfig.HostName { - log.Info().Msgf("Connecting to host %s", remoteConfig.HostName) + } + } + if identityFile == "" { + identityFile = remoteHost.PrivateKeyPath + } + + remoteHost.PrivateKeyPath, _ = resolveDir(identityFile) +} + +// GetHostNameWithPort checks if the port from the config file is empty +// If it is the port is searched in the SSH config file +func (remoteHost *Host) GetHostNameWithPort() { + var port string - sshClient, connectErr = ssh.Dial("tcp", remoteConfig.HostName, sshConfig) - if connectErr != nil { - log.Fatal().Str("host", remoteConfig.HostName).Err(connectErr).Send() + if remoteHost.Port == 0 { + if remoteHost.HostName == "" { + remoteHost.HostName, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "HostName") + if remoteHost.HostName == "" { + remoteHost.HostName = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "HostName") } - // } - break + port, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "Port") + if port == "" { + port = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "Port") + if port == "" { + port = "22" + } + } + } + remoteHost.HostName = remoteHost.HostName + ":" + port + } else { + if remoteHost.HostName == "" { + remoteHost.HostName, _ = remoteHost.SSHConfigFile.SshConfigFile.Get(remoteHost.Host, "HostName") + if remoteHost.HostName == "" { + remoteHost.HostName = remoteHost.SSHConfigFile.DefaultUserSettings.Get(remoteHost.Host, "HostName") + } + } + remoteHost.HostName = remoteHost.HostName + ":" + fmt.Sprintf("%v", remoteHost.Port) + } +} + +func (remoteHost *Host) ConnectThroughBastion() (*ssh.Client, error) { + // connect to the bastion host + bClient, err := ssh.Dial("tcp", remoteHost.ProxyHost.HostName, remoteHost.ProxyHost.ClientConfig) + if err != nil { + return nil, err + } + + // Dial a connection to the service host, from the bastion + conn, err := bClient.Dial("tcp", remoteHost.HostName) + if err != nil { + return nil, err + } + + ncc, chans, reqs, err := ssh.NewClientConn(conn, remoteHost.HostName, remoteHost.ClientConfig) + if err != nil { + log.Fatal(err) + } + + sClient := ssh.NewClient(ncc, chans, reqs) + // sClient is an ssh client connected to the service host, through the bastion host. + + return sClient, nil +} + +func GetKnownHosts(khPath string) (string, error) { + if TS(khPath) != "" { + return resolveDir(khPath) + } + return resolveDir("~/.ssh/known_hosts") +} + +func GetPrivateKeyPassword(key string) (string, error) { + var prKeyPassword string + if strings.HasPrefix(key, "file:") { + privKeyPassFilePath := strings.TrimPrefix(key, "file:") + privKeyPassFilePath, _ = resolveDir(privKeyPassFilePath) + keyFile, keyFileErr := os.Open(privKeyPassFilePath) + if keyFileErr != nil { + return "", ErrPrivateKeyFileFailedToOpen } + passwordScanner := bufio.NewScanner(keyFile) + for passwordScanner.Scan() { + prKeyPassword = passwordScanner.Text() + } + } else if strings.HasPrefix(key, "env:") { + privKey := strings.TrimPrefix(key, "env:") + privKey = strings.TrimPrefix(privKey, "${") + privKey = strings.TrimSuffix(privKey, "}") + privKey = strings.TrimPrefix(privKey, "$") + prKeyPassword = os.Getenv(privKey) + } else { + prKeyPassword = key + } + return prKeyPassword, nil +} +func GetPassword(key string) (string, error) { + key = strings.TrimSpace(key) + if key == "" { + return "", nil + } + var password string + if strings.HasPrefix(key, "file:") { + passFilePath := strings.TrimPrefix(key, "file:") + passFilePath, _ = resolveDir(passFilePath) + keyFile, keyFileErr := os.Open(passFilePath) + if keyFileErr != nil { + return "", errors.New("Password file failed to open") + } + passwordScanner := bufio.NewScanner(keyFile) + for passwordScanner.Scan() { + password = passwordScanner.Text() + } + } else if strings.HasPrefix(key, "env:") { + passEnv := strings.TrimPrefix(key, "env:") + passEnv = strings.TrimPrefix(passEnv, "${") + passEnv = strings.TrimSuffix(passEnv, "}") + passEnv = strings.TrimPrefix(passEnv, "$") + password = os.Getenv(passEnv) + } else { + password = key } - return sshClient, connectErr + return password, nil } diff --git a/pkg/backy/types.go b/pkg/backy/types.go index 3eba0b7..d206fa2 100644 --- a/pkg/backy/types.go +++ b/pkg/backy/types.go @@ -4,22 +4,59 @@ package backy import ( + "bytes" + + "github.com/kevinburke/ssh_config" + "github.com/nikoksr/notify" "github.com/rs/zerolog" "github.com/spf13/viper" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "golang.org/x/crypto/ssh" ) +type CmdConfigSchema struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + CmdList []string `bson:"command-list,omitempty"` + Name string `bson:"name,omitempty"` +} +type CmdSchema struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Cmd string `bson:"cmd,omitempty"` + Args []string `bson:"args,omitempty"` + Host string `bson:"host,omitempty"` + Dir string `bson:"dir,omitempty"` +} + +type Schemas struct { + CmdConfigSchema + CmdSchema +} + // Host defines a host to which to connect. // If not provided, the values will be looked up in the default ssh config files type Host struct { - ConfigFilePath string `yaml:"config-file-path,omitempty"` - UseConfigFile bool - Empty bool - Host string - HostName string - Port uint16 - PrivateKeyPath string - PrivateKeyPassword string - User string + ConfigFilePath string `yaml:"configfilepath,omitempty"` + Host string `yaml:"host,omitempty"` + HostName string `yaml:"hostname,omitempty"` + KnownHostsFile string `yaml:"knownhostsfile,omitempty"` + ClientConfig *ssh.ClientConfig + SSHConfigFile sshConfigFile + Port uint16 `yaml:"port,omitempty"` + JumpHost string `yaml:"jumphost,omitempty"` + Password string `yaml:"password,omitempty"` + PrivateKeyPath string `yaml:"privatekeypath,omitempty"` + PrivateKeyPassword string `yaml:"privatekeypassword,omitempty"` + UseConfigFiles bool `yaml:"use_config_files,omitempty"` + useDefaultConfig bool + User string `yaml:"user,omitempty"` + // ProxyHost holds the configuration for a JumpHost host + ProxyHost *Host +} + +type sshConfigFile struct { + SshConfigFile *ssh_config.Config + DefaultUserSettings *ssh_config.UserSettings } type Command struct { @@ -39,10 +76,10 @@ type Command struct { */ Shell string `yaml:"shell,omitempty"` - RemoteHost Host `yaml:"-"` + RemoteHost *Host `yaml:"-"` - // cmdArgs is an array that holds the arguments to cmd - CmdArgs []string `yaml:"cmdArgs,omitempty"` + // Args is an array that holds the arguments to cmd + Args []string `yaml:"Args,omitempty"` /* Dir specifies a directory in which to run the command. @@ -59,10 +96,14 @@ type Command struct { type BackyOptionFunc func(*BackyConfigOpts) -type CmdConfig struct { - Order []string `yaml:"order,omitempty"` - Notifications []string `yaml:"notifications,omitempty"` - NotificationsConfig map[string]*NotificationsConfig +type CmdList struct { + Name string `yaml:"name,omitempty"` + Cron string `yaml:"cron,omitempty"` + Order []string `yaml:"order,omitempty"` + Notifications []string `yaml:"notifications,omitempty"` + NotifyConfig *notify.Notify + // NotificationsConfig map[string]*NotificationsConfig + // NotifyConfig map[string]*notify.Notify } type BackyConfigFile struct { @@ -73,11 +114,11 @@ type BackyConfigFile struct { // CmdConfigLists holds the lists of commands to be run in order. // Key is the command list name. - CmdConfigLists map[string]*CmdConfig `yaml:"cmd-configs"` + CmdConfigLists map[string]*CmdList `yaml:"cmd-configs"` // Hosts holds the Host config. // key is the host. - Hosts map[string]Host `yaml:"hosts"` + Hosts map[string]*Host `yaml:"hosts"` // Notifications holds the config for different notifications. Notifications map[string]*NotificationsConfig @@ -86,14 +127,24 @@ type BackyConfigFile struct { } type BackyConfigOpts struct { + // Global log level + BackyLogLvl *string // Holds config file ConfigFile *BackyConfigFile // Holds config file ConfigFilePath string + + Schemas + + DB *mongo.Database + // use command lists using cron + useCron bool // Holds commands to execute for the exec command executeCmds []string - // Global log level - BackyLogLvl *string + // Holds commands to execute for the exec command + executeLists []string + + viper *viper.Viper } type NotificationsConfig struct { @@ -102,11 +153,16 @@ type NotificationsConfig struct { } type CmdOutput struct { - StdErr []byte - StdOut []byte + Err error + Output bytes.Buffer } type BackyCommandOutput interface { Error() error GetOutput() CmdOutput } + +type environmentVars struct { + file string + env []string +} diff --git a/pkg/backy/utils.go b/pkg/backy/utils.go new file mode 100644 index 0000000..c094050 --- /dev/null +++ b/pkg/backy/utils.go @@ -0,0 +1,209 @@ +// utils.go +// Copyright (C) Andrew Woodlee 2023 +// License: Apache-2.0 + +package backy + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "git.andrewnw.xyz/CyberShell/backy/pkg/logging" + "github.com/joho/godotenv" + "github.com/rs/zerolog" + "github.com/spf13/viper" + "golang.org/x/crypto/ssh" +) + +func injectEnvIntoSSH(envVarsToInject environmentVars, process *ssh.Session, log *zerolog.Logger) { + if envVarsToInject.file != "" { + envPath, envPathErr := resolveDir(envVarsToInject.file) + if envPathErr != nil { + log.Fatal().Str("envFile", envPath).Err(envPathErr).Send() + } + file, err := os.Open(envPath) + if err != nil { + log.Fatal().Str("envFile", envPath).Err(err).Send() + } + defer file.Close() + + envMap, err := godotenv.Parse(file) + if err != nil { + log.Error().Str("envFile", envPath).Err(err).Send() + goto errEnvFile + } + for key, val := range envMap { + process.Setenv(key, val) + } + } + +errEnvFile: + if len(envVarsToInject.env) > 0 { + for _, envVal := range envVarsToInject.env { + // don't append env Vars for Backy + if strings.Contains(envVal, "=") && !strings.HasPrefix(envVal, "BACKY_") { + envVarArr := strings.Split(envVal, "=") + process.Setenv(envVarArr[0], envVarArr[1]) + } + } + } +} + +func injectEnvIntoLocalCMD(envVarsToInject environmentVars, process *exec.Cmd, log *zerolog.Logger) { + if envVarsToInject.file != "" { + envPath, _ := resolveDir(envVarsToInject.file) + + file, _ := os.Open(envPath) + // if err != nil { + // log.Fatal().Str("envFile", envPath).Err(err).Send() + // } + defer file.Close() + envMap, err := godotenv.Parse(file) + if err != nil { + log.Error().Str("envFile", envPath).Err(err).Send() + goto errEnvFile + } + for key, val := range envMap { + process.Env = append(process.Env, fmt.Sprintf("%s=%s", key, val)) + } + + } +errEnvFile: + if len(envVarsToInject.env) > 0 { + for _, envVal := range envVarsToInject.env { + if strings.Contains(envVal, "=") { + process.Env = append(process.Env, envVal) + } + } + } + envVarsToInject.env = append(envVarsToInject.env, os.Environ()...) +} + +func (cmd *Command) checkCmdExists() bool { + _, err := exec.LookPath(cmd.Cmd) + return err == nil +} + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +func CheckConfigValues(config *viper.Viper) { + + for _, key := range requiredKeys { + isKeySet := config.IsSet(key) + if !isKeySet { + logging.ExitWithMSG(Sprintf("Config key %s is not defined in %s", key, config.ConfigFileUsed()), 1, nil) + } + + } +} + +func testFile(c string) error { + if strings.TrimSpace(c) != "" { + file, fileOpenErr := os.Open(c) + file.Close() + if errors.Is(fileOpenErr, os.ErrNotExist) { + return fileOpenErr + } + + fmt.Printf("%s\t\t%v", c, fileOpenErr) + } + + return nil +} + +func (c *BackyConfigOpts) LogLvl(level string) BackyOptionFunc { + + return func(bco *BackyConfigOpts) { + c.BackyLogLvl = &level + } +} + +// AddCommands adds commands to BackyConfigOpts +func AddCommands(commands []string) BackyOptionFunc { + return func(bco *BackyConfigOpts) { + bco.executeCmds = append(bco.executeCmds, commands...) + } +} + +// AddCommandLists adds lists to BackyConfigOpts +func AddCommandLists(lists []string) BackyOptionFunc { + return func(bco *BackyConfigOpts) { + bco.executeLists = append(bco.executeLists, lists...) + } +} + +// UseCron enables the execution of command lists at specified times +func UseCron() BackyOptionFunc { + return func(bco *BackyConfigOpts) { + bco.useCron = true + } +} + +// UseCron enables the execution of command lists at specified times +func (c *BackyConfigOpts) AddViper(v *viper.Viper) BackyOptionFunc { + return func(bco *BackyConfigOpts) { + c.viper = v + } +} + +func NewOpts(configFilePath string, opts ...BackyOptionFunc) *BackyConfigOpts { + b := &BackyConfigOpts{} + b.ConfigFilePath = configFilePath + for _, opt := range opts { + if opt != nil { + opt(b) + } + } + return b +} + +/* +NewConfig initializes new config that holds information from the config file +*/ +func NewConfig() *BackyConfigFile { + return &BackyConfigFile{ + Cmds: make(map[string]*Command), + CmdConfigLists: make(map[string]*CmdList), + Hosts: make(map[string]*Host), + Notifications: make(map[string]*NotificationsConfig), + } +} + +func IsTerminalActive() bool { + return os.Getenv("BACKY_TERM") == "enabled" +} + +func IsCmdStdOutEnabled() bool { + return os.Getenv("BACKY_STDOUT") == "enabled" +} + +func resolveDir(path string) (string, error) { + if path == "~" { + homeDir, err := os.UserHomeDir() + if err != nil { + return path, err + } + // In case of "~", which won't be caught by the "else if" + path = homeDir + } else if strings.HasPrefix(path, "~/") { + homeDir, err := os.UserHomeDir() + if err != nil { + return path, err + } + // Use strings.HasPrefix so we don't match paths like + // "/something/~/something/" + path = filepath.Join(homeDir, path[2:]) + } + return path, nil +} diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index f25b119..7e20881 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -3,8 +3,12 @@ package logging import ( "fmt" "os" + "strings" + "time" "github.com/rs/zerolog" + "github.com/spf13/viper" + "gopkg.in/natefinch/lumberjack.v2" ) type Logging struct { @@ -20,3 +24,54 @@ func ExitWithMSG(msg string, code int, log *zerolog.Logger) { fmt.Printf("%s\n", msg) os.Exit(code) } + +func SetLoggingWriters(v *viper.Viper, logFile string) (writers zerolog.LevelWriter) { + + console := zerolog.ConsoleWriter{} + if IsConsoleLoggingEnabled() { + + console = zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC1123} + console.FormatLevel = func(i interface{}) string { + return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) + } + console.FormatMessage = func(i any) string { + if i == nil { + return "" + } + return fmt.Sprintf("MSG: %s", i) + } + console.FormatFieldName = func(i interface{}) string { + return fmt.Sprintf("%s: ", i) + } + console.FormatFieldValue = func(i interface{}) string { + return fmt.Sprintf("%s", i) + // return strings.ToUpper(fmt.Sprintf("%s", i)) + } + } + + fileLogger := &lumberjack.Logger{ + MaxSize: 500, // megabytes + MaxBackups: 3, + MaxAge: 28, //days + Compress: true, // disabled by default + } + if strings.TrimSpace(logFile) != "" { + fileLogger.Filename = logFile + } else { + fileLogger.Filename = "./backy.log" + } + + // UNIX Time is faster and smaller than most timestamps + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + // zerolog.TimeFieldFormat = time.RFC1123 + writers = zerolog.MultiLevelWriter(fileLogger) + + if IsConsoleLoggingEnabled() { + writers = zerolog.MultiLevelWriter(console, fileLogger) + } + return +} + +func IsConsoleLoggingEnabled() bool { + return os.Getenv("BACKY_CONSOLE_LOGGING") == "enabled" +} diff --git a/pkg/notification/notification.go b/pkg/notification/notification.go deleted file mode 100644 index 3a2437c..0000000 --- a/pkg/notification/notification.go +++ /dev/null @@ -1,101 +0,0 @@ -// notification.go -// Copyright (C) Andrew Woodlee 2023 -// License: Apache-2.0 -package notification - -import ( - "fmt" - - "git.andrewnw.xyz/CyberShell/backy/pkg/backy" - "github.com/nikoksr/notify" - "github.com/nikoksr/notify/service/mail" - "github.com/nikoksr/notify/service/matrix" - "maunium.net/go/mautrix/id" -) - -type matrixStruct struct { - homeserver string - roomid id.RoomID - accessToken string - userId id.UserID -} - -type mailConfig struct { - senderaddress string - host string - to []string - username string - password string - port string -} - -var services []notify.Notifier - -func SetupCommandsNotifiers(backyConfig backy.BackyConfigFile, ids ...string) { - -} - -// SetupNotify sets up notify instances for each command list. - -func SetupNotify(backyConfig backy.BackyConfigFile) { - - for _, cmdConfig := range backyConfig.CmdConfigLists { - for notifyID, notifConfig := range cmdConfig.NotificationsConfig { - if cmdConfig.NotificationsConfig[notifyID].Enabled { - config := notifConfig.Config - switch notifConfig.Config.GetString("type") { - case "matrix": - // println(config.GetString("access-token")) - mtrx := matrixStruct{ - userId: id.UserID(config.GetString("user-id")), - roomid: id.RoomID(config.GetString("room-id")), - accessToken: config.GetString("access-token"), - homeserver: config.GetString("homeserver"), - } - mtrxClient, _ := setupMatrix(mtrx) - services = append(services, mtrxClient) - case "mail": - mailCfg := mailConfig{ - senderaddress: config.GetString("senderaddress"), - password: config.GetString("password"), - username: config.GetString("username"), - to: config.GetStringSlice("to"), - host: config.GetString("host"), - port: fmt.Sprint(config.GetUint16("port")), - } - mailClient := setupMail(mailCfg) - services = append(services, mailClient) - } - } - } - } - backyNotify := notify.New() - - backyNotify.UseServices(services...) - - // err := backyNotify.Send( - // context.Background(), - // "Subject/Title", - // "The actual message - Hello, you awesome gophers! :)", - // ) - // if err != nil { - // panic(err) - // } - // logging.ExitWithMSG("This was a test of notifications", 0, nil) -} - -func setupMatrix(config matrixStruct) (*matrix.Matrix, error) { - matrixClient, matrixErr := matrix.New(config.userId, config.roomid, config.homeserver, config.accessToken) - if matrixErr != nil { - panic(matrixErr) - } - return matrixClient, nil - -} - -func setupMail(config mailConfig) *mail.Mail { - mailClient := mail.New(config.senderaddress, config.host+":"+config.port) - mailClient.AuthenticateSMTP("", config.username, config.password, config.host) - mailClient.AddReceivers(config.to...) - return mailClient -}