From e2f4553303422f9d4130e3c7f361c92b22fb0f76 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 17 Jan 2023 00:55:28 -0600 Subject: [PATCH] A runnable command - Added backup sub-command - Added better parsing for config file - Basis for notifications, no running after a command yet - Updated docs and added License --- License | 13 + README | 1 - README.md | 92 ++++++ backup.go => backy.go | 0 cmd/backup.go | 16 +- cmd/root.go | 20 +- examples/backy.yaml | 68 ++++ go.mod | 25 +- go.sum | 40 +++ pkg/backy/backy.go | 417 +++++++++++++++--------- pkg/backy/ssh.go | 45 +-- pkg/backy/types.go | 115 +++++++ pkg/config/backy/config.go | 85 ----- pkg/logging/logging.go | 12 + pkg/{config => }/notifications/email.go | 2 +- pkg/notifications/notification.go | 100 +++++- 16 files changed, 758 insertions(+), 293 deletions(-) create mode 100644 License delete mode 100644 README create mode 100644 README.md rename backup.go => backy.go (100%) create mode 100644 examples/backy.yaml create mode 100644 pkg/backy/types.go delete mode 100644 pkg/config/backy/config.go rename pkg/{config => }/notifications/email.go (51%) diff --git a/License b/License new file mode 100644 index 0000000..d3221b7 --- /dev/null +++ b/License @@ -0,0 +1,13 @@ + Copyright 2023 Andrew Woodlee + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README b/README deleted file mode 100644 index 7cdf470..0000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -# Plan \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bcf5ef --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Backy - an application to manage backups + +This app is in development, and is currently not stable. Expect core functionality to possiblly break. + +To install: `go install git.andrewnw.xyz/CyberShell/backy` + +If you leave the config path blank, the following paths will be searched in order: + - `./backy.yaml` + - `~/.config/backy.yaml` + +Create a file at `~/.config/backy.yaml`: + +```yaml +commands: + stop-docker-container: + cmd: docker + cmdArgs: + - compose + - -f /some/path/to/docker-compose.yaml + - down + # if host is not defined, + host: some-host + env: ~/path/to/env/file + backup-docker-container-script: + cmd: /path/to/script + host: some-host + env: ~/path/to/env/file + shell-cmd: + cmd: rsync + shell: bash + cmdArgs: + - -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: + # all commands have to be defined + order: + - stop-docker-container + - backup-docker-container-script + - shell-cmd + - hostname + notifications: + - matrix + hostname: + order: + - hostname + notifications: + - prod-email + +hosts: + some-host: + config: + usefile: true + user: root + private-key-path: + +logging: + verbose: true + file: /path/to/logs/commands.log + + +notifications: + prod-email: + id: prod-email + type: mail + host: yourhost.tld + port: 587 + senderAddress: email@domain.tld + to: + - admin@domain.tld + username: smtp-username@domain.tld + password: your-password-here + matrix: + id: matrix + type: matrix + homeserver: your-home-server.tld + room-id: room-id + access-token: your-access-token + user-id: your-user-id + +``` + +To run a config: +```backy backup ``` + +Or to use a specific file: +```backy backup -c /path/to/file``` + +Note, let me know if a path lookup fails due to using Go's STDLib `os` \ No newline at end of file diff --git a/backup.go b/backy.go similarity index 100% rename from backup.go rename to backy.go diff --git a/cmd/backup.go b/cmd/backup.go index ee81884..de352d1 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -2,6 +2,7 @@ package cmd import ( "git.andrewnw.xyz/CyberShell/backy/pkg/backy" + "git.andrewnw.xyz/CyberShell/backy/pkg/notifications" "github.com/spf13/cobra" ) @@ -12,17 +13,20 @@ var ( Short: "Runs commands defined in config file.", Long: `Backup executes commands defined in config file, use the -cmds flag to execute the specified commands.`, + Run: Backup, } ) -var CmdList *[]string +var CmdList []string func init() { - cobra.OnInitialize(initConfig) + // cobra.OnInitialize(initConfig) + + backupCmd.Flags().StringSliceVar(&CmdList, "cmds", nil, "Accepts a comma-separated list of command lists to execute.") - backupCmd.Flags().StringSliceVarP(CmdList, "commands", "cmds", nil, "Accepts a comma-separated list of command lists to execute.") } -func backup() { - backyConfig := backy.NewOpts(cfgFile) - backyConfig.GetConfig() +func Backup(cmd *cobra.Command, args []string) { + config := backy.ReadAndParseConfigFile(cfgFile) + notifications.SetupNotify(*config) + config.RunBackyConfig() } diff --git a/cmd/root.go b/cmd/root.go index fec7602..a97c6fb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,9 +1,14 @@ +// root.go +// Copyright (C) Andrew Woodlee 2023 +// License: Apache-2.0 + package cmd import ( "fmt" "os" "path" + "strings" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -17,14 +22,16 @@ var ( rootCmd = &cobra.Command{ Use: "backy", Short: "An easy-to-configure backup tool.", - Long: `Backy is a command-line application useful - for configuring backups, or any commands run in sequence.`, + Long: `Backy is a command-line application useful for configuring backups, or any commands run in sequence.`, } ) // Execute executes the root command. -func Execute() error { - return rootCmd.Execute() +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } } func init() { @@ -33,11 +40,12 @@ func init() { rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file to read from") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Sets verbose level") + rootCmd.AddCommand(backupCmd) } func initConfig() { backyConfig := viper.New() - if cfgFile != "" { + if cfgFile != strings.TrimSpace("") { // Use config file from the flag. backyConfig.SetConfigFile(cfgFile) } else { @@ -55,6 +63,6 @@ func initConfig() { backyConfig.AutomaticEnv() if err := backyConfig.ReadInConfig(); err == nil { - fmt.Println("Using config file:", backyConfig.ConfigFileUsed()) + // fmt.Println("Using config file:", backyConfig.ConfigFileUsed()) } } diff --git a/examples/backy.yaml b/examples/backy.yaml new file mode 100644 index 0000000..bc0629d --- /dev/null +++ b/examples/backy.yaml @@ -0,0 +1,68 @@ +commands: + stop-docker-container: + cmd: docker + cmdArgs: + - compose + - -f /some/path/to/docker-compose.yaml + - down + # if host is not defined, + host: some-host + env: ~/path/to/env/file + backup-docker-container-script: + cmd: /path/to/script + host: some-host + env: ~/path/to/env/file + shell-cmd: + cmd: rsync + shell: bash + cmdArgs: + - -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: + # all commands have to be defined + order: + - stop-docker-container + - backup-docker-container-script + - shell-cmd + - hostname + notifications: + - matrix + hostname: + order: + - hostname + notifications: + - prod-email + +hosts: + some-host: + config: + usefile: true + user: root + private-key-path: + +logging: + verbose: true + file: /path/to/logs/commands.log + + +notifications: + prod-email: + id: prod-email + type: mail + host: yourhost.tld:port + senderAddress: email@domain.tld + to: + - admin@domain.tld + username: smtp-username@domain.tld + password: your-password-here + matrix: + id: matrix + type: matrix + home-server: your-home-server.tld + room-id: room-id + access-token: your-access-token + user-id: your-user-id \ No newline at end of file diff --git a/go.mod b/go.mod index 11a5139..9dd564c 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,5 @@ module git.andrewnw.xyz/CyberShell/backy -// module git.andrewnw.xyz/CyberShell/command - go 1.19 require ( @@ -9,28 +7,45 @@ require ( github.com/rs/zerolog v1.28.0 github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.14.0 - golang.org/x/crypto v0.4.0 + golang.org/x/crypto v0.5.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // 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/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/nikoksr/notify v0.36.0 // 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/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/testify v1.8.1 // indirect github.com/subosito/gotenv v1.4.1 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/text v0.5.0 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + go.uber.org/zap v1.24.0 // 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 + golang.org/x/text v0.6.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + maunium.net/go/mautrix v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index af1bb95..6f28f58 100644 --- a/go.sum +++ b/go.sum @@ -129,6 +129,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/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= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -151,10 +153,13 @@ 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/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= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -180,6 +185,7 @@ github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -191,6 +197,17 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= +github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +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.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/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= @@ -201,6 +218,12 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -210,6 +233,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +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= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -274,6 +299,10 @@ 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.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +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= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -293,6 +322,8 @@ 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.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= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -333,8 +364,11 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -343,6 +377,8 @@ 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.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +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= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -506,6 +542,10 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +maunium.net/go/mautrix v0.12.3 h1:pUeO1ThhtZxE6XibGCzDhRuxwDIFNugsreVr1yYq96k= +maunium.net/go/mautrix v0.12.3/go.mod h1:uOUjkOjm2C+nQS3mr9B5ATjqemZfnPHvjdd1kZezAwg= +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= 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 533a72e..b130c2c 100644 --- a/pkg/backy/backy.go +++ b/pkg/backy/backy.go @@ -1,136 +1,121 @@ +// backy.go +// Copyright (C) Andrew Woodlee 2023 +// License: Apache-2.0 package backy import ( + "bufio" "bytes" "errors" "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" ) -// 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 - UseConfigFile bool - Empty bool - Host string - HostName string - Port uint16 - PrivateKeyPath string - PrivateKeyPassword string - User string -} - -type Command struct { - Remote bool `yaml:"remote,omitempty"` - - // command to run - Cmd string `yaml:"cmd"` - - // host on which to run cmd - Host *string `yaml:"host,omitempty"` +var requiredKeys = []string{"commands", "cmd-configs"} - /* - Shell specifies which shell to run the command in, if any. - Not applicable when host is defined. - */ - Shell string `yaml:"shell,omitempty"` +var Sprintf = fmt.Sprintf - RemoteHost Host `yaml:"-"` - - // cmdArgs is an array that holds the arguments to cmd - CmdArgs []string `yaml:"cmdArgs,omitempty"` +type BackyOptionFunc func(*BackyConfigOpts) - /* - Dir specifies a directory in which to run the command. - Ignored if Host is set. - */ - Dir *string `yaml:"dir,omitempty"` +func (c *BackyConfigOpts) LogLvl(level string) BackyOptionFunc { + return func(bco *BackyConfigOpts) { + c.BackyLogLvl = &level + } } -type BackyGlobalOpts struct { +func (c *BackyConfigOpts) GetConfig() { + c.ConfigFile = ReadAndParseConfigFile(c.ConfigFilePath) } -type BackyConfigFile struct { - /* - Cmds holds the commands for a list. - Key is the name of the command, - */ - Cmds map[string]Command `yaml:"commands"` - - /* - CmdLists holds the lists of commands to be run in order. - Key is the command list name. - */ - CmdLists map[string][]string `yaml:"cmd-lists"` - - /* - Hosts holds the Host config. - key is the host. - */ - Hosts map[string]Host `yaml:"hosts"` - - Logger zerolog.Logger +func NewOpts(configFilePath string, opts ...BackyOptionFunc) *BackyConfigOpts { + b := &BackyConfigOpts{} + b.ConfigFilePath = configFilePath + for _, opt := range opts { + opt(b) + } + return b } -// BackupConfig is a configuration struct that is used to define backups -type BackupConfig struct { - Name string - BackupType string - ConfigPath string +/* +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), + } +} - Cmd Command +type environmentVars struct { + file string + env []string } /* * Runs a backup configuration */ -func (command Command) RunCmd(log *zerolog.Logger) { +func (command *Command) RunCmd(log *zerolog.Logger) { + + var 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) } - - fmt.Printf("\n\nRunning command: " + command.Cmd + " " + cmdArgsStr + " on host " + *command.Host + "...\n\n") + var hostStr string if command.Host != nil { + hostStr = *command.Host + } + 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 sshc, err := command.RemoteHost.ConnectToSSHHost(log) if err != nil { - panic(fmt.Errorf("ssh dial: %w", err)) + log.Err(fmt.Errorf("ssh dial: %w", err)).Send() } defer sshc.Close() - s, err := sshc.NewSession() + commandSession, err := sshc.NewSession() if err != nil { - panic(fmt.Errorf("new ssh session: %w", err)) + log.Err(fmt.Errorf("new ssh session: %w", err)).Send() } - defer s.Close() + defer commandSession.Close() + injectEnvIntoSSH(envVars, commandSession, log) cmd := command.Cmd for _, a := range command.CmdArgs { cmd += " " + a } var stdoutBuf, stderrBuf bytes.Buffer - s.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) - s.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) - err = s.Run(cmd) + commandSession.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) + commandSession.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) + 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() if err != nil { - panic(fmt.Errorf("error when running cmd " + cmd + "\n Error: " + err.Error())) + log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() } } else { // shell := "/bin/bash" @@ -145,12 +130,13 @@ func (command Command) RunCmd(log *zerolog.Logger) { var stdoutBuf, stderrBuf bytes.Buffer localCMD.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) localCMD.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) + injectEnvIntoLocalCMD(envVars, localCMD, log) 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() if err != nil { - panic(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)) + log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() } return } @@ -161,70 +147,58 @@ func (command Command) RunCmd(log *zerolog.Logger) { var stdoutBuf, stderrBuf bytes.Buffer localCMD.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) localCMD.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) + injectEnvIntoLocalCMD(envVars, localCMD, log) 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() if err != nil { - panic(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)) + log.Error().Err(fmt.Errorf("error when running cmd: %s: %w", command.Cmd, err)).Send() } } } -func (config *BackyConfigFile) RunBackyConfig() { - for _, list := range config.CmdLists { - for _, cmd := range list { +func cmdListWorker(id int, jobs <-chan *CmdConfig, config *BackyConfigFile, results chan<- string) { + for j := range jobs { + // fmt.Println("worker", id, "started job", j) + for _, cmd := range j.Order { cmdToRun := config.Cmds[cmd] cmdToRun.RunCmd(&config.Logger) } + // fmt.Println("worker", id, "finished job", j) + results <- "done" } } -type BackyConfigOpts struct { - // Holds config file - ConfigFile *BackyConfigFile - // Holds config file - ConfigFilePath string - - // Global log level - BackyLogLvl *string -} +// RunBackyConfig runs a command list from the BackyConfigFile. +func (config *BackyConfigFile) RunBackyConfig() { + configListsLen := len(config.CmdConfigLists) + jobs := make(chan *CmdConfig, configListsLen) + results := make(chan string) + // configChan := make(chan map[string]Command) -type BackyOptionFunc func(*BackyConfigOpts) + // 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) -func (c *BackyConfigOpts) LogLvl(level string) BackyOptionFunc { - return func(bco *BackyConfigOpts) { - c.BackyLogLvl = &level } -} -func (c *BackyConfigOpts) GetConfig() { - c.ConfigFile = ReadAndParseConfigFile(c.ConfigFilePath) -} -func New() BackupConfig { - return BackupConfig{} -} - -func NewOpts(configFilePath string, opts ...BackyOptionFunc) *BackyConfigOpts { - b := &BackyConfigOpts{} - b.ConfigFilePath = configFilePath - for _, opt := range opts { - opt(b) + // Here we send 5 `jobs` and then `close` that + // 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) } - return b -} + close(jobs) -/* -* NewConfig initializes new config that holds information -* from the config file - */ -func NewConfig() *BackyConfigFile { - return &BackyConfigFile{ - Cmds: make(map[string]Command), - CmdLists: make(map[string][]string), - Hosts: make(map[string]Host), + for a := 1; a <= configListsLen; a++ { + <-results } + } +// ReadAndParseConfigFile validates and reads the config file. func ReadAndParseConfigFile(configFile string) *BackyConfigFile { backyConfigFile := NewConfig() @@ -234,22 +208,28 @@ func ReadAndParseConfigFile(configFile string) *BackyConfigFile { if configFile != "" { backyViper.SetConfigFile(configFile) } else { - backyViper.SetConfigName("backy") // name of config file (without extension) + 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 finding config file: %w", err)) + panic(fmt.Errorf("fatal error reading config file %s: %w", backyViper.ConfigFileUsed(), err)) } - backyLoggingOpts := backyViper.Sub("logging") + CheckForConfigValues(backyViper) + + 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.Level.String(zerolog.DebugLevel) + zerolog.SetGlobalLevel(zerolog.ErrorLevel) } output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC1123} output.FormatLevel = func(i interface{}) string { @@ -271,7 +251,7 @@ func ReadAndParseConfigFile(configFile string) *BackyConfigFile { MaxAge: 28, //days Compress: true, // disabled by default } - if strings.Trim(logFile, " ") != "" { + if strings.TrimSpace(logFile) != "" { fileLogger.Filename = logFile } else { fileLogger.Filename = "./backy.log" @@ -290,24 +270,8 @@ func ReadAndParseConfigFile(configFile string) *BackyConfigFile { unmarshalErr := commandsMapViper.Unmarshal(&backyConfigFile.Cmds) if unmarshalErr != nil { panic(fmt.Errorf("error unmarshalling cmds struct: %w", unmarshalErr)) - } else { - for cmdName, cmdConf := range backyConfigFile.Cmds { - fmt.Printf("\nCommand Name: %s\n", cmdName) - fmt.Printf("Shell: %v\n", cmdConf.Shell) - fmt.Printf("Command: %s\n", cmdConf.Cmd) - - if len(cmdConf.CmdArgs) > 0 { - fmt.Println("\nCmd Args:") - for _, args := range cmdConf.CmdArgs { - fmt.Printf("%s\n", args) - } - } - if cmdConf.Host != nil { - fmt.Printf("Host: %s\n", *backyConfigFile.Cmds[cmdName].Host) - } - } - os.Exit(0) } + var cmdNames []string for k := range commandsMap { cmdNames = append(cmdNames, k) @@ -336,24 +300,84 @@ func ReadAndParseConfigFile(configFile string) *BackyConfigFile { } - cmdListCfg := backyViper.GetStringMapStringSlice("cmd-lists") + 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 cmdListCfg { - for _, cmdInList := range cmdList { + for cmdListName, cmdList := range backyConfigFile.CmdConfigLists { + for _, cmdInList := range cmdList.Order { + // log.Info().Msgf("CmdList %s Cmd %s", cmdListName, cmdInList) _, cmdNameFound := backyConfigFile.Cmds[cmdInList] - if !backyViper.IsSet(getNestedConfig("commands", cmdInList)) && !cmdNameFound { - cmdNotFoundStr := fmt.Sprintf("command definition %s is not in config file\n", cmdInList) + if !cmdNameFound { + cmdNotFoundStr := fmt.Sprintf("command %s is not defined in config file", cmdInList) cmdNotFoundErr := errors.New(cmdNotFoundStr) cmdNotFoundSliceErr = append(cmdNotFoundSliceErr, cmdNotFoundErr) } else { - backyConfigFile.CmdLists[cmdListName] = append(backyConfigFile.CmdLists[cmdListName], cmdInList) + log.Info().Str(cmdInList, "found in "+cmdListName).Send() + // backyConfigFile.CmdLists[cmdListName] = append(backyConfigFile.CmdLists[cmdListName], cmdInList) } } } - for _, err := range cmdNotFoundSliceErr { - if err != nil { - fmt.Println(err.Error()) + if len(cmdNotFoundSliceErr) > 0 { + var cmdNotFoundErrorLog = log.Fatal() + for _, err := range cmdNotFoundSliceErr { + if err != nil { + cmdNotFoundErrorLog.Err(err) + } } + cmdNotFoundErrorLog.Send() + } + + // var notificationSlice []string + for name, cmdCfg := range backyConfigFile.CmdConfigLists { + for _, notificationID := range cmdCfg.Notifications { + // if !contains(notificationSlice, notificationID) { + + cmdCfg.NotificationsConfig = make(map[string]*NotificationsConfig) + notifConfig := backyViper.Sub(getNestedConfig("notifications", notificationID)) + config := &NotificationsConfig{ + Config: notifConfig, + Enabled: true, + } + cmdCfg.NotificationsConfig[notificationID] = config + // First we get a "copy" of the entry + if entry, ok := cmdCfg.NotificationsConfig[notificationID]; ok { + + // Then we modify the copy + entry.Config = notifConfig + entry.Enabled = true + + // Then we reassign the copy + cmdCfg.NotificationsConfig[notificationID] = entry + } + backyConfigFile.CmdConfigLists[name].NotificationsConfig[notificationID] = config + } + // } + } + + 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 _, notif := range backyConfigFile.Notifications { + // fmt.Printf("Type: %s\n", notif.Config.GetString("type")) + // notificationID := notif.Config.GetString("id") + // if !contains(notificationSlice, notificationID) { + // config := backyConfigFile.Notifications[notificationID] + // config.Enabled = false + // backyConfigFile.Notifications[notificationID] = config + // } + // } } return backyConfigFile @@ -363,6 +387,101 @@ func getNestedConfig(nestedConfig, key string) string { return fmt.Sprintf("%s.%s", nestedConfig, key) } -func getNestedSSHConfig(key string) string { - return fmt.Sprintf("hosts.%s.config", key) +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.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() + 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) + } + } + } +} + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +func CheckForConfigValues(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/ssh.go b/pkg/backy/ssh.go index c4bad4f..403ed12 100644 --- a/pkg/backy/ssh.go +++ b/pkg/backy/ssh.go @@ -1,7 +1,10 @@ +// ssh.go +// Copyright (C) Andrew Woodlee 2023 +// License: Apache-2.0 + package backy import ( - "errors" "os" "os/user" "path/filepath" @@ -13,43 +16,9 @@ import ( "golang.org/x/crypto/ssh/knownhosts" ) -type SshConfig struct { - // Config file to open - configFile string - - // Private key path - privateKey string - - // Port to connect to - port uint16 - - // host to check - host string - - // host name to connect to - hostName string - - user string -} - -func (config SshConfig) GetSSHConfig() (SshConfig, error) { - hostNames := ssh_config.Get(config.host, "HostName") - if hostNames == "" { - return SshConfig{}, errors.New("hostname not found") - } - config.hostName = hostNames - privKey, err := ssh_config.GetStrict(config.host, "IdentityFile") - if err != nil { - return SshConfig{}, err - } - config.privateKey = privKey - User := ssh_config.Get(config.host, "User") - if User == "" { - return SshConfig{}, errors.New("user not found") - } - return config, nil -} - +// 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 returns an ssh.Client used to run commands against. func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger) (*ssh.Client, error) { var sshClient *ssh.Client diff --git a/pkg/backy/types.go b/pkg/backy/types.go new file mode 100644 index 0000000..b4b1451 --- /dev/null +++ b/pkg/backy/types.go @@ -0,0 +1,115 @@ +// types.go +// Copyright (C) Andrew Woodlee 2023 +package backy + +import ( + "github.com/rs/zerolog" + "github.com/spf13/viper" +) + +// 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 +} + +type Command struct { + // Remote bool `yaml:"remote,omitempty"` + + Output BackyCommandOutput `yaml:"-"` + + // command to run + Cmd string `yaml:"cmd"` + + // host on which to run cmd + Host *string `yaml:"host,omitempty"` + + /* + Shell specifies which shell to run the command in, if any. + Not applicable when host is defined. + */ + Shell string `yaml:"shell,omitempty"` + + RemoteHost Host `yaml:"-"` + + // cmdArgs is an array that holds the arguments to cmd + CmdArgs []string `yaml:"cmdArgs,omitempty"` + + /* + Dir specifies a directory in which to run the command. + Ignored if Host is set. + */ + Dir *string `yaml:"dir,omitempty"` + + // Env points to a file containing env variables to be used with the command + Env string `yaml:"env,omitempty"` + + // Environment holds env variables to be used with the command + Environment []string `yaml:"environment,omitempty"` +} + +type CmdConfig struct { + Order []string `yaml:"order,omitempty"` + Notifications []string `yaml:"notifications,omitempty"` + NotificationsConfig map[string]*NotificationsConfig +} + +type BackyConfigFile struct { + /* + Cmds holds the commands for a list. + Key is the name of the command, + */ + Cmds map[string]Command `yaml:"commands"` + + /* + CmdLConfigists holds the lists of commands to be run in order. + Key is the command list name. + */ + CmdConfigLists map[string]*CmdConfig `yaml:"cmd-configs"` + + /* + Hosts holds the Host config. + key is the host. + */ + Hosts map[string]Host `yaml:"hosts"` + + /* + Notifications holds the config for different notifications. + */ + Notifications map[string]*NotificationsConfig + + Logger zerolog.Logger +} + +type BackyConfigOpts struct { + // Holds config file + ConfigFile *BackyConfigFile + // Holds config file + ConfigFilePath string + + // Global log level + BackyLogLvl *string +} + +type NotificationsConfig struct { + Config *viper.Viper + Enabled bool +} + +type CmdOutput struct { + StdErr []byte + StdOut []byte +} + +type BackyCommandOutput interface { + Error() error + GetOutput() CmdOutput +} diff --git a/pkg/config/backy/config.go b/pkg/config/backy/config.go deleted file mode 100644 index eae9252..0000000 --- a/pkg/config/backy/config.go +++ /dev/null @@ -1,85 +0,0 @@ -package config - -import ( - "git.andrewnw.xyz/CyberShell/backy/pkg/backy" - "github.com/spf13/viper" -) - -func ReadConfig(Config *viper.Viper) (*viper.Viper, error) { - - backyViper := viper.New() - - // Check for existing config, if none exists, return new one - if Config == nil { - - backyViper.AddConfigPath(".") - // name of config file (without extension) - backyViper.SetConfigName("backy") - // REQUIRED if the config file does not have the extension in the name - backyViper.SetConfigType("yaml") - - if err := backyViper.ReadInConfig(); err != nil { - if configFileNotFound, ok := err.(viper.ConfigFileNotFoundError); ok { - return nil, configFileNotFound - // Config file not found; ignore error if desired - } else { - // Config file was found but another error was produced - } - } - } else { - // Config exists, try to read config file - if err := Config.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - - backyViper.AddConfigPath(".") - // name of config file (without extension) - backyViper.SetConfigName("backy") - // REQUIRED if the config file does not have the extension in the name - backyViper.SetConfigType("yaml") - - if err := backyViper.ReadInConfig(); err != nil { - if configFileNotFound, ok := err.(viper.ConfigFileNotFoundError); ok { - return nil, configFileNotFound - } else { - // Config file was found but another error was produced - } - } - - } else { - // Config file was found but another error was produced - } - } - } - - return backyViper, nil -} - -func unmarshallConfig(backup string, config *viper.Viper) (*viper.Viper, error) { - err := viper.ReadInConfig() - if err != nil { - return nil, err - } - - yamlConfigPath := "backup." + backup - conf := config.Sub(yamlConfigPath) - - backupConfig := config.Unmarshal(&conf) - if backupConfig == nil { - return nil, backupConfig - } - - return conf, nil -} - -// CreateConfig creates a configuration -// pass Name-level (i.e. "backups."+configName) to function -func CreateConfig(backup backy.BackupConfig) backy.BackupConfig { - newBackupConfig := backy.BackupConfig{ - Name: backup.Name, - BackupType: backup.BackupType, - - ConfigPath: backup.ConfigPath, - } - - return newBackupConfig -} diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index fa66721..f25b119 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -1,5 +1,12 @@ package logging +import ( + "fmt" + "os" + + "github.com/rs/zerolog" +) + type Logging struct { Err error Output string @@ -8,3 +15,8 @@ type Logging struct { type Logfile struct { LogfilePath string } + +func ExitWithMSG(msg string, code int, log *zerolog.Logger) { + fmt.Printf("%s\n", msg) + os.Exit(code) +} diff --git a/pkg/config/notifications/email.go b/pkg/notifications/email.go similarity index 51% rename from pkg/config/notifications/email.go rename to pkg/notifications/email.go index 2d1fbb3..aa6a308 100644 --- a/pkg/config/notifications/email.go +++ b/pkg/notifications/email.go @@ -1,4 +1,4 @@ -package notification +package notifications func GetConfig() { diff --git a/pkg/notifications/notification.go b/pkg/notifications/notification.go index 990c6aa..816f1e4 100644 --- a/pkg/notifications/notification.go +++ b/pkg/notifications/notification.go @@ -1,5 +1,101 @@ +// notification.go +// Copyright (C) Andrew Woodlee 2023 +// License: Apache-2.0 package notifications -func SetupNotify() error { - return nil +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 }