diff --git a/CHANGELOG.md b/CHANGELOG.md index 94c48f00..3281682a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -## 8.10.1 (unreleased) +## 8.11.0 (December 17th 2023) +### New features +- [PR #1488](https://github.com/rqlite/rqlite/pull/1488): Add `.boot` command to rqlite shell, to support _Booting_ nodes. + ### Implementation changes and bug fixes - [PR #1487](https://github.com/rqlite/rqlite/pull/1487): Small improvements to rqlite shell. diff --git a/cmd/rqlite/backup.go b/cmd/rqlite/backup.go index 72793968..3ffc55ea 100644 --- a/cmd/rqlite/backup.go +++ b/cmd/rqlite/backup.go @@ -82,6 +82,20 @@ func dump(ctx *cli.Context, filename string, argv *argT) error { return nil } +func validSQLiteFile(path string) bool { + file, err := os.Open(path) + if err != nil { + return false + } + defer file.Close() + b := make([]byte, 16) + _, err = file.Read(b) + if err != nil { + return false + } + return validSQLiteData(b) +} + func validSQLiteData(b []byte) bool { return len(b) > 13 && string(b[0:13]) == "SQLite format" } @@ -188,3 +202,89 @@ func restore(ctx *cli.Context, filename string, argv *argT) error { ctx.String("database restored successfully\n") return nil } + +func boot(ctx *cli.Context, filename string, argv *argT) error { + statusURL := fmt.Sprintf("%s://%s:%d/status", argv.Protocol, argv.Host, argv.Port) + client := http.Client{Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{InsecureSkipVerify: argv.Insecure}, + }} + + req, err := http.NewRequest("GET", statusURL, nil) + if err != nil { + return err + } + if argv.Credentials != "" { + creds := strings.Split(argv.Credentials, ":") + if len(creds) != 2 { + return fmt.Errorf("invalid Basic Auth credentials format") + } + req.SetBasicAuth(creds[0], creds[1]) + } + + // Check that we can talk to the node OK. + statusResp, err := client.Do(req) + if err != nil { + return err + } + defer statusResp.Body.Close() + if statusResp.StatusCode == http.StatusUnauthorized { + return fmt.Errorf("unauthorized") + } + body, err := io.ReadAll(statusResp.Body) + if err != nil { + return err + } + statusRet := &statusResponse{} + if err := parseResponse(&body, &statusRet); err != nil { + return err + } + if statusRet.Store == nil { + return fmt.Errorf("unexpected server response: store status not found") + } + + // File is OK? + if !validSQLiteFile(filename) { + return fmt.Errorf("%s is not a valid SQLite file", filename) + } + + // Do the boot. + fd, err := os.Open(filename) + if err != nil { + return err + } + defer fd.Close() + + bootURL := fmt.Sprintf("%s://%s:%d/boot", argv.Protocol, argv.Host, argv.Port) + req, err = http.NewRequest("POST", bootURL, fd) + if err != nil { + return err + } + if argv.Credentials != "" { + creds := strings.Split(argv.Credentials, ":") + if len(creds) != 2 { + return fmt.Errorf("invalid Basic Auth credentials format") + } + req.SetBasicAuth(creds[0], creds[1]) + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + body, err = io.ReadAll(resp.Body) + if err != nil { + return err + } + if resp.StatusCode != http.StatusOK { + errMsg := fmt.Sprintf("boot failed, status code: %s", resp.Status) + if len(body) > 0 { + errMsg += fmt.Sprintf(", %s", string(body)) + } + return fmt.Errorf(errMsg) + } + + ctx.String("node booted successfully\n") + return nil +} diff --git a/cmd/rqlite/main.go b/cmd/rqlite/main.go index a37696b2..a00b36dd 100644 --- a/cmd/rqlite/main.go +++ b/cmd/rqlite/main.go @@ -50,6 +50,7 @@ type argT struct { var cliHelp = []string{ `.backup Write database backup to SQLite file`, + `.boot Boot the node from a SQLite database file`, `.consistency [none|weak|strong] Show or set read consistency level`, `.dump Dump the database in SQL text format to a file`, `.exit Exit this program`, @@ -191,6 +192,12 @@ func main() { break } err = restore(ctx, line[index+1:], argv) + case ".BOOT": + if index == -1 || index == len(line)-1 { + err = fmt.Errorf("please specify an input file to boot with") + break + } + err = boot(ctx, line[index+1:], argv) case ".SYSDUMP": if index == -1 || index == len(line)-1 { err = fmt.Errorf("please specify an output file for the sysdump")