From 7a81104082ff5ac29266fee2df77951e96c009d6 Mon Sep 17 00:00:00 2001 From: Philip O'Toole Date: Fri, 29 Nov 2019 12:34:23 -0500 Subject: [PATCH 1/3] Support backups from CLI Port PR436. --- cmd/rqlite/execute.go | 8 +++++++- cmd/rqlite/main.go | 44 ++++++++++++++++++++++++++++++------------- cmd/rqlite/query.go | 8 +++++++- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/cmd/rqlite/execute.go b/cmd/rqlite/execute.go index 07d3a08c..d5870203 100644 --- a/cmd/rqlite/execute.go +++ b/cmd/rqlite/execute.go @@ -32,8 +32,14 @@ func execute(ctx *cli.Context, cmd, line string, timer bool, argv *argT) error { Path: fmt.Sprintf("%sdb/execute", argv.Prefix), RawQuery: queryStr.Encode(), } + + response, err := sendRequest(ctx, "POST", u.String(), line, argv) + if err != nil { + return err + } + ret := &executeResponse{} - if err := sendRequest(ctx, u.String(), line, argv, ret); err != nil { + if err := parseResponse(response, &ret); err != nil { return err } if ret.Error != "" { diff --git a/cmd/rqlite/main.go b/cmd/rqlite/main.go index df546e12..c6f83033 100644 --- a/cmd/rqlite/main.go +++ b/cmd/rqlite/main.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "strings" @@ -34,6 +35,7 @@ const cliHelp = `.help Show this message .expvar Show expvar (Go runtime) information for connected node .tables List names of tables .timer on|off Turn SQL timer on or off +.backup Write database backup to file ` func main() { @@ -78,6 +80,12 @@ func main() { err = status(ctx, cmd, line, argv) case ".EXPVAR": err = expvar(ctx, cmd, line, argv) + case ".BACKUP": + if index == -1 || index == len(line)-1 { + err = fmt.Errorf("Please specify an output file for the backup") + break + } + err = backup(ctx, line[index+1:], argv) case ".HELP": err = help(ctx, cmd, line, argv) case ".QUIT", "QUIT", "EXIT": @@ -127,22 +135,28 @@ func expvar(ctx *cli.Context, cmd, line string, argv *argT) error { return cliJSON(ctx, cmd, line, url, argv) } -func sendRequest(ctx *cli.Context, urlStr string, line string, argv *argT, ret interface{}) error { - data := makeJSONBody(line) +func sendRequest(ctx *cli.Context, method string, urlStr string, line string, argv *argT) (*[]byte, error) { + var requestData io.Reader + if line != "" { + requestData = strings.NewReader(makeJSONBody(line)) + } else { + requestData = nil + } + url := urlStr var rootCAs *x509.CertPool if argv.CACert != "" { pemCerts, err := ioutil.ReadFile(argv.CACert) if err != nil { - return err + return nil, err } rootCAs = x509.NewCertPool() ok := rootCAs.AppendCertsFromPEM(pemCerts) if !ok { - return fmt.Errorf("failed to parse root CA certificate(s)") + return nil, fmt.Errorf("failed to parse root CA certificate(s)") } } @@ -157,48 +171,52 @@ func sendRequest(ctx *cli.Context, urlStr string, line string, argv *argT, ret i nRedirect := 0 for { - req, err := http.NewRequest("POST", url, strings.NewReader(data)) + req, err := http.NewRequest(method, url, requestData) if err != nil { - return err + return nil, err } if argv.Credentials != "" { creds := strings.Split(argv.Credentials, ":") if len(creds) != 2 { - return fmt.Errorf("invalid Basic Auth credentials format") + return nil, fmt.Errorf("invalid Basic Auth credentials format") } req.SetBasicAuth(creds[0], creds[1]) } resp, err := client.Do(req) if err != nil { - return err + return nil, err } defer resp.Body.Close() if resp.StatusCode == http.StatusUnauthorized { - return fmt.Errorf("unauthorized") + return nil, fmt.Errorf("unauthorized") } // Check for redirect. if resp.StatusCode == http.StatusMovedPermanently { nRedirect++ if nRedirect > maxRedirect { - return fmt.Errorf("maximum leader redirect limit exceeded") + return nil, fmt.Errorf("maximum leader redirect limit exceeded") } url = resp.Header["Location"][0] continue } - body, err := ioutil.ReadAll(resp.Body) + response, err := ioutil.ReadAll(resp.Body) if err != nil { - return err + return nil, err } - return json.Unmarshal(body, ret) + return &response, nil } } +func parseResponse(response *[]byte, ret interface{}) error { + return json.Unmarshal(*response, ret) +} + // cliJSON fetches JSON from a URL, and displays it at the CLI. func cliJSON(ctx *cli.Context, cmd, line, url string, argv *argT) error { // Recursive JSON printer. diff --git a/cmd/rqlite/query.go b/cmd/rqlite/query.go index 20dffbe2..f18b1b59 100644 --- a/cmd/rqlite/query.go +++ b/cmd/rqlite/query.go @@ -91,8 +91,14 @@ func query(ctx *cli.Context, cmd, line string, timer bool, argv *argT) error { Path: fmt.Sprintf("%sdb/query", argv.Prefix), RawQuery: queryStr.Encode(), } + + response, err := sendRequest(ctx, "POST", u.String(), line, argv) + if err != nil { + return err + } + ret := &queryResponse{} - if err := sendRequest(ctx, u.String(), line, argv, ret); err != nil { + if err := parseResponse(response, &ret); err != nil { return err } if ret.Error != "" { From ce9736a1b7b41474d190974854f734a9b665f54c Mon Sep 17 00:00:00 2001 From: Philip O'Toole Date: Fri, 29 Nov 2019 12:35:45 -0500 Subject: [PATCH 2/3] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dddd41f6..e1414891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## 4.6.0 (unreleased) +- [PR #585](https://github.com/rqlite/rqlite/pull/585): Add backup command to CLI: Thanks @eariassoto. - [PR #584](https://github.com/rqlite/rqlite/pull/584): Support showing timings in the CLI. Thanks @joaodrp. - [PR #583](https://github.com/rqlite/rqlite/pull/583): Add BasicAuth support to the CLI. Thanks @joaodrp. - [PR #564](https://github.com/rqlite/rqlite/pull/564): rqlite server supports specifying trusted root CA certificate. Thanks @zmedico. From ea8d568406f98afc889bf6ef873bb206971c2490 Mon Sep 17 00:00:00 2001 From: Philip O'Toole Date: Fri, 29 Nov 2019 12:38:44 -0500 Subject: [PATCH 3/3] Commit new file --- cmd/rqlite/backup.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 cmd/rqlite/backup.go diff --git a/cmd/rqlite/backup.go b/cmd/rqlite/backup.go new file mode 100644 index 00000000..ff2b868a --- /dev/null +++ b/cmd/rqlite/backup.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/url" + + "github.com/mkideal/cli" +) + +type backupResponse struct { + BackupFile []byte +} + +func backup(ctx *cli.Context, filename string, argv *argT) error { + queryStr := url.Values{} + u := url.URL{ + Scheme: argv.Protocol, + Host: fmt.Sprintf("%s:%d", argv.Host, argv.Port), + Path: fmt.Sprintf("%sdb/backup", argv.Prefix), + RawQuery: queryStr.Encode(), + } + response, err := sendRequest(ctx, "GET", u.String(), "", argv) + if err != nil { + return err + } + + err = ioutil.WriteFile(filename, *response, 0644) + if err != nil { + return err + } + + ctx.String("backup written successfully to %s\n", filename) + return nil +}