initial commit
This commit is contained in:
commit
d30cb6f304
557
backup.go
Normal file
557
backup.go
Normal file
@ -0,0 +1,557 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Zfsnap map[string]string `json:"zfsnap"`
|
||||
Box map[string]BoxConfig `json:"box"`
|
||||
Apps []AppConfig `json:apps`
|
||||
ssh map[string]*SSHConfig
|
||||
}
|
||||
|
||||
type Location string
|
||||
|
||||
type Snapshot string
|
||||
|
||||
type AppConfig struct {
|
||||
Name string `json:"name"`
|
||||
Schedule []string `json:"schedule"`
|
||||
Sources []Location `json:"src"`
|
||||
Destinations []Location `json:"dest"`
|
||||
Before map[string]Location `json:"before"`
|
||||
After map[string]Location `json:"after"`
|
||||
}
|
||||
type SSHConfig struct {
|
||||
signer ssh.Signer
|
||||
config *ssh.ClientConfig
|
||||
client *ssh.Client
|
||||
logged bool
|
||||
name string
|
||||
zfs map[string]string
|
||||
snapshot []Snapshot
|
||||
}
|
||||
|
||||
type BoxConfig struct {
|
||||
Addr string `json:"addr"`
|
||||
User string `json:"user"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
var (
|
||||
cfgFile = flag.String("config", "config.json", "config file")
|
||||
schedFlag = flag.String("schedule", "", "specific schedule")
|
||||
testFlag = flag.Bool("test", true, "test run")
|
||||
debugFlag = flag.Bool("debug", true, "debug")
|
||||
cfg Config
|
||||
)
|
||||
|
||||
func (l Location) Box() string {
|
||||
s := strings.Split(string(l), `:`)
|
||||
return s[0]
|
||||
}
|
||||
|
||||
func (l Location) Path() string {
|
||||
s := strings.Split(string(l), `:`)
|
||||
return s[1]
|
||||
}
|
||||
|
||||
func (l Location) Valid() bool {
|
||||
s := strings.Split(string(l), `:`)
|
||||
return len(s) == 2
|
||||
}
|
||||
|
||||
//Load config from file
|
||||
func (c *Config) Load() error {
|
||||
b, err := ioutil.ReadFile(*cfgFile)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", *cfgFile, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &c)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : json.Unmarshal : %s", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.ssh = make(map[string]*SSHConfig)
|
||||
for k, v := range c.Box {
|
||||
s := &SSHConfig{
|
||||
logged: false,
|
||||
name: k,
|
||||
}
|
||||
cfg.ssh[k] = s
|
||||
keyRaw, err := ioutil.ReadFile(v.Key)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := ssh.ParseRawPrivateKey(keyRaw)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ssh.ParseRawPrivateKey(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
s.signer, err = ssh.NewSignerFromKey(key)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ssh.NewSignerFromKey(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
s.config = &ssh.ClientConfig{
|
||||
User: v.User,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(s.signer),
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
s.client, err = ssh.Dial("tcp", v.Addr, s.config)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ssh.Dial(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := s.client.NewSession()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : client.NewSession(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
session.Stdout = &b
|
||||
err = session.Run("/usr/bin/uname -a")
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : client.NewSession(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : logged into %s : %s", k, b.String())
|
||||
}
|
||||
session.Close()
|
||||
s.logged = true
|
||||
|
||||
}
|
||||
|
||||
for _, app := range c.Apps {
|
||||
for _, src := range app.Sources {
|
||||
if !src.Valid() {
|
||||
return fmt.Errorf("Source not valid : %s", string(src))
|
||||
}
|
||||
if _, ok := cfg.ssh[src.Box()]; !ok {
|
||||
return fmt.Errorf("No box defined for source : %s", string(src))
|
||||
}
|
||||
}
|
||||
for _, dest := range app.Destinations {
|
||||
if !dest.Valid() {
|
||||
return fmt.Errorf("Destination not valid : %s", string(dest))
|
||||
}
|
||||
if _, ok := cfg.ssh[dest.Box()]; !ok {
|
||||
return fmt.Errorf("No box defined for destination : %s", string(dest))
|
||||
}
|
||||
}
|
||||
for val, before := range app.Before {
|
||||
_, err = regexp.Compile(val)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : invalid regex : %s", val)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !before.Valid() {
|
||||
return fmt.Errorf("Before not valid : %s", string(before))
|
||||
}
|
||||
if _, ok := cfg.ssh[before.Box()]; !ok {
|
||||
return fmt.Errorf("No box defined for before : %s", string(before))
|
||||
}
|
||||
}
|
||||
for val, after := range app.After {
|
||||
_, err = regexp.Compile(val)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : invalid regex : %s", val)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !after.Valid() {
|
||||
return fmt.Errorf("After not valid : %s", string(after))
|
||||
}
|
||||
if _, ok := cfg.ssh[after.Box()]; !ok {
|
||||
return fmt.Errorf("No box defined for after : %s", string(after))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Close config
|
||||
func (c *Config) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SSHConfig) getSnapshotList() error {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.getSnapshotList : Start %s", s.name)
|
||||
}
|
||||
if !s.logged {
|
||||
return fmt.Errorf("Client %s not logged in.", s.name)
|
||||
}
|
||||
|
||||
session, err := s.client.NewSession()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.getSnapshotList : client.NewSession(%s) : %s", s.name, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
session.Stdout = &b
|
||||
err = session.Run("/usr/sbin/zfs list -H -t snapshot -o name")
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.getSnapshotList : session.Run(%s) : %s", s.name, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
s.snapshot = make([]Snapshot, 0)
|
||||
|
||||
csvReader := csv.NewReader(&b)
|
||||
csvReader.Comma = '\t'
|
||||
csvReader.FieldsPerRecord = 1
|
||||
|
||||
csvData, err := csvReader.ReadAll()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.getSnapshotList : csvReader.ReadAll(%s) : %s", s.name, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rec := range csvData {
|
||||
s.snapshot = append(s.snapshot, Snapshot(rec[0]))
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.getSnapshotList : %s : read %d zfs snapshots", s.name, len(s.snapshot))
|
||||
}
|
||||
|
||||
session.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SSHConfig) getZFSList() error {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.getZFSList : Start %s", s.name)
|
||||
}
|
||||
if !s.logged {
|
||||
return fmt.Errorf("Client %s not logged in.", s.name)
|
||||
}
|
||||
|
||||
session, err := s.client.NewSession()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.getZFSList : client.NewSession(%s) : %s", s.name, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
session.Stdout = &b
|
||||
err = session.Run("/sbin/zfs list -H -o name,mountpoint")
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.getZFSList : session.Run(%s) : %s", s.name, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
s.zfs = make(map[string]string)
|
||||
|
||||
csvReader := csv.NewReader(&b)
|
||||
csvReader.Comma = '\t'
|
||||
csvReader.FieldsPerRecord = 2
|
||||
|
||||
csvData, err := csvReader.ReadAll()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.getZFSList : csvReader.ReadAll(%s) : %s", s.name, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rec := range csvData {
|
||||
s.zfs[rec[0]] = rec[1]
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.getZFSList : %s : read %d zfs file systems", s.name, len(s.zfs))
|
||||
}
|
||||
|
||||
session.Close()
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *SSHConfig) isZFS(path string) bool {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.isZFS : Start %s:%s", s.name, path)
|
||||
}
|
||||
if len(s.zfs) == 0 {
|
||||
err := s.getZFSList()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.isZFS : s.getZFSList(%s) : %s", s.name, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
_, ok := s.zfs[path]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SSHConfig) exec(cmd string) error {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.exec : Start %s on %s", cmd, s.name)
|
||||
}
|
||||
|
||||
session, err := s.client.NewSession()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.exec : client(%s).NewSession(%s) : %s", s.name, cmd, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err = session.Run(cmd)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.exec : session(%s).Run(%s) : %s", s.name, cmd, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
session.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SSHConfig) createZFS(path string) error {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.createZFS : Start %s:%s", s.name, path)
|
||||
}
|
||||
if len(s.zfs) == 0 {
|
||||
err := s.getZFSList()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.createZFS : s.getZFSList(%s) : %s", s.name, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
p := strings.Split(path, `/`)
|
||||
var base string
|
||||
for _, d := range p {
|
||||
if base == "" {
|
||||
base = d
|
||||
} else {
|
||||
base = base + `/` + d
|
||||
}
|
||||
if _, ok := s.zfs[base]; !ok {
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.createZFS : Creating %s:%s", s.name, base)
|
||||
}
|
||||
|
||||
err := s.exec("/sbin/zfs create -o mountpoint=none " + base)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.createZFS : s.exec(%s) : %s", s.name, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
s.zfs[base] = "none"
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (a AppConfig) RunAppSchedule(schedule string) error {
|
||||
if *debugFlag {
|
||||
log.Printf("RunAppSchedule : Running %s(%s)", a.Name, schedule)
|
||||
}
|
||||
for _, src := range a.Sources {
|
||||
if !cfg.ssh[src.Box()].isZFS(src.Path()) {
|
||||
return fmt.Errorf("No path %s on source", string(src))
|
||||
}
|
||||
for _, dest := range a.Destinations {
|
||||
if !cfg.ssh[dest.Box()].isZFS(dest.Path() + "/" + src.Box() + "/" + src.Path()) {
|
||||
err := cfg.ssh[dest.Box()].createZFS(dest.Path() + "/" + src.Box() + "/" + src.Path())
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("RunAppSchedule : Error creating %s on %s", dest.Path()+"/"+src.Box()+"/"+src.Path(), dest.Box())
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range a.Before {
|
||||
re := regexp.MustCompile(k)
|
||||
if re.MatchString(schedule) {
|
||||
err := cfg.ssh[v.Box()].exec(v.Path())
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("RunAppSchedule : Error executing %s", string(v))
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
var refreshSnapshot map[string]bool
|
||||
refreshSnapshot = make(map[string]bool)
|
||||
for _, v := range a.Sources {
|
||||
if *debugFlag {
|
||||
log.Printf("RunAppSchedule : taking %s snapshot for %s", schedule, v.Path())
|
||||
}
|
||||
err := cfg.ssh[v.Box()].exec("/usr/sbin/zfsnap snapshot -p '" + schedule + "-' -a " + cfg.Zfsnap[schedule] + " " + v.Path())
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("RunAppSchedule : Error executing zfsnap on %s", string(v))
|
||||
}
|
||||
return err
|
||||
}
|
||||
refreshSnapshot[v.Box()] = true
|
||||
}
|
||||
for k, _ := range refreshSnapshot {
|
||||
if *debugFlag {
|
||||
log.Printf("RunAppSchedule : refreshing snapshots for source %s", k)
|
||||
}
|
||||
err := cfg.ssh[k].getSnapshotList()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("RunAppSchedule : Error getting snapshots on %s", k)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, src := range a.Sources {
|
||||
for _, dest := range a.Destinations {
|
||||
if *debugFlag {
|
||||
log.Printf("RunAppSchedule : Sending snapshots from %s to %s", string(src), string(dest))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range a.After {
|
||||
re := regexp.MustCompile(k)
|
||||
if re.MatchString(schedule) {
|
||||
err := cfg.ssh[v.Box()].exec(v.Path())
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("RunAppSchedule : Error executing %s on %s", v.Path(), v.Box())
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
err := cfg.Load()
|
||||
if err != nil {
|
||||
log.Printf("Cannot load config (%s)", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
schedule := *schedFlag
|
||||
if schedule == "" {
|
||||
log.Printf("Main : Finding out schedule.")
|
||||
now := time.Now()
|
||||
if now.Day() == 1 && int(now.Month()) == 1 {
|
||||
schedule = "yearly"
|
||||
} else if now.Day() == 1 {
|
||||
schedule = "monthly"
|
||||
} else if now.Weekday().String() == "Monday" {
|
||||
schedule = "weekly"
|
||||
} else {
|
||||
schedule = "daily"
|
||||
}
|
||||
}
|
||||
|
||||
err = RunSchedule(schedule)
|
||||
if err != nil {
|
||||
log.Printf("Cannot run schedule (%s)", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
//RunSchedule run all backup targets where schedule is registered
|
||||
func RunSchedule(schedule string) error {
|
||||
if *debugFlag {
|
||||
log.Printf("RunSchedule : Start %s", schedule)
|
||||
}
|
||||
if _, ok := cfg.Zfsnap[schedule]; !ok {
|
||||
return fmt.Errorf("No retention defined for %s schedule", schedule)
|
||||
}
|
||||
|
||||
for _, app := range cfg.Apps {
|
||||
for _, schedName := range app.Schedule {
|
||||
if schedName == schedule {
|
||||
err := app.RunAppSchedule(schedule)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("RunSchedule : Error running %s(%s)", app.Name, schedule)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module gogs.siteop.biz/shoopea/backup
|
||||
|
||||
go 1.16
|
||||
|
||||
require golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
9
go.sum
Normal file
9
go.sum
Normal file
@ -0,0 +1,9 @@
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
Loading…
Reference in New Issue
Block a user