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
v0.1.0
Andrew 1 year ago
parent 15a7ca6c3d
commit e2f4553303

@ -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.

@ -1 +0,0 @@
# Plan

@ -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`

@ -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()
}

@ -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())
}
}

@ -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

@ -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
)

@ -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=

@ -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)
}
}
}

@ -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

@ -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
}

@ -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
}

@ -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)
}

@ -1,4 +1,4 @@
package notification
package notifications
func GetConfig() {

@ -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
}

Loading…
Cancel
Save