diff --git a/app/vmbackup/main.go b/app/vmbackup/main.go index eb5f08297..ed24bc38c 100644 --- a/app/vmbackup/main.go +++ b/app/vmbackup/main.go @@ -4,7 +4,9 @@ import ( "flag" "fmt" "os" + "strings" + "github.com/VictoriaMetrics/VictoriaMetrics/app/vmbackup/snapshot" "github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/actions" "github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/common" "github.com/VictoriaMetrics/VictoriaMetrics/lib/backup/fslocal" @@ -14,9 +16,13 @@ import ( ) var ( - storageDataPath = flag.String("storageDataPath", "victoria-metrics-data", "Path to VictoriaMetrics data. Must match -storageDataPath from VictoriaMetrics or vmstorage") - snapshotName = flag.String("snapshotName", "", "Name for the snapshot to backup. See https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/README.md#how-to-work-with-snapshots") - dst = flag.String("dst", "", "Where to put the backup on the remote storage. "+ + storageDataPath = flag.String("storageDataPath", "victoria-metrics-data", "Path to VictoriaMetrics data. Must match -storageDataPath from VictoriaMetrics or vmstorage") + snapshotName = flag.String("snapshotName", "", "Name for the snapshot to backup. See https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/README.md#how-to-work-with-snapshots") + snapshotCreateURL = flag.String("snapshot.createURL", "", "VictoriaMetrics create snapshot url. When this is given a snapshot will automatically be created during backup."+ + "Example: http://victoriametrics:8428/snaphsot/create") + snapshotDeleteURL = flag.String("snapshot.deleteURL", "", "VictoriaMetrics delete snapshot url. Optional. Will be generated from snapshotCreateURL if not provided. All created snaphosts will be automatically deleted."+ + "Example: http://victoriametrics:8428/snaphsot/delete") + dst = flag.String("dst", "", "Where to put the backup on the remote storage. "+ "Example: gcs://bucket/path/to/backup/dir, s3://bucket/path/to/backup/dir or fs:///path/to/local/backup/dir\n"+ "-dst can point to the previous backup. In this case incremental backup is performed, i.e. only changed data is uploaded") origin = flag.String("origin", "", "Optional origin directory on the remote storage with old backup for server-side copying when performing full backup. This speeds up full backups") @@ -29,6 +35,34 @@ func main() { envflag.Parse() buildinfo.Init() + if len(*snapshotCreateURL) > 0 { + logger.Infof("%s", "Snapshots enabled") + logger.Infof("Snapshot create url %s", *snapshotCreateURL) + if len(*snapshotDeleteURL) <= 0 { + err := flag.Set("snapshot.deleteURL", strings.Replace(*snapshotCreateURL, "/create", "/delete", 1)) + if err != nil { + logger.Fatalf("Failed to set snapshot.deleteURL flag: %v", err) + } + } + logger.Infof("Snapshot delete url %s", *snapshotDeleteURL) + + name, err := snapshot.Create(*snapshotCreateURL) + if err != nil { + logger.Fatalf("%s", err) + } + err = flag.Set("snapshotName", name) + if err != nil { + logger.Fatalf("Failed to set snapshotName flag: %v", err) + } + + defer func() { + err := snapshot.Delete(*snapshotDeleteURL, name) + if err != nil { + logger.Fatalf("%s", err) + } + }() + } + srcFS, err := newSrcFS() if err != nil { logger.Fatalf("%s", err) @@ -67,7 +101,7 @@ See the docs at https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/a func newSrcFS() (*fslocal.FS, error) { if len(*snapshotName) == 0 { - return nil, fmt.Errorf("`-snapshotName` cannot be empty") + return nil, fmt.Errorf("`-snapshotName` or `-snapshot.createURL` must be provided") } snapshotPath := *storageDataPath + "/snapshots/" + *snapshotName diff --git a/app/vmbackup/snapshot/snapshot.go b/app/vmbackup/snapshot/snapshot.go new file mode 100644 index 000000000..852c0b3b4 --- /dev/null +++ b/app/vmbackup/snapshot/snapshot.go @@ -0,0 +1,91 @@ +package snapshot + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "github.com/VictoriaMetrics/VictoriaMetrics/lib/logger" +) + +type snapshot struct { + Status string `json:"status"` + Snapshot string `json:"snapshot"` + Msg string `json:"msg"` +} + +// Create creates a snapshot and the provided api endpoint and returns +// the snapshot name +func Create(createSnapshotURL string) (string, error) { + logger.Infof("%s", "Creating snapshot") + u, err := url.Parse(createSnapshotURL) + if err != nil { + return "", err + } + + resp, err := http.Get(u.String()) + if err != nil { + return "", err + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + snap := snapshot{} + err = json.Unmarshal(body, &snap) + if err != nil { + return "", err + } + + if snap.Status == "ok" { + logger.Infof("Snapshot %s created", snap.Snapshot) + return snap.Snapshot, nil + } else if snap.Status == "error" { + return "", errors.New(snap.Msg) + } else { + return "", fmt.Errorf("Unkown status: %v", snap.Status) + } +} + +// Delete deletes a snapshot and the provided api endpoint returns any failure +func Delete(deleteSnapshotURL string, snapshotName string) error { + logger.Infof("Deleting snapshot %s", snapshotName) + formData := url.Values{ + "snapshot": {snapshotName}, + } + + u, err := url.Parse(deleteSnapshotURL) + if err != nil { + return err + } + + resp, err := http.PostForm(u.String(), formData) + if err != nil { + return err + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + snap := snapshot{} + err = json.Unmarshal(body, &snap) + if err != nil { + return err + } + + if snap.Status == "ok" { + logger.Infof("Snapshot %s deleted", snapshotName) + return nil + } else if snap.Status == "error" { + return errors.New(snap.Msg) + } else { + return fmt.Errorf("Unkown status: %v", snap.Status) + } +} diff --git a/app/vmbackup/snapshot/snapshot_test.go b/app/vmbackup/snapshot/snapshot_test.go new file mode 100644 index 000000000..6d4266de6 --- /dev/null +++ b/app/vmbackup/snapshot/snapshot_test.go @@ -0,0 +1,106 @@ +package snapshot + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" +) + +func TestCreateSnapshot(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/snapshot/create" { + _, err := io.WriteString(w, `{"status":"ok","snapshot":"mysnapshot"}`) + if err != nil { + t.Fatalf("Failed to write response output: %v", err) + } + } else { + t.Fatalf("Invalid path, got %v", r.URL.Path) + } + }) + + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + + snapshotName, err := Create(server.URL + "/snapshot/create") + if err != nil { + t.Fatalf("Failed taking snapshot: %v", err) + } + + if snapshotName != "mysnapshot" { + t.Fatalf("Snapshot name is not correct, got %v", snapshotName) + } +} + +func TestCreateSnapshotFailed(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/snapshot/create" { + _, err := io.WriteString(w, `{"status":"error","msg":"I am unwell"}`) + if err != nil { + t.Fatalf("Failed to write response output: %v", err) + } + } else { + t.Fatalf("Invalid path, got %v", r.URL.Path) + } + }) + + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + + snapshotName, err := Create(server.URL + "/snapshot/create") + if err == nil { + t.Fatalf("Snapshot did not fail, got snapshot: %v", snapshotName) + } +} + +func TestDeleteSnapshot(t *testing.T) { + snapshotName := "mysnapshot" + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/snapshot/delete" { + _, err := io.WriteString(w, `{"status":"ok"}`) + if err != nil { + t.Fatalf("Failed to write response output: %v", err) + } + } else { + t.Fatalf("Invalid path, got %v", r.URL.Path) + } + if r.FormValue("snapshot") != snapshotName { + t.Fatalf("Invalid snapshot name, got %v", snapshotName) + } + }) + + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + + err := Delete(server.URL+"/snapshot/delete", snapshotName) + if err != nil { + t.Fatalf("Failed to delete snapshot: %v", err) + } +} + +func TestDeleteSnapshotFailed(t *testing.T) { + snapshotName := "mysnapshot" + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/snapshot/delete" { + _, err := io.WriteString(w, `{"status":"error", "msg":"failed to delete"}`) + if err != nil { + t.Fatalf("Failed to write response output: %v", err) + } + } else { + t.Fatalf("Invalid path, got %v", r.URL.Path) + } + if r.FormValue("snapshot") != snapshotName { + t.Fatalf("Invalid snapshot name, got %v", snapshotName) + } + }) + + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + + err := Delete(server.URL+"/snapshot/delete", snapshotName) + if err == nil { + t.Fatalf("Snapshot should have failed, got: %v", err) + } +}