diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index dfdc867d4a4..26a1e05e813 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -9,6 +9,7 @@ import ( "time" "github.com/grafana/grafana/pkg/api/cloudwatch" + "github.com/grafana/grafana/pkg/api/sqldb" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" @@ -85,11 +86,6 @@ func ProxyDataSourceRequest(c *middleware.Context) { return } - if ds.Type == m.DS_CLOUDWATCH { - cloudwatch.HandleRequest(c, ds) - return - } - if ds.Type == m.DS_INFLUXDB { if c.Query("db") != ds.Database { c.JsonApiErr(403, "Datasource is not configured to allow this database", nil) @@ -97,12 +93,24 @@ func ProxyDataSourceRequest(c *middleware.Context) { } } - targetUrl, _ := url.Parse(ds.Url) - if len(setting.DataProxyWhiteList) > 0 { - if _, exists := setting.DataProxyWhiteList[targetUrl.Host]; !exists { - c.JsonApiErr(403, "Data proxy hostname and ip are not included in whitelist", nil) + if ds.Type == m.DS_CLOUDWATCH { + cloudwatch.HandleRequest(c, ds) + return + } + + if ds.Type == m.DS_SQLDB { + targetUrl, _ := url.Parse(ds.Url) + if !checkWhiteList(c, targetUrl.Host) { return } + + sqldb.HandleRequest(c, ds) + return + } + + targetUrl, _ := url.Parse(ds.Url) + if !checkWhiteList(c, targetUrl.Host) { + return } proxyPath := c.Params("*") @@ -157,3 +165,14 @@ func logProxyRequest(dataSourceType string, c *middleware.Context) { "method", c.Req.Request.Method, "body", body) } + +func checkWhiteList(c *middleware.Context, host string) bool { + if host != "" && len(setting.DataProxyWhiteList) > 0 { + if _, exists := setting.DataProxyWhiteList[host]; !exists { + c.JsonApiErr(403, "Data proxy hostname and ip are not included in whitelist", nil) + return false + } + } + + return true +} diff --git a/pkg/api/sqldb/sqldb.go b/pkg/api/sqldb/sqldb.go new file mode 100644 index 00000000000..d6d05ffd7d6 --- /dev/null +++ b/pkg/api/sqldb/sqldb.go @@ -0,0 +1,164 @@ +package sqldb + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "strings" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/middleware" + m "github.com/grafana/grafana/pkg/models" + + _ "github.com/go-sql-driver/mysql" + "github.com/go-xorm/core" + "github.com/go-xorm/xorm" + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" +) + +type sqlDataRequest struct { + Query string `json:"query"` + Body []byte `json:"-"` +} + +type seriesStruct struct { + Columns []string `json:"columns"` + Name string `json:"name"` + Values [][]interface{} `json:"values"` +} + +type resultsStruct struct { + Series []seriesStruct `json:"series"` +} + +type dataStruct struct { + Results []resultsStruct `json:"results"` +} + +func getEngine(ds *m.DataSource) (*xorm.Engine, error) { + dbms, err := ds.JsonData.Get("dbms").String() + if err != nil { + return nil, errors.New("Invalid DBMS") + } + + host, err := ds.JsonData.Get("host").String() + if err != nil { + return nil, errors.New("Invalid host") + } + + port, err := ds.JsonData.Get("port").String() + if err != nil { + return nil, errors.New("Invalid port") + } + + constr := "" + + switch dbms { + case "mysql": + constr = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8", + ds.User, ds.Password, host, port, ds.Database) + + case "postgres": + sslEnabled, _ := ds.JsonData.Get("ssl").Bool() + sslMode := "disable" + if sslEnabled { + sslMode = "require" + } + + constr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s", + ds.User, ds.Password, host, port, ds.Database, sslMode) + + default: + return nil, fmt.Errorf("Unknown DBMS: %s", dbms) + } + + return xorm.NewEngine(dbms, constr) +} + +func getData(db *core.DB, req *sqlDataRequest) (interface{}, error) { + queries := strings.Split(req.Query, ";") + + data := dataStruct{} + data.Results = make([]resultsStruct, 1) + data.Results[0].Series = make([]seriesStruct, 0) + + for i := range queries { + if queries[i] == "" { + continue + } + + rows, err := db.Query(queries[i]) + if err != nil { + return nil, err + } + defer rows.Close() + + name := fmt.Sprintf("table_%d", i+1) + series, err := arrangeResult(rows, name) + if err != nil { + return nil, err + } + data.Results[0].Series = append(data.Results[0].Series, series.(seriesStruct)) + } + + return data, nil +} + +func arrangeResult(rows *core.Rows, name string) (interface{}, error) { + columnNames, err := rows.Columns() + + series := seriesStruct{} + series.Columns = columnNames + series.Name = name + + for rows.Next() { + columnValues := make([]interface{}, len(columnNames)) + + err = rows.ScanSlice(&columnValues) + if err != nil { + return nil, err + } + + // bytes -> string + for i := range columnValues { + switch columnValues[i].(type) { + case []byte: + columnValues[i] = fmt.Sprintf("%s", columnValues[i]) + } + } + + series.Values = append(series.Values, columnValues) + } + + return series, err +} + +func HandleRequest(c *middleware.Context, ds *m.DataSource) { + var req sqlDataRequest + req.Body, _ = ioutil.ReadAll(c.Req.Request.Body) + json.Unmarshal(req.Body, &req) + + log.Debug("SQL request: query='%v'", req.Query) + + engine, err := getEngine(ds) + if err != nil { + c.JsonApiErr(500, "Unable to open SQL connection", err) + return + } + defer engine.Close() + + session := engine.NewSession() + defer session.Close() + + db := session.DB() + + result, err := getData(db, &req) + if err != nil { + c.JsonApiErr(500, fmt.Sprintf("Data error: %v, Query: %s", err.Error(), req.Query), err) + return + } + + c.JSON(200, result) +} diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 4a90edd9bfd..58bf8b4ca18 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -17,6 +17,7 @@ const ( DS_CLOUDWATCH = "cloudwatch" DS_KAIROSDB = "kairosdb" DS_PROMETHEUS = "prometheus" + DS_SQLDB = "sqldb" DS_ACCESS_DIRECT = "direct" DS_ACCESS_PROXY = "proxy" ) @@ -62,6 +63,7 @@ var knownDatasourcePlugins map[string]bool = map[string]bool{ DS_CLOUDWATCH: true, DS_PROMETHEUS: true, DS_OPENTSDB: true, + DS_SQLDB: true, "opennms": true, "druid": true, "dalmatinerdb": true,