The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/4459
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === This is a first stab at #4390, it implements the low-hanging fruits. The rest will come in separate branches.
From 449c3868c5e0d23a7e456631ebe93fb603ce789e Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Mon, 16 Apr 2018 15:14:37 +0000 Subject: [PATCH 1/4] Handle empty query strings Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- lxd/api_internal.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lxd/api_internal.go b/lxd/api_internal.go index 5f234537d..93ec99ad7 100644 --- a/lxd/api_internal.go +++ b/lxd/api_internal.go @@ -107,6 +107,9 @@ func internalSQL(d *Daemon, r *http.Request) Response { if err != nil { return BadRequest(err) } + if req.Query == "" { + return BadRequest(fmt.Errorf("No query provided")) + } db := d.cluster.DB() result := internalSQLResult{} if strings.HasPrefix(strings.ToUpper(req.Query), "SELECT") { From bf99c6837db2d8381068877cf16e95a3585f37f5 Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Mon, 16 Apr 2018 15:21:25 +0000 Subject: [PATCH 2/4] Support reading queries from standard in Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- lxd/main_sql.go | 15 +++++++++++++++ test/main.sh | 1 + test/suites/sql.sh | 12 ++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 test/suites/sql.sh diff --git a/lxd/main_sql.go b/lxd/main_sql.go index b466cf7a1..ed961e4f1 100644 --- a/lxd/main_sql.go +++ b/lxd/main_sql.go @@ -3,10 +3,13 @@ package main import ( "encoding/json" "fmt" + "io/ioutil" + "os" "strconv" "strings" "time" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/lxc/lxd/client" @@ -23,6 +26,9 @@ func (c *cmdSql) Command() *cobra.Command { cmd.Long = `Description: Execute a SQL query against the LXD database + If <query> is the special value "-", than the query is read from + standard input. + This internal command is mostly useful for debugging and disaster recovery. The LXD team will occasionally provide hotfixes to users as a set of database queries to fix some data inconsistency. @@ -49,6 +55,15 @@ func (c *cmdSql) Run(cmd *cobra.Command, args []string) error { query := args[0] + if query == "-" { + // Read from stdin + bytes, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return errors.Wrap(err, "Failed to read from stdin") + } + query = string(bytes) + } + // Connect to LXD d, err := lxd.ConnectLXDUnix("", nil) if err != nil { diff --git a/test/main.sh b/test/main.sh index be52c3b9e..ee8c4a89f 100755 --- a/test/main.sh +++ b/test/main.sh @@ -204,6 +204,7 @@ run_test test_clustering_containers "clustering containers" run_test test_clustering_storage "clustering storage" run_test test_clustering_network "clustering network" #run_test test_clustering_upgrade "clustering upgrade" +run_test test_sql "lxd sql" # shellcheck disable=SC2034 TEST_RESULT=success diff --git a/test/suites/sql.sh b/test/suites/sql.sh new file mode 100644 index 000000000..c0f50fb01 --- /dev/null +++ b/test/suites/sql.sh @@ -0,0 +1,12 @@ +# Test the lxd sql command. +test_sql() { + # Invalid arguments + ! lxd sql "" + ! lxd sql + + # Single query + lxd sql "SELECT * FROM config" | grep "core.trust_password" + + # Standard input + echo "SELECT * FROM config" | lxd sql - | grep "core.trust_password" +} From 0940f8f8c34a1f4607544c664d5fb8572ea6162a Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Mon, 16 Apr 2018 15:52:05 +0000 Subject: [PATCH 3/4] Support passing multiple queries Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- lxd/api_internal.go | 99 +++++++++++++++++++++++++++++++++-------------------- lxd/main_sql.go | 96 ++++++++++++++++++++++++++++----------------------- test/suites/sql.sh | 7 ++-- 3 files changed, 121 insertions(+), 81 deletions(-) diff --git a/lxd/api_internal.go b/lxd/api_internal.go index 93ec99ad7..daf29e66c 100644 --- a/lxd/api_internal.go +++ b/lxd/api_internal.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/gorilla/mux" + "github.com/pkg/errors" "gopkg.in/yaml.v2" "github.com/lxc/lxd/lxd/db" @@ -94,7 +95,12 @@ type internalSQLPost struct { Query string `json:"query" yaml:"query"` } +type internalSQLBatch struct { + Results []internalSQLResult +} + type internalSQLResult struct { + Type string `json:"type" yaml:"type"` Columns []string `json:"columns" yaml:"columns"` Rows [][]interface{} `json:"rows" yaml:"rows"` RowsAffected int64 `json:"rows_affected" yaml:"rows_affected"` @@ -111,53 +117,72 @@ func internalSQL(d *Daemon, r *http.Request) Response { return BadRequest(fmt.Errorf("No query provided")) } db := d.cluster.DB() - result := internalSQLResult{} - if strings.HasPrefix(strings.ToUpper(req.Query), "SELECT") { - rows, err := db.Query(req.Query) - if err != nil { - return SmartError(err) + batch := internalSQLBatch{} + for _, query := range strings.Split(req.Query, ";") { + query = strings.TrimLeft(query, " ") + result := internalSQLResult{} + if strings.HasPrefix(strings.ToUpper(query), "SELECT") { + err = internalSQLSelect(db, query, &result) + } else { + err = internalSQLExec(db, query, &result) } - defer rows.Close() - result.Columns, err = rows.Columns() if err != nil { return SmartError(err) } - for rows.Next() { - row := make([]interface{}, len(result.Columns)) - rowPointers := make([]interface{}, len(result.Columns)) - for i := range row { - rowPointers[i] = &row[i] - } - err := rows.Scan(rowPointers...) - if err != nil { - return SmartError(err) - } - for i, column := range row { - // Convert bytes to string. This is safe as - // long as we don't have any BLOB column type. - data, ok := column.([]byte) - if ok { - row[i] = string(data) - } - } - result.Rows = append(result.Rows, row) - } - err = rows.Err() - if err != nil { - return SmartError(err) + batch.Results = append(batch.Results, result) + } + return SyncResponse(true, batch) +} + +func internalSQLSelect(db *sql.DB, query string, result *internalSQLResult) error { + result.Type = "select" + rows, err := db.Query(query) + if err != nil { + return errors.Wrap(err, "failed to execute query") + } + defer rows.Close() + result.Columns, err = rows.Columns() + if err != nil { + return errors.Wrap(err, "failed to fetch colume names") + } + for rows.Next() { + row := make([]interface{}, len(result.Columns)) + rowPointers := make([]interface{}, len(result.Columns)) + for i := range row { + rowPointers[i] = &row[i] } - } else { - r, err := db.Exec(req.Query) + err := rows.Scan(rowPointers...) if err != nil { - return SmartError(err) + return errors.Wrap(err, "failed to scan row") } - result.RowsAffected, err = r.RowsAffected() - if err != nil { - return SmartError(err) + for i, column := range row { + // Convert bytes to string. This is safe as + // long as we don't have any BLOB column type. + data, ok := column.([]byte) + if ok { + row[i] = string(data) + } } + result.Rows = append(result.Rows, row) + } + err = rows.Err() + if err != nil { + return errors.Wrap(err, "rows error") + } + return nil +} +func internalSQLExec(db *sql.DB, query string, result *internalSQLResult) error { + result.Type = "exec" + r, err := db.Exec(query) + if err != nil { + return errors.Wrapf(err, "failed to exec query") + } + result.RowsAffected, err = r.RowsAffected() + if err != nil { + return errors.Wrap(err, "failed to fetch affected rows") } - return SyncResponse(true, result) + return nil } var internalShutdownCmd = Command{name: "shutdown", put: internalShutdown} diff --git a/lxd/main_sql.go b/lxd/main_sql.go index ed961e4f1..7f37eae95 100644 --- a/lxd/main_sql.go +++ b/lxd/main_sql.go @@ -78,54 +78,66 @@ func (c *cmdSql) Run(cmd *cobra.Command, args []string) error { return err } - result := internalSQLResult{} - err = json.Unmarshal(response.Metadata, &result) + batch := internalSQLBatch{} + err = json.Unmarshal(response.Metadata, &batch) if err != nil { return err } - if strings.HasPrefix(strings.ToUpper(query), "SELECT") { - // Print results in tabular format - widths := make([]int, len(result.Columns)) - for i, column := range result.Columns { - widths[i] = len(column) + for i, result := range batch.Results { + if len(batch.Results) > 1 { + fmt.Printf("=> Query %d:\n\n", i) } - for _, row := range result.Rows { - for i, v := range row { - width := 10 - switch v := v.(type) { - case string: - width = len(v) - case int: - width = 6 - case int64: - width = 6 - case time.Time: - width = 12 - } - if width > widths[i] { - widths[i] = width - } - } - } - format := "|" - separator := "+" - columns := make([]interface{}, len(result.Columns)) - for i, column := range result.Columns { - format += " %-" + strconv.Itoa(widths[i]) + "v |" - columns[i] = column - separator += strings.Repeat("-", widths[i]+2) + "+" + if result.Type == "select" { + sqlPrintSelectResult(result) + } else { + fmt.Printf("Rows affected: %d\n", result.RowsAffected) } - format += "\n" - separator += "\n" - fmt.Printf(separator) - fmt.Printf(fmt.Sprintf(format, columns...)) - fmt.Printf(separator) - for _, row := range result.Rows { - fmt.Printf(format, row...) + if len(batch.Results) > 1 { + fmt.Printf("\n") } - fmt.Printf(separator) - } else { - fmt.Printf("Rows affected: %d\n", result.RowsAffected) } return nil } + +func sqlPrintSelectResult(result internalSQLResult) { + // Print results in tabular format + widths := make([]int, len(result.Columns)) + for i, column := range result.Columns { + widths[i] = len(column) + } + for _, row := range result.Rows { + for i, v := range row { + width := 10 + switch v := v.(type) { + case string: + width = len(v) + case int: + width = 6 + case int64: + width = 6 + case time.Time: + width = 12 + } + if width > widths[i] { + widths[i] = width + } + } + } + format := "|" + separator := "+" + columns := make([]interface{}, len(result.Columns)) + for i, column := range result.Columns { + format += " %-" + strconv.Itoa(widths[i]) + "v |" + columns[i] = column + separator += strings.Repeat("-", widths[i]+2) + "+" + } + format += "\n" + separator += "\n" + fmt.Printf(separator) + fmt.Printf(fmt.Sprintf(format, columns...)) + fmt.Printf(separator) + for _, row := range result.Rows { + fmt.Printf(format, row...) + } + fmt.Printf(separator) +} diff --git a/test/suites/sql.sh b/test/suites/sql.sh index c0f50fb01..9a687ce51 100644 --- a/test/suites/sql.sh +++ b/test/suites/sql.sh @@ -5,8 +5,11 @@ test_sql() { ! lxd sql # Single query - lxd sql "SELECT * FROM config" | grep "core.trust_password" + lxd sql "SELECT * FROM config" | grep -q "core.trust_password" # Standard input - echo "SELECT * FROM config" | lxd sql - | grep "core.trust_password" + echo "SELECT * FROM config" | lxd sql - | grep -q "core.trust_password" + + # Multiple queries + lxd sql "SELECT * FROM config; SELECT * FROM containers" | grep -q "=> Query 0" } From 938d10eaa97078e320479f14b01cfb67d23b2630 Mon Sep 17 00:00:00 2001 From: Free Ekanayaka <free.ekanay...@canonical.com> Date: Mon, 16 Apr 2018 16:05:05 +0000 Subject: [PATCH 4/4] Support querying both local and global database Signed-off-by: Free Ekanayaka <free.ekanay...@canonical.com> --- lxd/api_internal.go | 17 +++++++++++++++-- lxd/main_sql.go | 30 ++++++++++++++++++++++++------ test/suites/sql.sh | 14 +++++++++----- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/lxd/api_internal.go b/lxd/api_internal.go index daf29e66c..450436383 100644 --- a/lxd/api_internal.go +++ b/lxd/api_internal.go @@ -92,7 +92,8 @@ func internalContainerOnStop(d *Daemon, r *http.Request) Response { } type internalSQLPost struct { - Query string `json:"query" yaml:"query"` + Database string `json:"database" yaml:"database"` + Query string `json:"query" yaml:"query"` } type internalSQLBatch struct { @@ -113,10 +114,22 @@ func internalSQL(d *Daemon, r *http.Request) Response { if err != nil { return BadRequest(err) } + + if !shared.StringInSlice(req.Database, []string{"local", "global"}) { + return BadRequest(fmt.Errorf("Invalid database")) + } + if req.Query == "" { return BadRequest(fmt.Errorf("No query provided")) } - db := d.cluster.DB() + + var db *sql.DB + if req.Database == "global" { + db = d.cluster.DB() + } else { + db = d.db.DB() + } + batch := internalSQLBatch{} for _, query := range strings.Split(req.Query, ";") { query = strings.TrimLeft(query, " ") diff --git a/lxd/main_sql.go b/lxd/main_sql.go index 7f37eae95..be0198e4f 100644 --- a/lxd/main_sql.go +++ b/lxd/main_sql.go @@ -13,6 +13,7 @@ import ( "github.com/spf13/cobra" "github.com/lxc/lxd/client" + "github.com/lxc/lxd/shared" ) type cmdSql struct { @@ -21,10 +22,19 @@ type cmdSql struct { func (c *cmdSql) Command() *cobra.Command { cmd := &cobra.Command{} - cmd.Use = "sql <query>" - cmd.Short = "Execute a SQL query against the LXD database" + cmd.Use = "sql <local|global> <query>" + cmd.Short = "Execute a SQL query against the LXD local or global database" cmd.Long = `Description: - Execute a SQL query against the LXD database + Execute a SQL query against the LXD local or global database + + The local database is specific to the LXD cluster member you target the + command to, and contains member-specific data (such as the member address). + + The global database is common to all LXD members in the cluster, and contains + cluster-specific data (such as profiles, containers, etc). + + If you are running a non-clustered LXD instance, the same applies, as that + instance forms effectively a single-member cluster. If <query> is the special value "-", than the query is read from standard input. @@ -43,7 +53,7 @@ func (c *cmdSql) Command() *cobra.Command { } func (c *cmdSql) Run(cmd *cobra.Command, args []string) error { - if len(args) != 1 { + if len(args) != 2 { cmd.Help() if len(args) == 0 { @@ -53,7 +63,14 @@ func (c *cmdSql) Run(cmd *cobra.Command, args []string) error { return fmt.Errorf("Missing required arguments") } - query := args[0] + database := args[0] + query := args[1] + + if !shared.StringInSlice(database, []string{"local", "global"}) { + cmd.Help() + + return fmt.Errorf("Invalid database type") + } if query == "-" { // Read from stdin @@ -71,7 +88,8 @@ func (c *cmdSql) Run(cmd *cobra.Command, args []string) error { } data := internalSQLPost{ - Query: query, + Database: database, + Query: query, } response, _, err := d.RawQuery("POST", "/internal/sql", data, "") if err != nil { diff --git a/test/suites/sql.sh b/test/suites/sql.sh index 9a687ce51..b158722e1 100644 --- a/test/suites/sql.sh +++ b/test/suites/sql.sh @@ -1,15 +1,19 @@ # Test the lxd sql command. test_sql() { # Invalid arguments - ! lxd sql "" ! lxd sql + ! lxd sql foo "SELECT * FROM CONFIG" + ! lxd sql global "" - # Single query - lxd sql "SELECT * FROM config" | grep -q "core.trust_password" + # Local database + lxd sql local "SELECT * FROM config" | grep -q "core.https_address" + + # Global database + lxd sql global "SELECT * FROM config" | grep -q "core.trust_password" # Standard input - echo "SELECT * FROM config" | lxd sql - | grep -q "core.trust_password" + echo "SELECT * FROM config" | lxd sql global - | grep -q "core.trust_password" # Multiple queries - lxd sql "SELECT * FROM config; SELECT * FROM containers" | grep -q "=> Query 0" + lxd sql global "SELECT * FROM config; SELECT * FROM containers" | grep -q "=> Query 0" }
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel