├── .gitignore ├── config.cnf ├── logovno.jpg ├── test.go ├── Gopkg.toml ├── govno.toml ├── mysqldump.go ├── LICENSE.md ├── main.go ├── config_test.go ├── config.go ├── backup.go ├── backup_test.go ├── awsstorage.go ├── dumpname_test.go ├── README.md ├── dumpname.go └── Gopkg.lock /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .idea/ 3 | .DS_Store 4 | govno -------------------------------------------------------------------------------- /config.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | user=root 3 | password=secret 4 | host=localhost -------------------------------------------------------------------------------- /logovno.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicklasos/govno/HEAD/logovno.jpg -------------------------------------------------------------------------------- /test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main_() { 4 | ReadConfig("govno.toml") 5 | } 6 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | [prune] 2 | go-tests = true 3 | unused-packages = true 4 | 5 | [[constraint]] 6 | name = "github.com/BurntSushi/toml" 7 | version = "0.3.0" 8 | 9 | [[constraint]] 10 | name = "github.com/codeskyblue/go-sh" 11 | version = "0.2.0" 12 | 13 | [[constraint]] 14 | name = "github.com/aws/aws-sdk-go" 15 | version = "1.15.77" 16 | -------------------------------------------------------------------------------- /govno.toml: -------------------------------------------------------------------------------- 1 | [[database]] 2 | name = "database_name" 3 | host = "127.0.0.1" 4 | cnf = "config.cnf" 5 | aws_bucket = "backup-site-test" 6 | aws_id = "" 7 | aws_key = "" 8 | aws_region = "us-west-2" 9 | 10 | [[database.vno]] 11 | name = "daily" 12 | path = "{month}/{day}/dump.1.sql.gz" 13 | 14 | [[database.vno]] 15 | name = "monthly" 16 | path = "{year}/{month}/{day}/dump.sql.gz" 17 | -------------------------------------------------------------------------------- /mysqldump.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/codeskyblue/go-sh" 7 | ) 8 | 9 | type MySQLDump struct{} 10 | 11 | func (d *MySQLDump) Create(db, file string) error { 12 | err := sh.Command( 13 | "mysqldump", 14 | "--defaults-file=config.cnf", 15 | "--single-transaction", 16 | db, 17 | ).Command("gzip", "-9").WriteStdout(file) 18 | 19 | if err != nil { 20 | return err 21 | } 22 | 23 | return nil 24 | } 25 | 26 | func (d *MySQLDump) Clear(file string) error { 27 | return os.Remove(file) 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | WTFPL 2 | ===== 3 | 4 | ``` 5 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 6 | Version 2, December 2004 7 | 8 | Copyright (C) 2004 Sam Hocevar 9 | 10 | Everyone is permitted to copy and distribute verbatim or modified 11 | copies of this license document, and changing it is allowed as long 12 | as the name is changed. 13 | 14 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 15 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 16 | 17 | 0. You just DO WHAT THE FUCK YOU WANT TO. 18 | ``` 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | if len(os.Args) < 2 { 11 | log.Fatal("You should provide vno name as first parameter") 12 | } 13 | 14 | vno := os.Args[1] 15 | file := os.Getenv("HOME") + "/.govno" 16 | 17 | if len(os.Args) > 2 { 18 | file = os.Args[2] 19 | } 20 | 21 | config, err := ReadConfig(file) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | 26 | fmt.Println(config) 27 | 28 | err = BackupDatabase( 29 | config.Database, 30 | vno, 31 | &AwsStorage{}, 32 | &MySQLDump{}, 33 | ) 34 | 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestReadConfigError(t *testing.T) { 8 | _, err := ReadConfig(".not-existed-config") 9 | if err == nil { 10 | t.Error("It should be an error on read non existed file") 11 | } 12 | } 13 | 14 | func TestReadConfig(t *testing.T) { 15 | config, err := ReadConfig("govno.toml") 16 | 17 | // fmt.Printf("%v+\n", config.Database) 18 | 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | 23 | if len(config.Database) == 0 { 24 | t.Error("Config length is zero") 25 | } 26 | 27 | if config.Database[0].Name != "database_name" { 28 | t.Error("Parse database name is not working") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/BurntSushi/toml" 4 | 5 | type Vno struct { 6 | Name string 7 | Path string 8 | } 9 | 10 | type Database struct { 11 | Name string 12 | Host string 13 | Cnf string 14 | AwsBucket string `toml:"aws_bucket"` 15 | AwsId string `toml:"aws_id"` 16 | AwsKey string `toml:"aws_key"` 17 | AwsRegion string `toml:"aws_region"` 18 | Vno []Vno 19 | } 20 | 21 | type Config struct { 22 | Database []Database 23 | } 24 | 25 | func ReadConfig(configFile string) (*Config, error) { 26 | var config Config 27 | if _, err := toml.DecodeFile(configFile, &config); err != nil { 28 | return nil, err 29 | } 30 | 31 | return &config, nil 32 | } 33 | -------------------------------------------------------------------------------- /backup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type Storage interface { 9 | Upload(db Database, filename, dest string) error 10 | } 11 | 12 | type Dump interface { 13 | Create(db, file string) error 14 | Clear(file string) error 15 | } 16 | 17 | func BackupDatabase(database []Database, vnoName string, storage Storage, dump Dump) error { 18 | for _, db := range database { 19 | for _, vno := range db.Vno { 20 | if vno.Name == vnoName { 21 | path, err := Convert(vno.Path) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | tmpBackupFile := fmt.Sprintf("%d.%s.%s.dump.sql.gz", time.Now().UnixNano(), vnoName, db.Name) 27 | 28 | err = dump.Create(db.Name, tmpBackupFile) 29 | if err != nil { 30 | return err 31 | } 32 | defer dump.Clear(tmpBackupFile) 33 | 34 | err = storage.Upload(db, tmpBackupFile, path) 35 | if err != nil { 36 | return err 37 | } 38 | } 39 | } 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /backup_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type MockStorage struct { 8 | Uploaded bool 9 | } 10 | 11 | func (s *MockStorage) Upload(bucket, file, dest string) error { 12 | s.Uploaded = true 13 | 14 | return nil 15 | } 16 | 17 | type MockBackup struct { 18 | Created bool 19 | Cleared bool 20 | } 21 | 22 | func (b *MockBackup) Create(db, file string) error { 23 | if db == "database_name" { 24 | b.Created = true 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func (b *MockBackup) Clear(file string) error { 31 | b.Cleared = true 32 | 33 | return nil 34 | } 35 | 36 | func TestBackupDatabase(t *testing.T) { 37 | config, _ := ReadConfig("govno.toml") 38 | 39 | mockStorage := &MockStorage{false} 40 | mockBackup := &MockBackup{false, false} 41 | 42 | BackupDatabase(config.Database, "daily", mockStorage, mockBackup) 43 | 44 | if !mockStorage.Uploaded { 45 | t.Error("Error in Upload") 46 | } 47 | 48 | if !mockBackup.Created { 49 | t.Error("Error in Create") 50 | } 51 | 52 | if !mockBackup.Cleared { 53 | t.Error("Error in Clear") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /awsstorage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/credentials" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/s3/s3manager" 12 | ) 13 | 14 | type AwsStorage struct{} 15 | 16 | func (s *AwsStorage) Upload(db Database, filename, dest string) error { 17 | file, err := os.Open(filename) 18 | if err != nil { 19 | return err 20 | } 21 | defer file.Close() 22 | 23 | conf := aws.Config{ 24 | Region: aws.String(db.AwsRegion), 25 | Credentials: credentials.NewStaticCredentials( 26 | db.AwsId, 27 | db.AwsKey, 28 | "", 29 | ), 30 | } 31 | sess := session.New(&conf) 32 | svc := s3manager.NewUploader(sess) 33 | 34 | result, err := svc.Upload(&s3manager.UploadInput{ 35 | Bucket: aws.String(db.AwsBucket), 36 | Key: aws.String(filepath.Base(filename)), 37 | Body: file, 38 | }) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | fmt.Printf("Successfully uploaded %s to %s\n", filename, result.Location) 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /dumpname_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestConvert(t *testing.T) { 11 | date := time.Now() 12 | 13 | dumpname, _ := Convert("foo/{year}/{month}/{day}/{hours}/{minutes}/{seconds}/{timestamp}") 14 | 15 | if dumpname != "foo/"+fmt.Sprintf("%d/%d/%d/%d/%d/%d/%d", date.Year(), date.Month(), date.Day(), date.Hour(), date.Minute(), date.Second(), date.Unix()) { 16 | t.Errorf("Dumpname is incorrect: %s", dumpname) 17 | } 18 | 19 | weekday, _ := Convert("{weekday}") 20 | _, isoWeekday := date.ISOWeek() 21 | if weekday != fmt.Sprintf("%d", isoWeekday) { 22 | t.Error("Weekday in Convert is incorrect") 23 | } 24 | 25 | week, _ := Convert("{week}") 26 | if week != fmt.Sprintf("%d", numberOfTheWeek(time.Now())) { 27 | t.Errorf("Number of the week is incorrent: %s", week) 28 | } 29 | 30 | host, _ := Convert("{hostname}") 31 | hostname, _ := os.Hostname() 32 | if host != hostname { 33 | t.Errorf("Hostname is incorrect: %s", host) 34 | } 35 | } 36 | 37 | func TestConvertErr(t *testing.T) { 38 | _, err := Convert("{typo}") 39 | if err.Error() != "undefined placeholders" { 40 | t.Error("Detecting typos is not working") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GO VNO 2 | VNO protocol implementation in Go
3 | VNO stands for Very Needed Object

4 | 5 | ![logovno](logovno.jpg) 6 | 7 | 8 | Example of govno.toml ~/.govno 9 | ```toml 10 | [[database]] 11 | name = "database_name" # database name 12 | host = "127.0.0.1" 13 | cnf = "config.cnf" 14 | aws_bucket = "backup-site-test" 15 | aws_id = "" 16 | aws_key = "" 17 | aws_region = "us-west-2" 18 | 19 | [[database.vno]] 20 | name = "daily" 21 | path = "{month}/{day}/file.sql.gz" 22 | 23 | [[database.vno]] 24 | name = "monthly" 25 | path = "{year}/{month}/{day}/file.sql.gz" 26 | 27 | [[database]] 28 | name = "another_database" 29 | host = "8.8.8.8" 30 | ... 31 | 32 | [[database.vno]] 33 | name = "daily" 34 | path = "{host}/mysql/daily/{year}/{month}/{day}.sql.gz" 35 | 36 | ``` 37 | 38 | Example of crontab file
39 | daily - name of vno object 40 | govno.toml - config location 41 | ``` 42 | 0 22 * * * govno daily govno.toml >> /dev/null 2>&1 43 | ``` 44 | 45 | Also you can put your govno.toml to ~/.govno 46 | ``` 47 | 0 22 * * * govno daily >> /dev/null 2>&1 48 | ``` 49 | 50 | config.cnf file 51 | ``` 52 | [client] 53 | user=root 54 | password=secret 55 | host=localhost 56 | ``` 57 | -------------------------------------------------------------------------------- /dumpname.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func Convert(name string) (string, error) { 12 | date := time.Now() 13 | 14 | _, isoWeekday := date.ISOWeek() 15 | weekday := fmt.Sprintf("%d", isoWeekday) 16 | 17 | hostname, err := os.Hostname() 18 | if err != nil { 19 | return "", err 20 | } 21 | 22 | replacer := strings.NewReplacer( 23 | "{year}", fmt.Sprintf("%d", date.Year()), 24 | "{month}", fmt.Sprintf("%d", date.Month()), 25 | "{day}", fmt.Sprintf("%d", date.Day()), 26 | "{hours}", fmt.Sprintf("%d", date.Hour()), 27 | "{minutes}", fmt.Sprintf("%d", date.Minute()), 28 | "{seconds}", fmt.Sprintf("%d", date.Second()), 29 | "{weekday}", weekday, 30 | "{week}", fmt.Sprintf("%d", numberOfTheWeek(date)), 31 | "{timestamp}", fmt.Sprintf("%d", time.Now().Unix()), 32 | "{hostname}", hostname, 33 | ) 34 | 35 | result := replacer.Replace(name) 36 | 37 | if strings.ContainsAny(result, "{}") { 38 | return "", errors.New("undefined placeholders") 39 | } 40 | 41 | return result, nil 42 | } 43 | 44 | func numberOfTheWeek(now time.Time) int { 45 | beginningOfTheMonth := time.Date(now.Year(), now.Month(), 1, 1, 1, 1, 1, time.UTC) 46 | _, thisWeek := now.ISOWeek() 47 | _, beginningWeek := beginningOfTheMonth.ISOWeek() 48 | 49 | return 1 + thisWeek - beginningWeek 50 | } 51 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:b16fbfbcc20645cb419f78325bb2e85ec729b338e996a228124d68931a6f2a37" 6 | name = "github.com/BurntSushi/toml" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "b26d9c308763d68093482582cea63d69be07a0f0" 10 | version = "v0.3.0" 11 | 12 | [[projects]] 13 | digest = "1:9cc80abecc469330afaa9cc72744b258419f5be8adc78b7898ada5d299152048" 14 | name = "github.com/aws/aws-sdk-go" 15 | packages = [ 16 | "aws", 17 | "aws/awserr", 18 | "aws/awsutil", 19 | "aws/client", 20 | "aws/client/metadata", 21 | "aws/corehandlers", 22 | "aws/credentials", 23 | "aws/credentials/ec2rolecreds", 24 | "aws/credentials/endpointcreds", 25 | "aws/credentials/processcreds", 26 | "aws/credentials/stscreds", 27 | "aws/csm", 28 | "aws/defaults", 29 | "aws/ec2metadata", 30 | "aws/endpoints", 31 | "aws/request", 32 | "aws/session", 33 | "aws/signer/v4", 34 | "internal/ini", 35 | "internal/s3err", 36 | "internal/sdkio", 37 | "internal/sdkrand", 38 | "internal/sdkuri", 39 | "internal/shareddefaults", 40 | "private/protocol", 41 | "private/protocol/eventstream", 42 | "private/protocol/eventstream/eventstreamapi", 43 | "private/protocol/json/jsonutil", 44 | "private/protocol/query", 45 | "private/protocol/query/queryutil", 46 | "private/protocol/rest", 47 | "private/protocol/restxml", 48 | "private/protocol/xml/xmlutil", 49 | "service/s3", 50 | "service/s3/s3iface", 51 | "service/s3/s3manager", 52 | "service/sts", 53 | ] 54 | pruneopts = "UT" 55 | revision = "d6c5ccab427af7408a67e0f45c6e6a3d515fdaee" 56 | version = "v1.19.39" 57 | 58 | [[projects]] 59 | digest = "1:63bd62f5b57c55f0e07c9f8a76d841c7fa74905cc3cb6b8a55e923450f4bee81" 60 | name = "github.com/codegangsta/inject" 61 | packages = ["."] 62 | pruneopts = "UT" 63 | revision = "37d7f8432a3e684eef9b2edece76bdfa6ac85b39" 64 | version = "v1.0-rc1" 65 | 66 | [[projects]] 67 | digest = "1:be0609bff973c07b037b52da33861d1ef19a1ac7e0c909cf748f02fee5ff2f07" 68 | name = "github.com/codeskyblue/go-sh" 69 | packages = ["."] 70 | pruneopts = "UT" 71 | revision = "b097669b1569203c3ce05a6b8717d43140fdb3d5" 72 | version = "0.2" 73 | 74 | [[projects]] 75 | digest = "1:bb81097a5b62634f3e9fec1014657855610c82d19b9a40c17612e32651e35dca" 76 | name = "github.com/jmespath/go-jmespath" 77 | packages = ["."] 78 | pruneopts = "UT" 79 | revision = "c2b33e84" 80 | 81 | [solve-meta] 82 | analyzer-name = "dep" 83 | analyzer-version = 1 84 | input-imports = [ 85 | "github.com/BurntSushi/toml", 86 | "github.com/aws/aws-sdk-go/aws", 87 | "github.com/aws/aws-sdk-go/aws/session", 88 | "github.com/aws/aws-sdk-go/service/s3/s3manager", 89 | "github.com/codeskyblue/go-sh", 90 | ] 91 | solver-name = "gps-cdcl" 92 | solver-version = 1 93 | --------------------------------------------------------------------------------