Merge branch 'master' into sql-proxy
This commit is contained in:
+4
-2
@@ -123,6 +123,7 @@ func (hs *HttpServer) registerRoutes() {
|
||||
// users (admin permission required)
|
||||
r.Group("/users", func() {
|
||||
r.Get("/", wrap(SearchUsers))
|
||||
r.Get("/search", wrap(SearchUsersWithPaging))
|
||||
r.Get("/:id", wrap(GetUserById))
|
||||
r.Get("/:id/orgs", wrap(GetUserOrgList))
|
||||
// query parameters /users/lookup?loginOrEmail=admin@example.com
|
||||
@@ -195,10 +196,11 @@ func (hs *HttpServer) registerRoutes() {
|
||||
|
||||
// Data sources
|
||||
r.Group("/datasources", func() {
|
||||
r.Get("/", GetDataSources)
|
||||
r.Get("/", wrap(GetDataSources))
|
||||
r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
|
||||
r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
|
||||
r.Delete("/:id", DeleteDataSource)
|
||||
r.Delete("/:id", DeleteDataSourceById)
|
||||
r.Delete("/name/:name", DeleteDataSourceByName)
|
||||
r.Get("/:id", wrap(GetDataSourceById))
|
||||
r.Get("/name/:name", wrap(GetDataSourceByName))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHttpApi(t *testing.T) {
|
||||
|
||||
// Convey("Given the grafana api", t, func() {
|
||||
// ConveyApiScenario("Can sign up", func(c apiTestContext) {
|
||||
// c.PostJson()
|
||||
// So(c.Resp, ShouldEqualJsonApiResponse, "User created and logged in")
|
||||
// })
|
||||
//
|
||||
// m := macaron.New()
|
||||
// m.Use(middleware.GetContextHandler())
|
||||
// m.Use(middleware.Sessioner(&session.Options{}))
|
||||
// Register(m)
|
||||
//
|
||||
// var context *middleware.Context
|
||||
// m.Get("/", func(c *middleware.Context) {
|
||||
// context = c
|
||||
// })
|
||||
//
|
||||
// resp := httptest.NewRecorder()
|
||||
// req, err := http.NewRequest("GET", "/", nil)
|
||||
// So(err, ShouldBeNil)
|
||||
//
|
||||
// m.ServeHTTP(resp, req)
|
||||
//
|
||||
// Convey("should red 200", func() {
|
||||
// So(resp.Code, ShouldEqual, 200)
|
||||
// })
|
||||
// })
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package cloudwatch
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/awsutil"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
@@ -114,19 +117,26 @@ func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
|
||||
DurationSeconds: aws.Int64(900),
|
||||
}
|
||||
|
||||
stsSess := session.New()
|
||||
stsSess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stsCreds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
|
||||
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(stsSess), ExpiryWindow: 5 * time.Minute},
|
||||
remoteCredProvider(stsSess),
|
||||
})
|
||||
stsConfig := &aws.Config{
|
||||
Region: aws.String(dsInfo.Region),
|
||||
Credentials: stsCreds,
|
||||
}
|
||||
|
||||
svc := sts.New(session.New(stsConfig), stsConfig)
|
||||
sess, err := session.NewSession(stsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svc := sts.New(sess, stsConfig)
|
||||
resp, err := svc.AssumeRole(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -139,7 +149,10 @@ func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
|
||||
}
|
||||
}
|
||||
|
||||
sess := session.New()
|
||||
sess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.StaticProvider{Value: credentials.Value{
|
||||
@@ -166,6 +179,30 @@ func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
func remoteCredProvider(sess *session.Session) credentials.Provider {
|
||||
ecsCredURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
|
||||
|
||||
if len(ecsCredURI) > 0 {
|
||||
return ecsCredProvider(sess, ecsCredURI)
|
||||
}
|
||||
return ec2RoleProvider(sess)
|
||||
}
|
||||
|
||||
func ecsCredProvider(sess *session.Session, uri string) credentials.Provider {
|
||||
const host = `169.254.170.2`
|
||||
|
||||
c := ec2metadata.New(sess)
|
||||
return endpointcreds.NewProviderClient(
|
||||
c.Client.Config,
|
||||
c.Client.Handlers,
|
||||
fmt.Sprintf("http://%s%s", host, uri),
|
||||
func(p *endpointcreds.Provider) { p.ExpiryWindow = 5 * time.Minute })
|
||||
}
|
||||
|
||||
func ec2RoleProvider(sess *session.Session) credentials.Provider {
|
||||
return &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute}
|
||||
}
|
||||
|
||||
func getAwsConfig(req *cwRequest) (*aws.Config, error) {
|
||||
creds, err := getCredentials(req.GetDatasourceInfo())
|
||||
if err != nil {
|
||||
@@ -185,7 +222,12 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
@@ -232,7 +274,12 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
@@ -273,7 +320,12 @@ func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
@@ -316,7 +368,12 @@ func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
@@ -360,7 +417,12 @@ func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
@@ -396,7 +458,12 @@ func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := ec2.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := ec2.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestECSCredProvider(t *testing.T) {
|
||||
Convey("Running in an ECS container task", t, func() {
|
||||
defer os.Clearenv()
|
||||
os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/abc/123")
|
||||
|
||||
provider := remoteCredProvider(&session.Session{})
|
||||
|
||||
So(provider, ShouldNotBeNil)
|
||||
|
||||
ecsProvider, ok := provider.(*endpointcreds.Provider)
|
||||
So(ecsProvider, ShouldNotBeNil)
|
||||
So(ok, ShouldBeTrue)
|
||||
|
||||
So(ecsProvider.Client.Endpoint, ShouldEqual, fmt.Sprintf("http://169.254.170.2/abc/123"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefaultEC2RoleProvider(t *testing.T) {
|
||||
Convey("Running outside an ECS container task", t, func() {
|
||||
provider := remoteCredProvider(&session.Session{})
|
||||
|
||||
So(provider, ShouldNotBeNil)
|
||||
|
||||
ec2Provider, ok := provider.(*ec2rolecreds.EC2RoleProvider)
|
||||
So(ec2Provider, ShouldNotBeNil)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
}
|
||||
@@ -81,7 +81,8 @@ func init() {
|
||||
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
||||
"AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "CommitLatency", "CommitThroughput", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "FailedSqlStatements", "FreeableMemory", "FreeStorageSpace", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
||||
"AWS/Route53": {"HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
|
||||
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects"},
|
||||
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
|
||||
"AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send"},
|
||||
"AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
|
||||
"AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
|
||||
"AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed",
|
||||
@@ -122,7 +123,8 @@ func init() {
|
||||
"AWS/Redshift": {"NodeID", "ClusterIdentifier"},
|
||||
"AWS/RDS": {"DBInstanceIdentifier", "DBClusterIdentifier", "DatabaseClass", "EngineName"},
|
||||
"AWS/Route53": {"HealthCheckId"},
|
||||
"AWS/S3": {"BucketName", "StorageType"},
|
||||
"AWS/S3": {"BucketName", "StorageType", "FilterId"},
|
||||
"AWS/SES": {},
|
||||
"AWS/SNS": {"Application", "Platform", "TopicName"},
|
||||
"AWS/SQS": {"QueueName"},
|
||||
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
|
||||
@@ -256,8 +258,11 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
|
||||
Region: aws.String(cwData.Region),
|
||||
Credentials: creds,
|
||||
}
|
||||
|
||||
svc := cloudwatch.New(session.New(cfg), cfg)
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return cloudwatch.ListMetricsOutput{}, err
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
params := &cloudwatch.ListMetricsInput{
|
||||
Namespace: aws.String(cwData.Namespace),
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/cloudwatch"
|
||||
@@ -115,6 +116,13 @@ func ProxyDataSourceRequest(c *middleware.Context) {
|
||||
|
||||
proxyPath := c.Params("*")
|
||||
|
||||
if ds.Type == m.DS_PROMETHEUS {
|
||||
if c.Req.Request.Method != http.MethodGet || !strings.HasPrefix(proxyPath, "api/") {
|
||||
c.JsonApiErr(403, "GET is only allowed on proxied Prometheus datasource", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ds.Type == m.DS_ES {
|
||||
if c.Req.Request.Method == "DELETE" {
|
||||
c.JsonApiErr(403, "Deletes not allowed on proxied Elasticsearch datasource", nil)
|
||||
|
||||
+25
-6
@@ -11,12 +11,11 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func GetDataSources(c *middleware.Context) {
|
||||
func GetDataSources(c *middleware.Context) Response {
|
||||
query := m.GetDataSourcesQuery{OrgId: c.OrgId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
c.JsonApiErr(500, "Failed to query datasources", err)
|
||||
return
|
||||
return ApiError(500, "Failed to query datasources", err)
|
||||
}
|
||||
|
||||
result := make(dtos.DataSourceList, 0)
|
||||
@@ -46,7 +45,8 @@ func GetDataSources(c *middleware.Context) {
|
||||
}
|
||||
|
||||
sort.Sort(result)
|
||||
c.JSON(200, result)
|
||||
|
||||
return Json(200, &result)
|
||||
}
|
||||
|
||||
func GetDataSourceById(c *middleware.Context) Response {
|
||||
@@ -68,7 +68,7 @@ func GetDataSourceById(c *middleware.Context) Response {
|
||||
return Json(200, &dtos)
|
||||
}
|
||||
|
||||
func DeleteDataSource(c *middleware.Context) {
|
||||
func DeleteDataSourceById(c *middleware.Context) {
|
||||
id := c.ParamsInt64(":id")
|
||||
|
||||
if id <= 0 {
|
||||
@@ -76,7 +76,26 @@ func DeleteDataSource(c *middleware.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := &m.DeleteDataSourceCommand{Id: id, OrgId: c.OrgId}
|
||||
cmd := &m.DeleteDataSourceByIdCommand{Id: id, OrgId: c.OrgId}
|
||||
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Failed to delete datasource", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JsonOK("Data source deleted")
|
||||
}
|
||||
|
||||
func DeleteDataSourceByName(c *middleware.Context) {
|
||||
name := c.Params(":name")
|
||||
|
||||
if name == "" {
|
||||
c.JsonApiErr(400, "Missing valid datasource name", nil)
|
||||
return
|
||||
}
|
||||
|
||||
cmd := &m.DeleteDataSourceByNameCommand{Name: name, OrgId: c.OrgId}
|
||||
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
const (
|
||||
TestOrgID = 1
|
||||
TestUserID = 1
|
||||
)
|
||||
|
||||
func TestDataSourcesProxy(t *testing.T) {
|
||||
Convey("Given a user is logged in", t, func() {
|
||||
loggedInUserScenario("When calling GET on", "/api/datasources/", func(sc *scenarioContext) {
|
||||
|
||||
// Stubs the database query
|
||||
bus.AddHandler("test", func(query *models.GetDataSourcesQuery) error {
|
||||
So(query.OrgId, ShouldEqual, TestOrgID)
|
||||
query.Result = []*models.DataSource{
|
||||
{Name: "mmm"},
|
||||
{Name: "ZZZ"},
|
||||
{Name: "BBB"},
|
||||
{Name: "aaa"},
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// handler func being tested
|
||||
sc.handlerFunc = GetDataSources
|
||||
sc.fakeReq("GET", "/api/datasources").exec()
|
||||
|
||||
respJSON := []map[string]interface{}{}
|
||||
err := json.NewDecoder(sc.resp.Body).Decode(&respJSON)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("should return list of datasources for org sorted alphabetically and case insensitively", func() {
|
||||
So(respJSON[0]["name"], ShouldEqual, "aaa")
|
||||
So(respJSON[1]["name"], ShouldEqual, "BBB")
|
||||
So(respJSON[2]["name"], ShouldEqual, "mmm")
|
||||
So(respJSON[3]["name"], ShouldEqual, "ZZZ")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
|
||||
Convey(desc+" "+url, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := &scenarioContext{
|
||||
url: url,
|
||||
}
|
||||
viewsPath, _ := filepath.Abs("../../public/views")
|
||||
|
||||
sc.m = macaron.New()
|
||||
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
|
||||
Directory: viewsPath,
|
||||
Delims: macaron.Delims{Left: "[[", Right: "]]"},
|
||||
}))
|
||||
|
||||
sc.m.Use(middleware.GetContextHandler())
|
||||
sc.m.Use(middleware.Sessioner(&session.Options{}))
|
||||
|
||||
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
|
||||
sc.context = c
|
||||
sc.context.UserId = TestUserID
|
||||
sc.context.OrgId = TestOrgID
|
||||
sc.context.OrgRole = models.ROLE_EDITOR
|
||||
if sc.handlerFunc != nil {
|
||||
return sc.handlerFunc(sc.context)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.m.Get(url, sc.defaultHandler)
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
|
||||
sc.resp = httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
So(err, ShouldBeNil)
|
||||
sc.req = req
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
|
||||
sc.resp = httptest.NewRecorder()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
q := req.URL.Query()
|
||||
for k, v := range queryParams {
|
||||
q.Add(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
So(err, ShouldBeNil)
|
||||
sc.req = req
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
m *macaron.Macaron
|
||||
context *middleware.Context
|
||||
resp *httptest.ResponseRecorder
|
||||
handlerFunc handlerFunc
|
||||
defaultHandler macaron.Handler
|
||||
req *http.Request
|
||||
url string
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) exec() {
|
||||
sc.m.ServeHTTP(sc.resp, sc.req)
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
||||
type handlerFunc func(c *middleware.Context) Response
|
||||
@@ -91,7 +91,7 @@ func (slice DataSourceList) Len() int {
|
||||
}
|
||||
|
||||
func (slice DataSourceList) Less(i, j int) bool {
|
||||
return slice[i].Name < slice[j].Name
|
||||
return strings.ToLower(slice[i].Name) < strings.ToLower(slice[j].Name)
|
||||
}
|
||||
|
||||
func (slice DataSourceList) Swap(i, j int) {
|
||||
|
||||
@@ -133,14 +133,17 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
||||
}
|
||||
|
||||
jsonObj := map[string]interface{}{
|
||||
"defaultDatasource": defaultDatasource,
|
||||
"datasources": datasources,
|
||||
"panels": panels,
|
||||
"appSubUrl": setting.AppSubUrl,
|
||||
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
|
||||
"authProxyEnabled": setting.AuthProxyEnabled,
|
||||
"ldapEnabled": setting.LdapEnabled,
|
||||
"alertingEnabled": setting.AlertingEnabled,
|
||||
"defaultDatasource": defaultDatasource,
|
||||
"datasources": datasources,
|
||||
"panels": panels,
|
||||
"appSubUrl": setting.AppSubUrl,
|
||||
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
|
||||
"authProxyEnabled": setting.AuthProxyEnabled,
|
||||
"ldapEnabled": setting.LdapEnabled,
|
||||
"alertingEnabled": setting.AlertingEnabled,
|
||||
"googleAnalyticsId": setting.GoogleAnalyticsId,
|
||||
"disableLoginForm": setting.DisableLoginForm,
|
||||
"disableSignoutMenu": setting.DisableSignoutMenu,
|
||||
"buildInfo": map[string]interface{}{
|
||||
"version": setting.BuildVersion,
|
||||
"commit": setting.BuildCommit,
|
||||
|
||||
+47
-3
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -24,6 +25,8 @@ type HttpServer struct {
|
||||
macaron *macaron.Macaron
|
||||
context context.Context
|
||||
streamManager *live.StreamManager
|
||||
|
||||
httpSrv *http.Server
|
||||
}
|
||||
|
||||
func NewHttpServer() *HttpServer {
|
||||
@@ -45,11 +48,20 @@ func (hs *HttpServer) Start(ctx context.Context) error {
|
||||
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
|
||||
hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl)
|
||||
|
||||
hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron}
|
||||
switch setting.Protocol {
|
||||
case setting.HTTP:
|
||||
err = http.ListenAndServe(listenAddr, hs.macaron)
|
||||
err = hs.httpSrv.ListenAndServe()
|
||||
if err == http.ErrServerClosed {
|
||||
hs.log.Debug("server was shutdown gracefully")
|
||||
return nil
|
||||
}
|
||||
case setting.HTTPS:
|
||||
err = hs.listenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile)
|
||||
err = hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
|
||||
if err == http.ErrServerClosed {
|
||||
hs.log.Debug("server was shutdown gracefully")
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
hs.log.Error("Invalid protocol", "protocol", setting.Protocol)
|
||||
err = errors.New("Invalid Protocol")
|
||||
@@ -58,6 +70,12 @@ func (hs *HttpServer) Start(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (hs *HttpServer) Shutdown(ctx context.Context) error {
|
||||
err := hs.httpSrv.Shutdown(ctx)
|
||||
hs.log.Info("stopped http server")
|
||||
return err
|
||||
}
|
||||
|
||||
func (hs *HttpServer) listenAndServeTLS(listenAddr, certfile, keyfile string) error {
|
||||
if certfile == "" {
|
||||
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
|
||||
@@ -75,7 +93,32 @@ func (hs *HttpServer) listenAndServeTLS(listenAddr, certfile, keyfile string) er
|
||||
return fmt.Errorf(`Cannot find SSL key_file at %v`, setting.KeyFile)
|
||||
}
|
||||
|
||||
return http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, hs.macaron)
|
||||
tlsCfg := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
PreferServerCipherSuites: true,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
},
|
||||
}
|
||||
srv := &http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: hs.macaron,
|
||||
TLSConfig: tlsCfg,
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
|
||||
}
|
||||
|
||||
return srv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
|
||||
}
|
||||
|
||||
func (hs *HttpServer) newMacaron() *macaron.Macaron {
|
||||
@@ -107,6 +150,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
|
||||
m.Use(middleware.GetContextHandler())
|
||||
m.Use(middleware.Sessioner(&setting.SessionOptions))
|
||||
m.Use(middleware.RequestMetrics())
|
||||
m.Use(middleware.OrgRedirect())
|
||||
|
||||
// needs to be after context handler
|
||||
if setting.EnforceDomain {
|
||||
|
||||
@@ -35,6 +35,11 @@ func LoginView(c *middleware.Context) {
|
||||
viewData.Settings["loginHint"] = setting.LoginHint
|
||||
viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
|
||||
|
||||
if loginError, ok := c.Session.Get("loginError").(string); ok {
|
||||
c.Session.Delete("loginError")
|
||||
viewData.Settings["loginError"] = loginError
|
||||
}
|
||||
|
||||
if !tryLoginUsingRememberCookie(c) {
|
||||
c.HTML(200, VIEW_INDEX, viewData)
|
||||
return
|
||||
@@ -94,6 +99,10 @@ func LoginApiPing(c *middleware.Context) {
|
||||
}
|
||||
|
||||
func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response {
|
||||
if setting.DisableLoginForm {
|
||||
return ApiError(401, "Login is disabled", nil)
|
||||
}
|
||||
|
||||
authQuery := login.LoginUserQuery{
|
||||
Username: cmd.User,
|
||||
Password: cmd.Password,
|
||||
|
||||
+27
-10
@@ -10,6 +10,7 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -22,6 +23,13 @@ import (
|
||||
"github.com/grafana/grafana/pkg/social"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
|
||||
ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
|
||||
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
|
||||
ErrUsersQuotaReached = errors.New("Users quota reached")
|
||||
)
|
||||
|
||||
func GenStateString() string {
|
||||
rnd := make([]byte, 32)
|
||||
rand.Read(rnd)
|
||||
@@ -44,8 +52,7 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
error := ctx.Query("error")
|
||||
if error != "" {
|
||||
errorDesc := ctx.Query("error_description")
|
||||
ctx.Logger.Info("OAuthLogin Failed", "error", error, "errorDesc", errorDesc)
|
||||
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1003")
|
||||
redirectWithError(ctx, ErrProviderDeniedRequest, "error", error, "errorDesc", errorDesc)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,10 +124,8 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
// get user info
|
||||
userInfo, err := connect.UserInfo(client)
|
||||
if err != nil {
|
||||
if err == social.ErrMissingTeamMembership {
|
||||
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1000")
|
||||
} else if err == social.ErrMissingOrganizationMembership {
|
||||
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1001")
|
||||
if sErr, ok := err.(*social.Error); ok {
|
||||
redirectWithError(ctx, sErr)
|
||||
} else {
|
||||
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
|
||||
}
|
||||
@@ -131,8 +136,7 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
|
||||
// validate that the email is allowed to login to grafana
|
||||
if !connect.IsEmailAllowed(userInfo.Email) {
|
||||
ctx.Logger.Info("OAuth login attempt with unallowed email", "email", userInfo.Email)
|
||||
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1002")
|
||||
redirectWithError(ctx, ErrEmailNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -142,7 +146,7 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
// create account if missing
|
||||
if err == m.ErrUserNotFound {
|
||||
if !connect.IsSignupAllowed() {
|
||||
ctx.Redirect(setting.AppSubUrl + "/login")
|
||||
redirectWithError(ctx, ErrSignUpNotAllowed)
|
||||
return
|
||||
}
|
||||
limitReached, err := middleware.QuotaReached(ctx, "user")
|
||||
@@ -151,7 +155,7 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
return
|
||||
}
|
||||
if limitReached {
|
||||
ctx.Redirect(setting.AppSubUrl + "/login")
|
||||
redirectWithError(ctx, ErrUsersQuotaReached)
|
||||
return
|
||||
}
|
||||
cmd := m.CreateUserCommand{
|
||||
@@ -177,5 +181,18 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
|
||||
metrics.M_Api_Login_OAuth.Inc(1)
|
||||
|
||||
if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
|
||||
ctx.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/")
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubUrl + "/")
|
||||
}
|
||||
|
||||
func redirectWithError(ctx *middleware.Context, err error, v ...interface{}) {
|
||||
ctx.Logger.Info(err.Error(), v...)
|
||||
// TODO: we can use the flash storage here once it's implemented
|
||||
ctx.Session.Set("loginError", err.Error())
|
||||
ctx.Redirect(setting.AppSubUrl + "/login")
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
|
||||
})
|
||||
}
|
||||
|
||||
resp, err := tsdb.HandleRequest(context.TODO(), request)
|
||||
resp, err := tsdb.HandleRequest(context.Background(), request)
|
||||
if err != nil {
|
||||
return ApiError(500, "Metric request error", err)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response
|
||||
if err != m.ErrUserNotFound {
|
||||
return ApiError(500, "Failed to query db for existing user check", err)
|
||||
}
|
||||
|
||||
if setting.DisableLoginForm {
|
||||
return ApiError(401, "User could not be found", nil)
|
||||
}
|
||||
} else {
|
||||
return inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
|
||||
}
|
||||
|
||||
+36
-2
@@ -210,14 +210,48 @@ func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand)
|
||||
|
||||
// GET /api/users
|
||||
func SearchUsers(c *middleware.Context) Response {
|
||||
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
query, err := searchUser(c)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to fetch users", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result.Users)
|
||||
}
|
||||
|
||||
// GET /api/search
|
||||
func SearchUsersWithPaging(c *middleware.Context) Response {
|
||||
query, err := searchUser(c)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to fetch users", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
}
|
||||
|
||||
func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) {
|
||||
perPage := c.QueryInt("perpage")
|
||||
if perPage <= 0 {
|
||||
perPage = 1000
|
||||
}
|
||||
page := c.QueryInt("page")
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
searchQuery := c.Query("query")
|
||||
|
||||
query := &m.SearchUsersQuery{Query: searchQuery, Page: page, Limit: perPage}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query.Result.Page = page
|
||||
query.Result.PerPage = perPage
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func SetHelpFlag(c *middleware.Context) Response {
|
||||
flag := c.ParamsInt64(":id")
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestUserApiEndpoint(t *testing.T) {
|
||||
Convey("Given a user is logged in", t, func() {
|
||||
mockResult := models.SearchUserQueryResult{
|
||||
Users: []*models.UserSearchHitDTO{
|
||||
{Name: "user1"},
|
||||
{Name: "user2"},
|
||||
},
|
||||
TotalCount: 2,
|
||||
}
|
||||
|
||||
loggedInUserScenario("When calling GET on", "/api/users", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchUsers
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 1000)
|
||||
So(sendPage, ShouldEqual, 1)
|
||||
|
||||
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
|
||||
So(err, ShouldBeNil)
|
||||
So(len(respJSON.MustArray()), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
loggedInUserScenario("When calling GET with page and limit querystring parameters on", "/api/users", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchUsers
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 10)
|
||||
So(sendPage, ShouldEqual, 2)
|
||||
})
|
||||
|
||||
loggedInUserScenario("When calling GET on", "/api/users/search", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchUsersWithPaging
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 1000)
|
||||
So(sendPage, ShouldEqual, 1)
|
||||
|
||||
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2)
|
||||
So(len(respJSON.Get("users").MustArray()), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
loggedInUserScenario("When calling GET with page and perpage querystring parameters on", "/api/users/search", func(sc *scenarioContext) {
|
||||
var sentLimit int
|
||||
var sendPage int
|
||||
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
|
||||
query.Result = mockResult
|
||||
|
||||
sentLimit = query.Limit
|
||||
sendPage = query.Page
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = SearchUsersWithPaging
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
||||
|
||||
So(sentLimit, ShouldEqual, 10)
|
||||
So(sendPage, ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func main() {
|
||||
cli.StringFlag{
|
||||
Name: "repo",
|
||||
Usage: "url to the plugin repository",
|
||||
Value: "https://grafana.net/api/plugins",
|
||||
Value: "https://grafana.com/api/plugins",
|
||||
EnvVar: "GF_PLUGIN_REPO",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
|
||||
@@ -40,6 +40,8 @@ func returnOsDefault(currentOs string) string {
|
||||
return "../data/plugins"
|
||||
case "darwin":
|
||||
return "/usr/local/var/lib/grafana/plugins"
|
||||
case "freebsd":
|
||||
return "/var/db/grafana/plugins"
|
||||
default: //"linux"
|
||||
return "/var/lib/grafana/plugins"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
@@ -39,6 +38,8 @@ type GrafanaServerImpl struct {
|
||||
shutdownFn context.CancelFunc
|
||||
childRoutines *errgroup.Group
|
||||
log log.Logger
|
||||
|
||||
httpServer *api.HttpServer
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Start() {
|
||||
@@ -74,9 +75,9 @@ func (g *GrafanaServerImpl) Start() {
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) startHttpServer() {
|
||||
httpServer := api.NewHttpServer()
|
||||
g.httpServer = api.NewHttpServer()
|
||||
|
||||
err := httpServer.Start(g.context)
|
||||
err := g.httpServer.Start(g.context)
|
||||
|
||||
if err != nil {
|
||||
g.log.Error("Fail to start server", "error", err)
|
||||
@@ -88,24 +89,15 @@ func (g *GrafanaServerImpl) startHttpServer() {
|
||||
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
||||
g.log.Info("Shutdown started", "code", code, "reason", reason)
|
||||
|
||||
err := g.httpServer.Shutdown(g.context)
|
||||
if err != nil {
|
||||
g.log.Error("Failed to shutdown server", "error", err)
|
||||
}
|
||||
|
||||
g.shutdownFn()
|
||||
err := g.childRoutines.Wait()
|
||||
err = g.childRoutines.Wait()
|
||||
|
||||
g.log.Info("Shutdown completed", "reason", err)
|
||||
log.Close()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// implement context.Context
|
||||
func (g *GrafanaServerImpl) Deadline() (deadline time.Time, ok bool) {
|
||||
return g.context.Deadline()
|
||||
}
|
||||
func (g *GrafanaServerImpl) Done() <-chan struct{} {
|
||||
return g.context.Done()
|
||||
}
|
||||
func (g *GrafanaServerImpl) Err() error {
|
||||
return g.context.Err()
|
||||
}
|
||||
func (g *GrafanaServerImpl) Value(key interface{}) interface{} {
|
||||
return g.context.Value(key)
|
||||
}
|
||||
|
||||
@@ -27,25 +27,15 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bucket := s3sec.Key("bucket_url").MustString("")
|
||||
bucketUrl := s3sec.Key("bucket_url").MustString("")
|
||||
accessKey := s3sec.Key("access_key").MustString("")
|
||||
secretKey := s3sec.Key("secret_key").MustString("")
|
||||
|
||||
region := ""
|
||||
rBucket := regexp.MustCompile(`https?:\/\/(.*)\.s3(-([^.]+))?\.amazonaws\.com\/?`)
|
||||
matches := rBucket.FindStringSubmatch(bucket)
|
||||
if len(matches) == 0 {
|
||||
return nil, fmt.Errorf("Could not find bucket setting for image.uploader.s3")
|
||||
} else {
|
||||
bucket = matches[1]
|
||||
if matches[3] != "" {
|
||||
region = matches[3]
|
||||
} else {
|
||||
region = "us-east-1"
|
||||
}
|
||||
info, err := getRegionAndBucketFromUrl(bucketUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewS3Uploader(region, bucket, "public-read", accessKey, secretKey), nil
|
||||
return NewS3Uploader(info.region, info.bucket, "public-read", accessKey, secretKey), nil
|
||||
case "webdav":
|
||||
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
|
||||
if err != nil {
|
||||
@@ -65,3 +55,37 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
|
||||
return NopImageUploader{}, nil
|
||||
}
|
||||
|
||||
type s3Info struct {
|
||||
region string
|
||||
bucket string
|
||||
}
|
||||
|
||||
func getRegionAndBucketFromUrl(url string) (*s3Info, error) {
|
||||
info := &s3Info{}
|
||||
urlRegex := regexp.MustCompile(`https?:\/\/(.*)\.s3(-([^.]+))?\.amazonaws\.com\/?`)
|
||||
matches := urlRegex.FindStringSubmatch(url)
|
||||
if len(matches) > 0 {
|
||||
info.bucket = matches[1]
|
||||
if matches[3] != "" {
|
||||
info.region = matches[3]
|
||||
} else {
|
||||
info.region = "us-east-1"
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
urlRegex2 := regexp.MustCompile(`https?:\/\/s3(-([^.]+))?\.amazonaws\.com\/(.*)?`)
|
||||
matches2 := urlRegex2.FindStringSubmatch(url)
|
||||
if len(matches2) > 0 {
|
||||
info.bucket = matches2[3]
|
||||
if matches2[2] != "" {
|
||||
info.region = matches2[2]
|
||||
} else {
|
||||
info.region = "us-east-1"
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Could not find bucket setting for image.uploader.s3")
|
||||
}
|
||||
|
||||
@@ -10,29 +10,66 @@ import (
|
||||
|
||||
func TestImageUploaderFactory(t *testing.T) {
|
||||
Convey("Can create image uploader for ", t, func() {
|
||||
Convey("S3ImageUploader", func() {
|
||||
var err error
|
||||
Convey("S3ImageUploader config", func() {
|
||||
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
HomePath: "../../../",
|
||||
})
|
||||
|
||||
setting.ImageUploadProvider = "s3"
|
||||
|
||||
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
|
||||
s3sec.NewKey("bucket_url", "https://foo.bar.baz.s3-us-east-2.amazonaws.com")
|
||||
s3sec.NewKey("access_key", "access_key")
|
||||
s3sec.NewKey("secret_key", "secret_key")
|
||||
Convey("with bucket url https://foo.bar.baz.s3-us-east-2.amazonaws.com", func() {
|
||||
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
|
||||
s3sec.NewKey("bucket_url", "https://foo.bar.baz.s3-us-east-2.amazonaws.com")
|
||||
s3sec.NewKey("access_key", "access_key")
|
||||
s3sec.NewKey("secret_key", "secret_key")
|
||||
|
||||
uploader, err := NewImageUploader()
|
||||
uploader, err := NewImageUploader()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
original, ok := uploader.(*S3Uploader)
|
||||
So(err, ShouldBeNil)
|
||||
original, ok := uploader.(*S3Uploader)
|
||||
|
||||
So(ok, ShouldBeTrue)
|
||||
So(original.region, ShouldEqual, "us-east-2")
|
||||
So(original.bucket, ShouldEqual, "foo.bar.baz")
|
||||
So(original.accessKey, ShouldEqual, "access_key")
|
||||
So(original.secretKey, ShouldEqual, "secret_key")
|
||||
So(ok, ShouldBeTrue)
|
||||
So(original.region, ShouldEqual, "us-east-2")
|
||||
So(original.bucket, ShouldEqual, "foo.bar.baz")
|
||||
So(original.accessKey, ShouldEqual, "access_key")
|
||||
So(original.secretKey, ShouldEqual, "secret_key")
|
||||
})
|
||||
|
||||
Convey("with bucket url https://s3.amazonaws.com/mybucket", func() {
|
||||
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
|
||||
s3sec.NewKey("bucket_url", "https://s3.amazonaws.com/my.bucket.com")
|
||||
s3sec.NewKey("access_key", "access_key")
|
||||
s3sec.NewKey("secret_key", "secret_key")
|
||||
|
||||
uploader, err := NewImageUploader()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
original, ok := uploader.(*S3Uploader)
|
||||
|
||||
So(ok, ShouldBeTrue)
|
||||
So(original.region, ShouldEqual, "us-east-1")
|
||||
So(original.bucket, ShouldEqual, "my.bucket.com")
|
||||
So(original.accessKey, ShouldEqual, "access_key")
|
||||
So(original.secretKey, ShouldEqual, "secret_key")
|
||||
})
|
||||
|
||||
Convey("with bucket url https://s3-us-west-2.amazonaws.com/mybucket", func() {
|
||||
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
|
||||
s3sec.NewKey("bucket_url", "https://s3-us-west-2.amazonaws.com/my.bucket.com")
|
||||
s3sec.NewKey("access_key", "access_key")
|
||||
s3sec.NewKey("secret_key", "secret_key")
|
||||
|
||||
uploader, err := NewImageUploader()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
original, ok := uploader.(*S3Uploader)
|
||||
|
||||
So(ok, ShouldBeTrue)
|
||||
So(original.region, ShouldEqual, "us-west-2")
|
||||
So(original.bucket, ShouldEqual, "my.bucket.com")
|
||||
So(original.accessKey, ShouldEqual, "access_key")
|
||||
So(original.secretKey, ShouldEqual, "secret_key")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Webdav uploader", func() {
|
||||
|
||||
@@ -35,7 +35,10 @@ func NewS3Uploader(region, bucket, acl, accessKey, secretKey string) *S3Uploader
|
||||
}
|
||||
|
||||
func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
|
||||
sess := session.New()
|
||||
sess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.StaticProvider{Value: credentials.Value{
|
||||
@@ -58,7 +61,11 @@ func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
svc := s3.New(session.New(cfg), cfg)
|
||||
sess, err = session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
svc := s3.New(sess, cfg)
|
||||
params := &s3.PutObjectInput{
|
||||
Bucket: aws.String(u.bucket),
|
||||
Key: aws.String(key),
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@@ -17,6 +19,18 @@ type WebdavUploader struct {
|
||||
password string
|
||||
}
|
||||
|
||||
var netTransport = &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 60 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
var netClient = &http.Client{
|
||||
Timeout: time.Second * 60,
|
||||
Transport: netTransport,
|
||||
}
|
||||
|
||||
func (u *WebdavUploader) Upload(pa string) (string, error) {
|
||||
url, _ := url.Parse(u.url)
|
||||
url.Path = path.Join(url.Path, util.GetRandomString(20)+".png")
|
||||
@@ -28,7 +42,7 @@ func (u *WebdavUploader) Upload(pa string) (string, error) {
|
||||
req.SetBasicAuth(u.username, u.password)
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
res, err := netClient.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -49,7 +49,9 @@ var (
|
||||
M_Alerting_Notification_Sent_Victorops Counter
|
||||
M_Alerting_Notification_Sent_OpsGenie Counter
|
||||
M_Alerting_Notification_Sent_Telegram Counter
|
||||
M_Alerting_Notification_Sent_Threema Counter
|
||||
M_Alerting_Notification_Sent_Sensu Counter
|
||||
M_Alerting_Notification_Sent_Pushover Counter
|
||||
M_Aws_CloudWatch_GetMetricStatistics Counter
|
||||
M_Aws_CloudWatch_ListMetrics Counter
|
||||
|
||||
@@ -118,8 +120,10 @@ func initMetricVars(settings *MetricSettings) {
|
||||
M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
|
||||
M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie")
|
||||
M_Alerting_Notification_Sent_Telegram = RegCounter("alerting.notifications_sent", "type", "telegram")
|
||||
M_Alerting_Notification_Sent_Threema = RegCounter("alerting.notifications_sent", "type", "threema")
|
||||
M_Alerting_Notification_Sent_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu")
|
||||
M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE")
|
||||
M_Alerting_Notification_Sent_Pushover = RegCounter("alerting.notifications_sent", "type", "pushover")
|
||||
|
||||
M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics")
|
||||
M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics")
|
||||
|
||||
@@ -90,6 +90,10 @@ func (s *mockSession) Get(k interface{}) interface{} {
|
||||
return s.value
|
||||
}
|
||||
|
||||
func (s *mockSession) Delete(k interface{}) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockSession) ID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -326,6 +326,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
|
||||
// mock out gc goroutine
|
||||
startSessionGC = func() {}
|
||||
sc.m.Use(Sessioner(&session.Options{}))
|
||||
sc.m.Use(OrgRedirect())
|
||||
|
||||
sc.defaultHandler = func(c *Context) {
|
||||
sc.context = c
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func OrgRedirect() macaron.Handler {
|
||||
return func(res http.ResponseWriter, req *http.Request, c *macaron.Context) {
|
||||
orgIdValue := req.URL.Query().Get("orgId")
|
||||
orgId, err := strconv.ParseInt(orgIdValue, 10, 32)
|
||||
|
||||
if err != nil || orgId == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, ok := c.Data["ctx"].(*Context)
|
||||
if !ok || !ctx.IsSignedIn {
|
||||
return
|
||||
}
|
||||
|
||||
if orgId == ctx.OrgId {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := models.SetUsingOrgCommand{UserId: ctx.UserId, OrgId: orgId}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if ctx.IsApiRequest() {
|
||||
ctx.JsonApiErr(404, "Not found", nil)
|
||||
} else {
|
||||
ctx.Error(404, "Not found")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(c.Req.URL.String(), 302)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestOrgRedirectMiddleware(t *testing.T) {
|
||||
|
||||
Convey("Can redirect to correct org", t, func() {
|
||||
middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) {
|
||||
sc.fakeReq("GET", "/").handler(func(c *Context) {
|
||||
c.Session.Set(SESS_KEY_USERID, int64(12))
|
||||
}).exec()
|
||||
|
||||
bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.m.Get("/", sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/?orgId=3").exec()
|
||||
|
||||
Convey("change org and redirect", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 302)
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) {
|
||||
sc.fakeReq("GET", "/").handler(func(c *Context) {
|
||||
c.Session.Set(SESS_KEY_USERID, int64(12))
|
||||
}).exec()
|
||||
|
||||
bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error {
|
||||
return fmt.Errorf("")
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
|
||||
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.m.Get("/", sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/?orgId=3").exec()
|
||||
|
||||
Convey("not allowed to change org", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 404)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/go-macaron/session"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
_ "github.com/go-macaron/session/mysql"
|
||||
_ "github.com/go-macaron/session/postgres"
|
||||
_ "github.com/go-macaron/session/redis"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
@@ -22,10 +24,12 @@ var sessionManager *session.Manager
|
||||
var sessionOptions *session.Options
|
||||
var startSessionGC func()
|
||||
var getSessionCount func() int
|
||||
var sessionLogger = log.New("session")
|
||||
|
||||
func init() {
|
||||
startSessionGC = func() {
|
||||
sessionManager.GC()
|
||||
sessionLogger.Debug("Session GC")
|
||||
time.AfterFunc(time.Duration(sessionOptions.Gclifetime)*time.Second, startSessionGC)
|
||||
}
|
||||
getSessionCount = func() int {
|
||||
@@ -67,7 +71,9 @@ func Sessioner(options *session.Options) macaron.Handler {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go startSessionGC()
|
||||
// start GC threads after some random seconds
|
||||
rndSeconds := 10 + rand.Int63n(180)
|
||||
time.AfterFunc(time.Duration(rndSeconds)*time.Second, startSessionGC)
|
||||
|
||||
return func(ctx *Context) {
|
||||
ctx.Next()
|
||||
@@ -87,6 +93,8 @@ type SessionStore interface {
|
||||
Set(interface{}, interface{}) error
|
||||
// Get gets value by given key in session.
|
||||
Get(interface{}) interface{}
|
||||
// Delete deletes a key from session.
|
||||
Delete(interface{}) interface{}
|
||||
// ID returns current session ID.
|
||||
ID() string
|
||||
// Release releases session resource and save data to provider.
|
||||
@@ -122,6 +130,13 @@ func (s *SessionWrapper) Get(k interface{}) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SessionWrapper) Delete(k interface{}) interface{} {
|
||||
if s.session != nil {
|
||||
return s.session.Delete(k)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SessionWrapper) ID() string {
|
||||
if s.session != nil {
|
||||
return s.session.ID()
|
||||
|
||||
@@ -35,6 +35,7 @@ const (
|
||||
|
||||
var (
|
||||
ErrCannotChangeStateOnPausedAlert error = fmt.Errorf("Cannot change state on pause alert")
|
||||
ErrRequiresNewState error = fmt.Errorf("update alert state requires a new state.")
|
||||
)
|
||||
|
||||
func (s AlertStateType) IsValid() bool {
|
||||
|
||||
@@ -122,11 +122,16 @@ type UpdateDataSourceCommand struct {
|
||||
Id int64 `json:"-"`
|
||||
}
|
||||
|
||||
type DeleteDataSourceCommand struct {
|
||||
type DeleteDataSourceByIdCommand struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
}
|
||||
|
||||
type DeleteDataSourceByNameCommand struct {
|
||||
Name string
|
||||
OrgId int64
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// QUERIES
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package models
|
||||
|
||||
import "context"
|
||||
|
||||
type GrafanaServer interface {
|
||||
context.Context
|
||||
|
||||
Start()
|
||||
Shutdown(code int, reason string)
|
||||
}
|
||||
|
||||
+8
-1
@@ -130,7 +130,14 @@ type SearchUsersQuery struct {
|
||||
Page int
|
||||
Limit int
|
||||
|
||||
Result []*UserSearchHitDTO
|
||||
Result SearchUserQueryResult
|
||||
}
|
||||
|
||||
type SearchUserQueryResult struct {
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
Users []*UserSearchHitDTO `json:"users"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
}
|
||||
|
||||
type GetUserOrgListQuery struct {
|
||||
|
||||
@@ -60,7 +60,7 @@ func checkForUpdates() {
|
||||
log.Trace("Checking for updates")
|
||||
|
||||
pluginSlugs := getAllExternalPluginSlugs()
|
||||
resp, err := httpClient.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
|
||||
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
|
||||
|
||||
if err != nil {
|
||||
log.Trace("Failed to get plugins repo from grafana.net, %v", err.Error())
|
||||
@@ -71,14 +71,14 @@ func checkForUpdates() {
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Trace("Update check failed, reading response from grafana.net, %v", err.Error())
|
||||
log.Trace("Update check failed, reading response from grafana.com, %v", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
gNetPlugins := []GrafanaNetPlugin{}
|
||||
err = json.Unmarshal(body, &gNetPlugins)
|
||||
if err != nil {
|
||||
log.Trace("Failed to unmarshal plugin repo, reading response from grafana.net, %v", err.Error())
|
||||
log.Trace("Failed to unmarshal plugin repo, reading response from grafana.com, %v", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
|
||||
matches = append(matches, &alerting.EvalMatch{
|
||||
Metric: series.Name,
|
||||
Value: reducedValue,
|
||||
Tags: series.Tags,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -78,7 +79,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
|
||||
|
||||
if context.IsTestRun {
|
||||
context.Logs = append(context.Logs, &alerting.ResultLogEntry{
|
||||
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Query Returned No Series (reduced to null/no value)", evalMatch),
|
||||
Message: fmt.Sprintf("Condition: Eval: %v, Query Returned No Series (reduced to null/no value)", evalMatch),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
|
||||
}
|
||||
}()
|
||||
|
||||
alertCtx, cancelFn := context.WithTimeout(context.TODO(), alertTimeout)
|
||||
alertCtx, cancelFn := context.WithTimeout(context.Background(), alertTimeout)
|
||||
|
||||
job.Running = true
|
||||
evalContext := NewEvalContext(alertCtx, job.Rule)
|
||||
|
||||
@@ -113,7 +113,7 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
if slug, err := c.GetDashboardSlug(); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d", setting.AppUrl, slug, c.Rule.PanelId)
|
||||
ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d", setting.AppUrl, slug, c.Rule.PanelId, c.Rule.OrgId)
|
||||
return ruleUrl, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,18 @@ type Notifier interface {
|
||||
GetIsDefault() bool
|
||||
}
|
||||
|
||||
type NotifierSlice []Notifier
|
||||
|
||||
func (notifiers NotifierSlice) ShouldUploadImage() bool {
|
||||
for _, notifier := range notifiers {
|
||||
if notifier.NeedsImage() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type ConditionResult struct {
|
||||
Firing bool
|
||||
NoDataFound bool
|
||||
|
||||
@@ -21,37 +21,25 @@ type NotifierPlugin struct {
|
||||
Factory NotifierFactory `json:"-"`
|
||||
}
|
||||
|
||||
type RootNotifier struct {
|
||||
type NotificationService interface {
|
||||
Send(context *EvalContext) error
|
||||
}
|
||||
|
||||
func NewNotificationService() NotificationService {
|
||||
return newNotificationService()
|
||||
}
|
||||
|
||||
type notificationService struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewRootNotifier() *RootNotifier {
|
||||
return &RootNotifier{
|
||||
func newNotificationService() *notificationService {
|
||||
return ¬ificationService{
|
||||
log: log.New("alerting.notifier"),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *RootNotifier) GetType() string {
|
||||
return "root"
|
||||
}
|
||||
|
||||
func (n *RootNotifier) NeedsImage() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *RootNotifier) PassesFilter(rule *Rule) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *RootNotifier) GetNotifierId() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (n *RootNotifier) GetIsDefault() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *RootNotifier) Notify(context *EvalContext) error {
|
||||
func (n *notificationService) Send(context *EvalContext) error {
|
||||
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -63,14 +51,16 @@ func (n *RootNotifier) Notify(context *EvalContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = n.uploadImage(context); err != nil {
|
||||
n.log.Error("Failed to upload alert panel image.", "error", err)
|
||||
if notifiers.ShouldUploadImage() {
|
||||
if err = n.uploadImage(context); err != nil {
|
||||
n.log.Error("Failed to upload alert panel image.", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return n.sendNotifications(context, notifiers)
|
||||
}
|
||||
|
||||
func (n *RootNotifier) sendNotifications(context *EvalContext, notifiers []Notifier) error {
|
||||
func (n *notificationService) sendNotifications(context *EvalContext, notifiers []Notifier) error {
|
||||
g, _ := errgroup.WithContext(context.Ctx)
|
||||
|
||||
for _, notifier := range notifiers {
|
||||
@@ -82,7 +72,7 @@ func (n *RootNotifier) sendNotifications(context *EvalContext, notifiers []Notif
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {
|
||||
func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
uploader, err := imguploader.NewImageUploader()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -116,7 +106,7 @@ func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) ([]Notifier, error) {
|
||||
func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
|
||||
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
|
||||
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
@@ -137,7 +127,7 @@ func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, contex
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
|
||||
func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
|
||||
notifierPlugin, found := notifierFactories[model.Type]
|
||||
if !found {
|
||||
return nil, errors.New("Unsupported notification type")
|
||||
|
||||
@@ -6,18 +6,22 @@ import (
|
||||
)
|
||||
|
||||
type NotifierBase struct {
|
||||
Name string
|
||||
Type string
|
||||
Id int64
|
||||
IsDeault bool
|
||||
Name string
|
||||
Type string
|
||||
Id int64
|
||||
IsDeault bool
|
||||
UploadImage bool
|
||||
}
|
||||
|
||||
func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase {
|
||||
uploadImage := model.Get("uploadImage").MustBool(true)
|
||||
|
||||
return NotifierBase{
|
||||
Id: id,
|
||||
Name: name,
|
||||
IsDeault: isDefault,
|
||||
Type: notifierType,
|
||||
Id: id,
|
||||
Name: name,
|
||||
IsDeault: isDefault,
|
||||
Type: notifierType,
|
||||
UploadImage: uploadImage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +34,7 @@ func (n *NotifierBase) GetType() string {
|
||||
}
|
||||
|
||||
func (n *NotifierBase) NeedsImage() bool {
|
||||
return true
|
||||
return n.UploadImage
|
||||
}
|
||||
|
||||
func (n *NotifierBase) GetNotifierId() int64 {
|
||||
|
||||
@@ -16,7 +16,7 @@ func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "email",
|
||||
Name: "Email",
|
||||
Description: "Sends notifications using Grafana server configured STMP settings",
|
||||
Description: "Sends notifications using Grafana server configured SMTP settings",
|
||||
Factory: NewEmailNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Email addresses</h3>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "hipchat",
|
||||
Name: "HipChat",
|
||||
Description: "Sends notifications uto a HipChat Room",
|
||||
Factory: NewHipChatNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">HipChat settings</h3>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Hip Chat Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="HipChat URL (ex https://grafana.hipchat.com)"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">API Key</span>
|
||||
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.apikey" placeholder="HipChat API Key"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Room ID</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.roomid"
|
||||
data-placement="right">
|
||||
</input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const (
|
||||
maxFieldCount int = 4
|
||||
)
|
||||
|
||||
func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
|
||||
url := model.Settings.Get("url").MustString()
|
||||
if strings.HasSuffix(url, "/") {
|
||||
url = url[:len(url)-1]
|
||||
}
|
||||
if url == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
|
||||
}
|
||||
|
||||
apikey := model.Settings.Get("apikey").MustString()
|
||||
roomId := model.Settings.Get("roomid").MustString()
|
||||
|
||||
return &HipChatNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
ApiKey: apikey,
|
||||
RoomId: roomId,
|
||||
log: log.New("alerting.notifier.hipchat"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type HipChatNotifier struct {
|
||||
NotifierBase
|
||||
Url string
|
||||
ApiKey string
|
||||
RoomId string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
this.log.Error("Failed get rule link", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "<br><a href=" + ruleUrl + ">Check Dasboard</a>"
|
||||
fields := make([]map[string]interface{}, 0)
|
||||
message += "<br>"
|
||||
for index, evt := range evalContext.EvalMatches {
|
||||
message += evt.Metric + " :: " + strconv.FormatFloat(evt.Value.Float64, 'f', -1, 64) + "<br>"
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"title": evt.Metric,
|
||||
"value": evt.Value,
|
||||
"short": true,
|
||||
})
|
||||
if index > maxFieldCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if evalContext.Error != nil {
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"title": "Error message",
|
||||
"value": evalContext.Error.Error(),
|
||||
"short": false,
|
||||
})
|
||||
}
|
||||
|
||||
if evalContext.Rule.State != models.AlertStateOK { //dont add message when going back to alert state ok.
|
||||
message += " " + evalContext.Rule.Message
|
||||
}
|
||||
//HipChat has a set list of colors
|
||||
var color string
|
||||
switch evalContext.Rule.State {
|
||||
case models.AlertStateOK:
|
||||
color = "green"
|
||||
case models.AlertStateNoData:
|
||||
color = "grey"
|
||||
case models.AlertStateAlerting:
|
||||
color = "red"
|
||||
}
|
||||
|
||||
// Add a card with link to the dashboard
|
||||
card := map[string]interface{}{
|
||||
"style": "link",
|
||||
"url": ruleUrl,
|
||||
"id": "1",
|
||||
"title": evalContext.GetNotificationTitle(),
|
||||
"description": evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text,
|
||||
"icon": map[string]interface{}{
|
||||
"url": "https://grafana.com/assets/img/fav32.png",
|
||||
},
|
||||
"date": evalContext.EndTime.Unix(),
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"message": message,
|
||||
"notify": "true",
|
||||
"message_format": "html",
|
||||
"color": color,
|
||||
"card": card,
|
||||
}
|
||||
|
||||
hipUrl := fmt.Sprintf("%s/v2/room/%s/notification?auth_token=%s", this.Url, this.RoomId, this.ApiKey)
|
||||
data, _ := json.Marshal(&body)
|
||||
cmd := &models.SendWebhookSync{Url: hipUrl, Body: string(data)}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send hipchat notification", "error", err, "webhook", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestHipChatNotifier(t *testing.T) {
|
||||
Convey("HipChat notifier tests", t, func() {
|
||||
|
||||
Convey("Parsing alert notification from settings", func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "hipchat",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewHipChatNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("from settings", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "http://google.com"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "hipchat",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewHipChatNotifier(model)
|
||||
hipchatNotifier := not.(*HipChatNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(hipchatNotifier.Name, ShouldEqual, "ops")
|
||||
So(hipchatNotifier.Type, ShouldEqual, "hipchat")
|
||||
So(hipchatNotifier.Url, ShouldEqual, "http://google.com")
|
||||
So(hipchatNotifier.ApiKey, ShouldEqual, "")
|
||||
So(hipchatNotifier.RoomId, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("from settings with Recipient and Mention", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "http://www.hipchat.com",
|
||||
"apikey": "1234",
|
||||
"roomid": "1234"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "hipchat",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewHipChatNotifier(model)
|
||||
hipchatNotifier := not.(*HipChatNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(hipchatNotifier.Name, ShouldEqual, "ops")
|
||||
So(hipchatNotifier.Type, ShouldEqual, "hipchat")
|
||||
So(hipchatNotifier.Url, ShouldEqual, "http://www.hipchat.com")
|
||||
So(hipchatNotifier.ApiKey, ShouldEqual, "1234")
|
||||
So(hipchatNotifier.RoomId, ShouldEqual, "1234")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func init() {
|
||||
label="Auto close incidents"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.settings.autoClose"
|
||||
tooltip="Automatically close alerts in OpseGenie once the alert goes back to ok.">
|
||||
tooltip="Automatically close alerts in OpsGenie once the alert goes back to ok.">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
const PUSHOVER_ENDPOINT = "https://api.pushover.net/1/messages.json"
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "pushover",
|
||||
Name: "Pushover",
|
||||
Description: "Sends HTTP POST request to the Pushover API",
|
||||
Factory: NewPushoverNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Pushover settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">API Token</span>
|
||||
<input type="text" class="gf-form-input" required placeholder="Application token" ng-model="ctrl.model.settings.apiToken"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">User key(s)</span>
|
||||
<input type="text" class="gf-form-input" required placeholder="comma-separated list" ng-model="ctrl.model.settings.userKey"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Device(s) (optional)</span>
|
||||
<input type="text" class="gf-form-input" placeholder="comma-separated list; leave empty to send to all devices" ng-model="ctrl.model.settings.device"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Priority</span>
|
||||
<select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.priority" ng-options="v as k for (k, v) in {
|
||||
Emergency: '2',
|
||||
High: '1',
|
||||
Normal: '0',
|
||||
Low: '-1',
|
||||
Lowest: '-2'
|
||||
}" ng-init="ctrl.model.settings.priority=ctrl.model.settings.priority||'0'"></select>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.model.settings.priority == '2'">
|
||||
<span class="gf-form-label width-10">Retry</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="minimum 30 seconds" ng-model="ctrl.model.settings.retry" ng-init="ctrl.model.settings.retry=ctrl.model.settings.retry||'60'></input>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.model.settings.priority == '2'">
|
||||
<span class="gf-form-label width-10">Expire</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="maximum 86400 seconds" ng-model="ctrl.model.settings.expire" ng-init="ctrl.model.settings.expire=ctrl.model.settings.expire||'3600'"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Sound</span>
|
||||
<select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.sound" ng-options="s for s in [
|
||||
'default',
|
||||
'pushover',
|
||||
'bike',
|
||||
'bugle',
|
||||
'cashregister',
|
||||
'classical',
|
||||
'cosmic',
|
||||
'falling',
|
||||
'gamelan',
|
||||
'incoming',
|
||||
'intermission',
|
||||
'magic',
|
||||
'mechanical',
|
||||
'pianobar',
|
||||
'siren',
|
||||
'spacealarm',
|
||||
'tugboat',
|
||||
'alien',
|
||||
'climb',
|
||||
'persistent',
|
||||
'echo',
|
||||
'updown',
|
||||
'none'
|
||||
]" ng-init="ctrl.model.settings.sound=ctrl.model.settings.sound||'default'"></select>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
userKey := model.Settings.Get("userKey").MustString()
|
||||
apiToken := model.Settings.Get("apiToken").MustString()
|
||||
device := model.Settings.Get("device").MustString()
|
||||
priority, _ := strconv.Atoi(model.Settings.Get("priority").MustString())
|
||||
retry, _ := strconv.Atoi(model.Settings.Get("retry").MustString())
|
||||
expire, _ := strconv.Atoi(model.Settings.Get("expire").MustString())
|
||||
sound := model.Settings.Get("sound").MustString()
|
||||
|
||||
if userKey == "" {
|
||||
return nil, alerting.ValidationError{Reason: "User key not given"}
|
||||
}
|
||||
if apiToken == "" {
|
||||
return nil, alerting.ValidationError{Reason: "API token not given"}
|
||||
}
|
||||
return &PushoverNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
UserKey: userKey,
|
||||
ApiToken: apiToken,
|
||||
Priority: priority,
|
||||
Retry: retry,
|
||||
Expire: expire,
|
||||
Device: device,
|
||||
Sound: sound,
|
||||
log: log.New("alerting.notifier.pushover"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type PushoverNotifier struct {
|
||||
NotifierBase
|
||||
UserKey string
|
||||
ApiToken string
|
||||
Priority int
|
||||
Retry int
|
||||
Expire int
|
||||
Device string
|
||||
Sound string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
metrics.M_Alerting_Notification_Sent_Pushover.Inc(1)
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
this.log.Error("Failed get rule link", "error", err)
|
||||
return err
|
||||
}
|
||||
message := evalContext.Rule.Message
|
||||
for idx, evt := range evalContext.EvalMatches {
|
||||
message += fmt.Sprintf("\n<b>%s</b>: %v", evt.Metric, evt.Value)
|
||||
if idx > 4 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if evalContext.Error != nil {
|
||||
message += fmt.Sprintf("\n<b>Error message</b> %s", evalContext.Error.Error())
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Add("user", this.UserKey)
|
||||
q.Add("token", this.ApiToken)
|
||||
q.Add("priority", strconv.Itoa(this.Priority))
|
||||
if this.Priority == 2 {
|
||||
q.Add("retry", strconv.Itoa(this.Retry))
|
||||
q.Add("expire", strconv.Itoa(this.Expire))
|
||||
}
|
||||
if this.Device != "" {
|
||||
q.Add("device", this.Device)
|
||||
}
|
||||
if this.Sound != "default" {
|
||||
q.Add("sound", this.Sound)
|
||||
}
|
||||
q.Add("title", evalContext.GetNotificationTitle())
|
||||
q.Add("url", ruleUrl)
|
||||
q.Add("url_title", "Show dashboard with alert")
|
||||
q.Add("message", message)
|
||||
q.Add("html", "1")
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: PUSHOVER_ENDPOINT,
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
Body: q.Encode(),
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send pushover notification", "error", err, "webhook", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPushoverNotifier(t *testing.T) {
|
||||
Convey("Pushover notifier tests", t, func() {
|
||||
|
||||
Convey("Parsing alert notification from settings", func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "Pushover",
|
||||
Type: "pushover",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewPushoverNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("from settings", func() {
|
||||
json := `
|
||||
{
|
||||
"apiToken": "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve",
|
||||
"userKey": "tzNZYf36y0ohWwXo4XoUrB61rz1A4o",
|
||||
"priority": "1",
|
||||
"sound": "pushover"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "Pushover",
|
||||
Type: "pushover",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewPushoverNotifier(model)
|
||||
pushoverNotifier := not.(*PushoverNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(pushoverNotifier.Name, ShouldEqual, "Pushover")
|
||||
So(pushoverNotifier.Type, ShouldEqual, "pushover")
|
||||
So(pushoverNotifier.ApiToken, ShouldEqual, "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve")
|
||||
So(pushoverNotifier.UserKey, ShouldEqual, "tzNZYf36y0ohWwXo4XoUrB61rz1A4o")
|
||||
So(pushoverNotifier.Priority, ShouldEqual, 1)
|
||||
So(pushoverNotifier.Sound, ShouldEqual, "pushover")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -116,6 +116,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
body := map[string]interface{}{
|
||||
"attachments": []map[string]interface{}{
|
||||
{
|
||||
"fallback": evalContext.GetNotificationTitle(),
|
||||
"color": evalContext.GetStateModel().Color,
|
||||
"title": evalContext.GetNotificationTitle(),
|
||||
"title_link": ruleUrl,
|
||||
@@ -123,7 +124,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
"fields": fields,
|
||||
"image_url": evalContext.ImagePublicUrl,
|
||||
"footer": "Grafana v" + setting.BuildVersion,
|
||||
"footer_icon": "http://grafana.org/assets/img/fav32.png",
|
||||
"footer_icon": "https://grafana.com/assets/img/fav32.png",
|
||||
"ts": time.Now().Unix(),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -93,6 +93,9 @@ func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
if err == nil {
|
||||
message = message + fmt.Sprintf("URL: %s\n", ruleUrl)
|
||||
}
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
|
||||
}
|
||||
bodyJSON.Set("text", message)
|
||||
|
||||
url := fmt.Sprintf(telegeramApiUrl, this.BotToken, "sendMessage")
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
var (
|
||||
threemaGwBaseURL = "https://msgapi.threema.ch/%s"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "threema",
|
||||
Name: "Threema Gateway",
|
||||
Description: "Sends notifications to Threema using the Threema Gateway",
|
||||
Factory: NewThreemaNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Threema Gateway settings</h3>
|
||||
<p>
|
||||
Notifications can be configured for any Threema Gateway ID of type
|
||||
"Basic". End-to-End IDs are not currently supported.
|
||||
</p>
|
||||
<p>
|
||||
The Threema Gateway ID can be set up at
|
||||
<a href="https://gateway.threema.ch/" target="_blank" rel="noopener noreferrer">https://gateway.threema.ch/</a>.
|
||||
</p>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Gateway ID</span>
|
||||
<input type="text" required maxlength="8" pattern="\*[0-9A-Z]{7}"
|
||||
class="gf-form-input max-width-14"
|
||||
ng-model="ctrl.model.settings.gateway_id"
|
||||
placeholder="*3MAGWID">
|
||||
</input>
|
||||
<info-popover mode="right-normal">
|
||||
Your 8 character Threema Gateway ID (starting with a *)
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Recipient ID</span>
|
||||
<input type="text" required maxlength="8" pattern="[0-9A-Z]{8}"
|
||||
class="gf-form-input max-width-14"
|
||||
ng-model="ctrl.model.settings.recipient_id"
|
||||
placeholder="YOUR3MID">
|
||||
</input>
|
||||
<info-popover mode="right-normal">
|
||||
The 8 character Threema ID that should receive the alerts
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">API Secret</span>
|
||||
<input type="text" required
|
||||
class="gf-form-input max-width-24"
|
||||
ng-model="ctrl.model.settings.api_secret">
|
||||
</input>
|
||||
<info-popover mode="right-normal">
|
||||
Your Threema Gateway API secret
|
||||
</info-popover>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type ThreemaNotifier struct {
|
||||
NotifierBase
|
||||
GatewayID string
|
||||
RecipientID string
|
||||
APISecret string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
if model.Settings == nil {
|
||||
return nil, alerting.ValidationError{Reason: "No Settings Supplied"}
|
||||
}
|
||||
|
||||
gatewayID := model.Settings.Get("gateway_id").MustString()
|
||||
recipientID := model.Settings.Get("recipient_id").MustString()
|
||||
apiSecret := model.Settings.Get("api_secret").MustString()
|
||||
|
||||
// Validation
|
||||
if gatewayID == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find Threema Gateway ID in settings"}
|
||||
}
|
||||
if !strings.HasPrefix(gatewayID, "*") {
|
||||
return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must start with a *"}
|
||||
}
|
||||
if len(gatewayID) != 8 {
|
||||
return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must be 8 characters long"}
|
||||
}
|
||||
if recipientID == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find Threema Recipient ID in settings"}
|
||||
}
|
||||
if len(recipientID) != 8 {
|
||||
return nil, alerting.ValidationError{Reason: "Invalid Threema Recipient ID: Must be 8 characters long"}
|
||||
}
|
||||
if apiSecret == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find Threema API secret in settings"}
|
||||
}
|
||||
|
||||
return &ThreemaNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
GatewayID: gatewayID,
|
||||
RecipientID: recipientID,
|
||||
APISecret: apiSecret,
|
||||
log: log.New("alerting.notifier.threema"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID)
|
||||
notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID)
|
||||
metrics.M_Alerting_Notification_Sent_Threema.Inc(1)
|
||||
|
||||
// Set up basic API request data
|
||||
data := url.Values{}
|
||||
data.Set("from", notifier.GatewayID)
|
||||
data.Set("to", notifier.RecipientID)
|
||||
data.Set("secret", notifier.APISecret)
|
||||
|
||||
// Determine emoji
|
||||
stateEmoji := ""
|
||||
switch evalContext.Rule.State {
|
||||
case m.AlertStateOK:
|
||||
stateEmoji = "\u2705 " // White Heavy Check Mark
|
||||
case m.AlertStateNoData:
|
||||
stateEmoji = "\u2753 " // Black Question Mark Ornament
|
||||
case m.AlertStateAlerting:
|
||||
stateEmoji = "\u26A0 " // Warning sign
|
||||
}
|
||||
|
||||
// Build message
|
||||
message := fmt.Sprintf("%s%s\n\n*State:* %s\n*Message:* %s\n",
|
||||
stateEmoji, evalContext.GetNotificationTitle(),
|
||||
evalContext.Rule.Name, evalContext.Rule.Message)
|
||||
ruleURL, err := evalContext.GetRuleUrl()
|
||||
if err == nil {
|
||||
message = message + fmt.Sprintf("*URL:* %s\n", ruleURL)
|
||||
}
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
message = message + fmt.Sprintf("*Image:* %s\n", evalContext.ImagePublicUrl)
|
||||
}
|
||||
data.Set("text", message)
|
||||
|
||||
// Prepare and send request
|
||||
url := fmt.Sprintf(threemaGwBaseURL, "send_simple")
|
||||
body := data.Encode()
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: url,
|
||||
Body: body,
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: headers,
|
||||
}
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
notifier.log.Error("Failed to send webhook", "error", err, "webhook", notifier.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestThreemaNotifier(t *testing.T) {
|
||||
Convey("Threema notifier tests", t, func() {
|
||||
|
||||
Convey("Parsing alert notification from settings", func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "threema_testing",
|
||||
Type: "threema",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewThreemaNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("valid settings should be parsed successfully", func() {
|
||||
json := `
|
||||
{
|
||||
"gateway_id": "*3MAGWID",
|
||||
"recipient_id": "ECHOECHO",
|
||||
"api_secret": "1234"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "threema_testing",
|
||||
Type: "threema",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewThreemaNotifier(model)
|
||||
So(err, ShouldBeNil)
|
||||
threemaNotifier := not.(*ThreemaNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(threemaNotifier.Name, ShouldEqual, "threema_testing")
|
||||
So(threemaNotifier.Type, ShouldEqual, "threema")
|
||||
So(threemaNotifier.GatewayID, ShouldEqual, "*3MAGWID")
|
||||
So(threemaNotifier.RecipientID, ShouldEqual, "ECHOECHO")
|
||||
So(threemaNotifier.APISecret, ShouldEqual, "1234")
|
||||
})
|
||||
|
||||
Convey("invalid Threema Gateway IDs should be rejected (prefix)", func() {
|
||||
json := `
|
||||
{
|
||||
"gateway_id": "ECHOECHO",
|
||||
"recipient_id": "ECHOECHO",
|
||||
"api_secret": "1234"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "threema_testing",
|
||||
Type: "threema",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewThreemaNotifier(model)
|
||||
So(not, ShouldBeNil)
|
||||
So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Gateway ID: Must start with a *")
|
||||
})
|
||||
|
||||
Convey("invalid Threema Gateway IDs should be rejected (length)", func() {
|
||||
json := `
|
||||
{
|
||||
"gateway_id": "*ECHOECHO",
|
||||
"recipient_id": "ECHOECHO",
|
||||
"api_secret": "1234"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "threema_testing",
|
||||
Type: "threema",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewThreemaNotifier(model)
|
||||
So(not, ShouldBeNil)
|
||||
So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Gateway ID: Must be 8 characters long")
|
||||
})
|
||||
|
||||
Convey("invalid Threema Recipient IDs should be rejected (length)", func() {
|
||||
json := `
|
||||
{
|
||||
"gateway_id": "*3MAGWID",
|
||||
"recipient_id": "ECHOECH",
|
||||
"api_secret": "1234"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "threema_testing",
|
||||
Type: "threema",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewThreemaNotifier(model)
|
||||
So(not, ShouldBeNil)
|
||||
So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Recipient ID: Must be 8 characters long")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -16,14 +16,14 @@ type ResultHandler interface {
|
||||
}
|
||||
|
||||
type DefaultResultHandler struct {
|
||||
notifier Notifier
|
||||
notifier NotificationService
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewResultHandler() *DefaultResultHandler {
|
||||
return &DefaultResultHandler{
|
||||
log: log.New("alerting.resultHandler"),
|
||||
notifier: NewRootNotifier(),
|
||||
notifier: NewNotificationService(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,12 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
handler.log.Error("Cannot change state on alert thats pause", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err == m.ErrRequiresNewState {
|
||||
handler.log.Info("Alert already updated")
|
||||
return nil
|
||||
}
|
||||
|
||||
handler.log.Error("Failed to save state", "error", err)
|
||||
}
|
||||
|
||||
@@ -85,7 +91,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
}
|
||||
|
||||
if evalContext.ShouldSendNotification() {
|
||||
handler.notifier.Notify(evalContext)
|
||||
handler.notifier.Send(evalContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ func init() {
|
||||
}
|
||||
|
||||
func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||
notifier := NewRootNotifier()
|
||||
notifier := newNotificationService()
|
||||
|
||||
model := &m.AlertNotification{
|
||||
Name: cmd.Name,
|
||||
@@ -38,10 +38,10 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return notifier.sendNotifications(createTestEvalContext(), []Notifier{notifiers})
|
||||
return notifier.sendNotifications(createTestEvalContext(cmd), []Notifier{notifiers})
|
||||
}
|
||||
|
||||
func createTestEvalContext() *EvalContext {
|
||||
func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext {
|
||||
testRule := &Rule{
|
||||
DashboardId: 1,
|
||||
PanelId: 1,
|
||||
@@ -50,8 +50,10 @@ func createTestEvalContext() *EvalContext {
|
||||
State: m.AlertStateAlerting,
|
||||
}
|
||||
|
||||
ctx := NewEvalContext(context.TODO(), testRule)
|
||||
ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
|
||||
ctx := NewEvalContext(context.Background(), testRule)
|
||||
if cmd.Settings.Get("uploadImage").MustBool(true) {
|
||||
ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
|
||||
}
|
||||
ctx.IsTestRun = true
|
||||
ctx.Firing = true
|
||||
ctx.Error = nil
|
||||
|
||||
@@ -49,7 +49,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error {
|
||||
func testAlertRule(rule *Rule) *EvalContext {
|
||||
handler := NewEvalHandler()
|
||||
|
||||
context := NewEvalContext(context.TODO(), rule)
|
||||
context := NewEvalContext(context.Background(), rule)
|
||||
context.IsTestRun = true
|
||||
|
||||
handler.Eval(context)
|
||||
|
||||
@@ -11,14 +11,14 @@ type Repository interface {
|
||||
type ItemQuery struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
From int64 `json:"from"`
|
||||
To int64 `json:"from"`
|
||||
To int64 `json:"to"`
|
||||
Type ItemType `json:"type"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
NewState []string `json:"newState"`
|
||||
|
||||
Limit int64 `json:"alertId"`
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
|
||||
type DeleteParams struct {
|
||||
@@ -48,7 +48,7 @@ type Item struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
CategoryId int64 `json:"panelId"`
|
||||
CategoryId int64 `json:"categoryId"`
|
||||
Type ItemType `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
|
||||
@@ -150,7 +150,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
|
||||
|
||||
return &Message{
|
||||
To: cmd.To,
|
||||
From: setting.Smtp.FromAddress,
|
||||
From: fmt.Sprintf("%s <%s>", setting.Smtp.FromName, setting.Smtp.FromAddress),
|
||||
Subject: subject,
|
||||
Body: buffer.String(),
|
||||
EmbededFiles: cmd.EmbededFiles,
|
||||
|
||||
@@ -48,7 +48,7 @@ func Init() error {
|
||||
}
|
||||
|
||||
if !util.IsEmail(setting.Smtp.FromAddress) {
|
||||
return errors.New("Invalid email address for smpt from_adress config")
|
||||
return errors.New("Invalid email address for SMTP from_address config")
|
||||
}
|
||||
|
||||
if setting.EmailCodeValidMinutes == 0 {
|
||||
|
||||
@@ -23,6 +23,7 @@ func TestNotifications(t *testing.T) {
|
||||
setting.Smtp.Enabled = true
|
||||
setting.Smtp.TemplatesPattern = "emails/*.html"
|
||||
setting.Smtp.FromAddress = "from@address.com"
|
||||
setting.Smtp.FromName = "Grafana Admin"
|
||||
|
||||
err := Init()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
@@ -18,12 +18,15 @@ func TestEmailIntegrationTest(t *testing.T) {
|
||||
setting.Smtp.Enabled = true
|
||||
setting.Smtp.TemplatesPattern = "emails/*.html"
|
||||
setting.Smtp.FromAddress = "from@address.com"
|
||||
setting.Smtp.FromName = "Grafana Admin"
|
||||
setting.BuildVersion = "4.0.0"
|
||||
|
||||
err := Init()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
addToMailQueue = func(msg *Message) {
|
||||
So(msg.From, ShouldEqual, "Grafana Admin <from@address.com>")
|
||||
So(msg.To[0], ShouldEqual, "asdf@asdf.com")
|
||||
ioutil.WriteFile("../../../tmp/test_email.html", []byte(msg.Body), 0777)
|
||||
}
|
||||
|
||||
@@ -40,6 +43,7 @@ func TestEmailIntegrationTest(t *testing.T) {
|
||||
"RuleUrl": "http://localhost:3000/dashboard/db/graphite-dashboard",
|
||||
"ImageLink": "http://localhost:3000/render/dashboard-solo/db/graphite-dashboard?panelId=1&from=1471008499616&to=1471012099617&width=1000&height=500",
|
||||
"AlertPageUrl": "http://localhost:3000/alerting",
|
||||
"EmbededImage": "test.png",
|
||||
"EvalMatches": []map[string]string{
|
||||
{
|
||||
"Metric": "desktop",
|
||||
@@ -51,7 +55,7 @@ func TestEmailIntegrationTest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
To: []string{"asdf@asdf.com "},
|
||||
To: []string{"asdf@asdf.com"},
|
||||
Template: "alert_notification.html",
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context/ctxhttp"
|
||||
|
||||
@@ -22,6 +24,18 @@ type Webhook struct {
|
||||
HttpHeader map[string]string
|
||||
}
|
||||
|
||||
var netTransport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
var netClient = &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
Transport: netTransport,
|
||||
}
|
||||
|
||||
var (
|
||||
webhookQueue chan *Webhook
|
||||
webhookLog log.Logger
|
||||
@@ -37,7 +51,7 @@ func processWebhookQueue() {
|
||||
for {
|
||||
select {
|
||||
case webhook := <-webhookQueue:
|
||||
err := sendWebRequestSync(context.TODO(), webhook)
|
||||
err := sendWebRequestSync(context.Background(), webhook)
|
||||
|
||||
if err != nil {
|
||||
webhookLog.Error("Failed to send webrequest ", "error", err)
|
||||
@@ -68,7 +82,7 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
|
||||
request.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := ctxhttp.Do(ctx, http.DefaultClient, request)
|
||||
resp, err := ctxhttp.Do(ctx, netClient, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -240,6 +240,10 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
|
||||
return m.ErrCannotChangeStateOnPausedAlert
|
||||
}
|
||||
|
||||
if alert.State == cmd.State {
|
||||
return m.ErrRequiresNewState
|
||||
}
|
||||
|
||||
alert.State = cmd.State
|
||||
alert.StateChanges += 1
|
||||
alert.NewStateDate = time.Now()
|
||||
|
||||
@@ -13,7 +13,8 @@ import (
|
||||
func init() {
|
||||
bus.AddHandler("sql", GetDataSources)
|
||||
bus.AddHandler("sql", AddDataSource)
|
||||
bus.AddHandler("sql", DeleteDataSource)
|
||||
bus.AddHandler("sql", DeleteDataSourceById)
|
||||
bus.AddHandler("sql", DeleteDataSourceByName)
|
||||
bus.AddHandler("sql", UpdateDataSource)
|
||||
bus.AddHandler("sql", GetDataSourceById)
|
||||
bus.AddHandler("sql", GetDataSourceByName)
|
||||
@@ -50,7 +51,7 @@ func GetDataSources(query *m.GetDataSourcesQuery) error {
|
||||
return sess.Find(&query.Result)
|
||||
}
|
||||
|
||||
func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error {
|
||||
func DeleteDataSourceById(cmd *m.DeleteDataSourceByIdCommand) error {
|
||||
return inTransaction(func(sess *xorm.Session) error {
|
||||
var rawSql = "DELETE FROM data_source WHERE id=? and org_id=?"
|
||||
_, err := sess.Exec(rawSql, cmd.Id, cmd.OrgId)
|
||||
@@ -58,6 +59,14 @@ func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteDataSourceByName(cmd *m.DeleteDataSourceByNameCommand) error {
|
||||
return inTransaction(func(sess *xorm.Session) error {
|
||||
var rawSql = "DELETE FROM data_source WHERE name=? and org_id=?"
|
||||
_, err := sess.Exec(rawSql, cmd.Name, cmd.OrgId)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func AddDataSource(cmd *m.AddDataSourceCommand) error {
|
||||
|
||||
return inTransaction(func(sess *xorm.Session) error {
|
||||
|
||||
@@ -79,8 +79,16 @@ func TestDataAccess(t *testing.T) {
|
||||
|
||||
ds := query.Result[0]
|
||||
|
||||
Convey("Can delete datasource", func() {
|
||||
err := DeleteDataSource(&m.DeleteDataSourceCommand{Id: ds.Id, OrgId: ds.OrgId})
|
||||
Convey("Can delete datasource by id", func() {
|
||||
err := DeleteDataSourceById(&m.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: ds.OrgId})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
GetDataSources(&query)
|
||||
So(len(query.Result), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Can delete datasource by name", func() {
|
||||
err := DeleteDataSourceByName(&m.DeleteDataSourceByNameCommand{Name: ds.Name, OrgId: ds.OrgId})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
GetDataSources(&query)
|
||||
@@ -88,7 +96,7 @@ func TestDataAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Can not delete datasource with wrong orgId", func() {
|
||||
err := DeleteDataSource(&m.DeleteDataSourceCommand{Id: ds.Id, OrgId: 123123})
|
||||
err := DeleteDataSourceById(&m.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: 123123})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
GetDataSources(&query)
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
glog "github.com/grafana/grafana/pkg/log"
|
||||
|
||||
"github.com/go-xorm/core"
|
||||
)
|
||||
|
||||
type XormLogger struct {
|
||||
grafanaLog glog.Logger
|
||||
level glog.Lvl
|
||||
showSQL bool
|
||||
}
|
||||
|
||||
func NewXormLogger(level glog.Lvl, grafanaLog glog.Logger) *XormLogger {
|
||||
return &XormLogger{
|
||||
grafanaLog: grafanaLog,
|
||||
level: level,
|
||||
showSQL: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Error implement core.ILogger
|
||||
func (s *XormLogger) Err(v ...interface{}) error {
|
||||
if s.level <= glog.LvlError {
|
||||
s.grafanaLog.Error(fmt.Sprint(v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Errorf implement core.ILogger
|
||||
func (s *XormLogger) Errf(format string, v ...interface{}) error {
|
||||
if s.level <= glog.LvlError {
|
||||
s.grafanaLog.Error(fmt.Sprintf(format, v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debug implement core.ILogger
|
||||
func (s *XormLogger) Debug(v ...interface{}) error {
|
||||
if s.level <= glog.LvlDebug {
|
||||
s.grafanaLog.Debug(fmt.Sprint(v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debugf implement core.ILogger
|
||||
func (s *XormLogger) Debugf(format string, v ...interface{}) error {
|
||||
if s.level <= glog.LvlDebug {
|
||||
s.grafanaLog.Debug(fmt.Sprintf(format, v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Info implement core.ILogger
|
||||
func (s *XormLogger) Info(v ...interface{}) error {
|
||||
if s.level <= glog.LvlInfo {
|
||||
s.grafanaLog.Info(fmt.Sprint(v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Infof implement core.ILogger
|
||||
func (s *XormLogger) Infof(format string, v ...interface{}) error {
|
||||
if s.level <= glog.LvlInfo {
|
||||
s.grafanaLog.Info(fmt.Sprintf(format, v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Warn implement core.ILogger
|
||||
func (s *XormLogger) Warning(v ...interface{}) error {
|
||||
if s.level <= glog.LvlWarn {
|
||||
s.grafanaLog.Warn(fmt.Sprint(v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Warnf implement core.ILogger
|
||||
func (s *XormLogger) Warningf(format string, v ...interface{}) error {
|
||||
if s.level <= glog.LvlWarn {
|
||||
s.grafanaLog.Warn(fmt.Sprintf(format, v...))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Level implement core.ILogger
|
||||
func (s *XormLogger) Level() core.LogLevel {
|
||||
switch s.level {
|
||||
case glog.LvlError:
|
||||
return core.LOG_ERR
|
||||
case glog.LvlWarn:
|
||||
return core.LOG_WARNING
|
||||
case glog.LvlInfo:
|
||||
return core.LOG_INFO
|
||||
case glog.LvlDebug:
|
||||
return core.LOG_DEBUG
|
||||
default:
|
||||
return core.LOG_ERR
|
||||
}
|
||||
}
|
||||
|
||||
// SetLevel implement core.ILogger
|
||||
func (s *XormLogger) SetLevel(l core.LogLevel) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShowSQL implement core.ILogger
|
||||
func (s *XormLogger) ShowSQL(show ...bool) {
|
||||
s.grafanaLog.Error("ShowSQL", "show", "show")
|
||||
if len(show) == 0 {
|
||||
s.showSQL = true
|
||||
return
|
||||
}
|
||||
s.showSQL = show[0]
|
||||
}
|
||||
|
||||
// IsShowSQL implement core.ILogger
|
||||
func (s *XormLogger) IsShowSQL() bool {
|
||||
return s.showSQL
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func addAlertMigrations(mg *Migrator) {
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "org_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "settings", Type: DB_Text, Nullable: false},
|
||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
||||
@@ -67,4 +67,19 @@ func addAlertMigrations(mg *Migrator) {
|
||||
}))
|
||||
mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
|
||||
|
||||
mg.AddMigration("Update alert table charset", NewTableCharsetMigration("alert", []*Column{
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "message", Type: DB_Text, Nullable: false},
|
||||
{Name: "state", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "settings", Type: DB_Text, Nullable: false},
|
||||
{Name: "severity", Type: DB_Text, Nullable: false},
|
||||
{Name: "execution_error", Type: DB_Text, Nullable: false},
|
||||
{Name: "eval_data", Type: DB_Text, Nullable: true},
|
||||
}))
|
||||
|
||||
mg.AddMigration("Update alert_notification table charset", NewTableCharsetMigration("alert_notification", []*Column{
|
||||
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "settings", Type: DB_Text, Nullable: false},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -44,4 +44,14 @@ func addAnnotationMig(mg *Migrator) {
|
||||
mg.AddMigration("add index annotation 2 v3", NewAddIndexMigration(table, table.Indices[2]))
|
||||
mg.AddMigration("add index annotation 3 v3", NewAddIndexMigration(table, table.Indices[3]))
|
||||
mg.AddMigration("add index annotation 4 v3", NewAddIndexMigration(table, table.Indices[4]))
|
||||
|
||||
mg.AddMigration("Update annotation table charset", NewTableCharsetMigration("annotation", []*Column{
|
||||
{Name: "type", Type: DB_NVarchar, Length: 25, Nullable: false},
|
||||
{Name: "title", Type: DB_Text, Nullable: false},
|
||||
{Name: "text", Type: DB_Text, Nullable: false},
|
||||
{Name: "metric", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "prev_state", Type: DB_NVarchar, Length: 25, Nullable: false},
|
||||
{Name: "new_state", Type: DB_NVarchar, Length: 25, Nullable: false},
|
||||
{Name: "data", Type: DB_Text, Nullable: false},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ func addApiKeyMigrations(mg *Migrator) {
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "account_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "key", Type: DB_Varchar, Length: 64, Nullable: false},
|
||||
{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
||||
@@ -41,8 +41,8 @@ func addApiKeyMigrations(mg *Migrator) {
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "org_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "key", Type: DB_Varchar, Length: 255, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "key", Type: DB_Varchar, Length: 190, Nullable: false},
|
||||
{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
||||
{Name: "updated", Type: DB_DateTime, Nullable: false},
|
||||
@@ -72,4 +72,10 @@ func addApiKeyMigrations(mg *Migrator) {
|
||||
}))
|
||||
|
||||
mg.AddMigration("Drop old table api_key_v1", NewDropTableMigration("api_key_v1"))
|
||||
|
||||
mg.AddMigration("Update api_key table charset", NewTableCharsetMigration("api_key", []*Column{
|
||||
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "key", Type: DB_Varchar, Length: 190, Nullable: false},
|
||||
{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ func addDashboardMigration(mg *Migrator) {
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "version", Type: DB_Int, Nullable: false},
|
||||
{Name: "slug", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "slug", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "data", Type: DB_Text, Nullable: false},
|
||||
{Name: "account_id", Type: DB_BigInt, Nullable: false},
|
||||
@@ -56,7 +56,7 @@ func addDashboardMigration(mg *Migrator) {
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "version", Type: DB_Int, Nullable: false},
|
||||
{Name: "slug", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "slug", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "data", Type: DB_Text, Nullable: false},
|
||||
{Name: "org_id", Type: DB_BigInt, Nullable: false},
|
||||
@@ -125,4 +125,15 @@ func addDashboardMigration(mg *Migrator) {
|
||||
mg.AddMigration("Add index for dashboard_id in dashboard_tag", NewAddIndexMigration(dashboardTagV1, &Index{
|
||||
Cols: []string{"dashboard_id"}, Type: IndexType,
|
||||
}))
|
||||
|
||||
mg.AddMigration("Update dashboard table charset", NewTableCharsetMigration("dashboard", []*Column{
|
||||
{Name: "slug", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "plugin_id", Type: DB_NVarchar, Nullable: true, Length: 255},
|
||||
{Name: "data", Type: DB_MediumText, Nullable: false},
|
||||
}))
|
||||
|
||||
mg.AddMigration("Update dashboard_tag table charset", NewTableCharsetMigration("dashboard_tag", []*Column{
|
||||
{Name: "term", Type: DB_NVarchar, Length: 50, Nullable: false},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "key", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "key", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "dashboard", Type: DB_Text, Nullable: false},
|
||||
{Name: "expires", Type: DB_DateTime, Nullable: false},
|
||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
||||
@@ -28,8 +28,8 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "key", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "delete_key", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "key", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "delete_key", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "org_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "user_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "external", Type: DB_Bool, Nullable: false},
|
||||
@@ -54,4 +54,12 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
|
||||
Sqlite("SELECT 0 WHERE 0;").
|
||||
Postgres("SELECT 0;").
|
||||
Mysql("ALTER TABLE dashboard_snapshot MODIFY dashboard MEDIUMTEXT;"))
|
||||
|
||||
mg.AddMigration("Update dashboard_snapshot table charset", NewTableCharsetMigration("dashboard_snapshot", []*Column{
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "key", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "delete_key", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "external_url", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "dashboard", Type: DB_MediumText, Nullable: false},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ func addDataSourceMigration(mg *Migrator) {
|
||||
{Name: "account_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "version", Type: DB_Int, Nullable: false},
|
||||
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "access", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "url", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
@@ -49,7 +49,7 @@ func addDataSourceMigration(mg *Migrator) {
|
||||
{Name: "org_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "version", Type: DB_Int, Nullable: false},
|
||||
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "access", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "url", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
@@ -106,4 +106,18 @@ func addDataSourceMigration(mg *Migrator) {
|
||||
mg.AddMigration("Add secure json data column", NewAddColumnMigration(tableV2, &Column{
|
||||
Name: "secure_json_data", Type: DB_Text, Nullable: true,
|
||||
}))
|
||||
|
||||
mg.AddMigration("Update data_source table charset", NewTableCharsetMigration(tableV2.Name, []*Column{
|
||||
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "access", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "url", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "user", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "database", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "basic_auth_user", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "basic_auth_password", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "json_data", Type: DB_Text, Nullable: true},
|
||||
{Name: "secure_json_data", Type: DB_Text, Nullable: true},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
// --- Migration Guide line ---
|
||||
// 1. Never change a migration that is committed and pushed to master
|
||||
// 2. Always add new migrations (to change or undo previous migrations)
|
||||
// 3. Some migraitons are not yet written (rename column, table, drop table, index etc)
|
||||
// 3. Some migrations are not yet written (rename column, table, drop table, index etc)
|
||||
|
||||
func AddMigrations(mg *Migrator) {
|
||||
addMigrationLogMigrations(mg)
|
||||
|
||||
@@ -8,7 +8,7 @@ func addOrgMigrations(mg *Migrator) {
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "version", Type: DB_Int, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "address1", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "address2", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "city", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
@@ -68,4 +68,19 @@ func addOrgMigrations(mg *Migrator) {
|
||||
|
||||
mg.AddMigration("Drop old table account", NewDropTableMigration("account"))
|
||||
mg.AddMigration("Drop old table account_user", NewDropTableMigration("account_user"))
|
||||
|
||||
mg.AddMigration("Update org table charset", NewTableCharsetMigration("org", []*Column{
|
||||
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "address1", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "address2", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "city", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "state", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "zip_code", Type: DB_NVarchar, Length: 50, Nullable: true},
|
||||
{Name: "country", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "billing_email", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
}))
|
||||
|
||||
mg.AddMigration("Update org_user table charset", NewTableCharsetMigration("org_user", []*Column{
|
||||
{Name: "role", Type: DB_NVarchar, Length: 20},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -32,4 +32,15 @@ func addPlaylistMigrations(mg *Migrator) {
|
||||
}
|
||||
|
||||
mg.AddMigration("create playlist item table v2", NewAddTableMigration(playlistItemV2))
|
||||
|
||||
mg.AddMigration("Update playlist table charset", NewTableCharsetMigration("playlist", []*Column{
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "interval", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
}))
|
||||
|
||||
mg.AddMigration("Update playlist_item table charset", NewTableCharsetMigration("playlist_item", []*Column{
|
||||
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "value", Type: DB_Text, Nullable: false},
|
||||
{Name: "title", Type: DB_Text, Nullable: false},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ func addAppSettingsMigration(mg *Migrator) {
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "org_id", Type: DB_BigInt, Nullable: true},
|
||||
{Name: "plugin_id", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "plugin_id", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "enabled", Type: DB_Bool, Nullable: false},
|
||||
{Name: "pinned", Type: DB_Bool, Nullable: false},
|
||||
{Name: "json_data", Type: DB_Text, Nullable: true},
|
||||
@@ -32,4 +32,10 @@ func addAppSettingsMigration(mg *Migrator) {
|
||||
Name: "plugin_version", Type: DB_NVarchar, Nullable: true, Length: 50,
|
||||
}))
|
||||
|
||||
mg.AddMigration("Update plugin_setting table charset", NewTableCharsetMigration("plugin_setting", []*Column{
|
||||
{Name: "plugin_id", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "json_data", Type: DB_Text, Nullable: true},
|
||||
{Name: "secure_json_data", Type: DB_Text, Nullable: true},
|
||||
{Name: "plugin_version", Type: DB_NVarchar, Nullable: true, Length: 50},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -29,4 +29,9 @@ func addPreferencesMigrations(mg *Migrator) {
|
||||
|
||||
// create table
|
||||
mg.AddMigration("create preferences table v3", NewAddTableMigration(preferencesV2))
|
||||
|
||||
mg.AddMigration("Update preferences table charset", NewTableCharsetMigration("preferences", []*Column{
|
||||
{Name: "timezone", Type: DB_NVarchar, Length: 50, Nullable: false},
|
||||
{Name: "theme", Type: DB_NVarchar, Length: 20, Nullable: false},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ func addQuotaMigration(mg *Migrator) {
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "org_id", Type: DB_BigInt, Nullable: true},
|
||||
{Name: "user_id", Type: DB_BigInt, Nullable: true},
|
||||
{Name: "target", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "target", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "limit", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
||||
{Name: "updated", Type: DB_DateTime, Nullable: false},
|
||||
@@ -25,4 +25,8 @@ func addQuotaMigration(mg *Migrator) {
|
||||
|
||||
//------- indexes ------------------
|
||||
addTableIndicesMigrations(mg, "v1", quotaV1)
|
||||
|
||||
mg.AddMigration("Update quota table charset", NewTableCharsetMigration("quota", []*Column{
|
||||
{Name: "target", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -35,4 +35,13 @@ func addTempUserMigrations(mg *Migrator) {
|
||||
// create table
|
||||
mg.AddMigration("create temp user table v1-7", NewAddTableMigration(tempUserV1))
|
||||
addTableIndicesMigrations(mg, "v1-7", tempUserV1)
|
||||
|
||||
mg.AddMigration("Update temp_user table charset", NewTableCharsetMigration("temp_user", []*Column{
|
||||
{Name: "email", Type: DB_NVarchar, Length: 255},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "role", Type: DB_NVarchar, Length: 20, Nullable: true},
|
||||
{Name: "code", Type: DB_NVarchar, Length: 255},
|
||||
{Name: "status", Type: DB_Varchar, Length: 20},
|
||||
{Name: "remote_addr", Type: DB_Varchar, Length: 255, Nullable: true},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ func addUserMigrations(mg *Migrator) {
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "version", Type: DB_Int, Nullable: false},
|
||||
{Name: "login", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "email", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "login", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "email", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "salt", Type: DB_NVarchar, Length: 50, Nullable: true},
|
||||
@@ -47,8 +47,8 @@ func addUserMigrations(mg *Migrator) {
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "version", Type: DB_Int, Nullable: false},
|
||||
{Name: "login", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "email", Type: DB_NVarchar, Length: 255, Nullable: false},
|
||||
{Name: "login", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "email", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "salt", Type: DB_NVarchar, Length: 50, Nullable: true},
|
||||
@@ -92,4 +92,15 @@ func addUserMigrations(mg *Migrator) {
|
||||
mg.AddMigration("Add column help_flags1 to user table", NewAddColumnMigration(userV2, &Column{
|
||||
Name: "help_flags1", Type: DB_BigInt, Nullable: false, Default: "0",
|
||||
}))
|
||||
|
||||
mg.AddMigration("Update user table charset", NewTableCharsetMigration("user", []*Column{
|
||||
{Name: "login", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "email", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "salt", Type: DB_NVarchar, Length: 50, Nullable: true},
|
||||
{Name: "rands", Type: DB_NVarchar, Length: 50, Nullable: true},
|
||||
{Name: "company", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
{Name: "theme", Type: DB_NVarchar, Length: 255, Nullable: true},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ type Dialect interface {
|
||||
|
||||
TableCheckSql(tableName string) (string, []interface{})
|
||||
RenameTable(oldName string, newName string) string
|
||||
UpdateTableSql(tableName string, columns []*Column) string
|
||||
}
|
||||
|
||||
func NewDialect(name string) Dialect {
|
||||
@@ -102,7 +103,7 @@ func (b *BaseDialect) CreateTableSql(table *Table) string {
|
||||
|
||||
sql = sql[:len(sql)-2] + ")"
|
||||
if b.dialect.SupportEngine() {
|
||||
sql += " ENGINE=InnoDB DEFAULT CHARSET UTF8 "
|
||||
sql += " ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci"
|
||||
}
|
||||
|
||||
sql += ";"
|
||||
@@ -160,3 +161,7 @@ func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
|
||||
name = index.XName(tableName)
|
||||
return fmt.Sprintf("DROP INDEX %v ON %s", quote(name), quote(tableName))
|
||||
}
|
||||
|
||||
func (db *BaseDialect) UpdateTableSql(tableName string, columns []*Column) string {
|
||||
return "-- NOT REQUIRED"
|
||||
}
|
||||
|
||||
@@ -200,3 +200,17 @@ func (m *CopyTableDataMigration) IfTableExists(tableName string) *CopyTableDataM
|
||||
func (m *CopyTableDataMigration) Sql(d Dialect) string {
|
||||
return d.CopyTableData(m.sourceTable, m.targetTable, m.sourceCols, m.targetCols)
|
||||
}
|
||||
|
||||
type TableCharsetMigration struct {
|
||||
MigrationBase
|
||||
tableName string
|
||||
columns []*Column
|
||||
}
|
||||
|
||||
func NewTableCharsetMigration(tableName string, columns []*Column) *TableCharsetMigration {
|
||||
return &TableCharsetMigration{tableName: tableName, columns: columns}
|
||||
}
|
||||
|
||||
func (m *TableCharsetMigration) Sql(d Dialect) string {
|
||||
return d.UpdateTableSql(m.tableName, m.columns)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package migrator
|
||||
|
||||
import "strconv"
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Mysql struct {
|
||||
BaseDialect
|
||||
@@ -76,6 +79,12 @@ func (db *Mysql) SqlType(c *Column) string {
|
||||
} else if hasLen1 {
|
||||
res += "(" + strconv.Itoa(c.Length) + ")"
|
||||
}
|
||||
|
||||
switch c.Type {
|
||||
case DB_Char, DB_Varchar, DB_NVarchar, DB_TinyText, DB_Text, DB_MediumText, DB_LongText:
|
||||
res += " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -84,3 +93,15 @@ func (db *Mysql) TableCheckSql(tableName string) (string, []interface{}) {
|
||||
sql := "SELECT `TABLE_NAME` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? and `TABLE_NAME`=?"
|
||||
return sql, args
|
||||
}
|
||||
|
||||
func (db *Mysql) UpdateTableSql(tableName string, columns []*Column) string {
|
||||
var statements = []string{}
|
||||
|
||||
statements = append(statements, "DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
|
||||
|
||||
for _, col := range columns {
|
||||
statements = append(statements, "MODIFY "+col.StringNoPk(db))
|
||||
}
|
||||
|
||||
return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package migrator
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Postgres struct {
|
||||
@@ -112,3 +113,13 @@ func (db *Postgres) DropIndexSql(tableName string, index *Index) string {
|
||||
idxName := index.XName(tableName)
|
||||
return fmt.Sprintf("DROP INDEX %v", quote(idxName))
|
||||
}
|
||||
|
||||
func (db *Postgres) UpdateTableSql(tableName string, columns []*Column) string {
|
||||
var statements = []string{}
|
||||
|
||||
for _, col := range columns {
|
||||
statements = append(statements, "ALTER "+db.QuoteStr()+col.Name+db.QuoteStr()+" TYPE "+db.SqlType(col))
|
||||
}
|
||||
|
||||
return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";"
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ func TestAccountDataAccess(t *testing.T) {
|
||||
err := SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result[0].Email, ShouldEqual, "ac1@test.com")
|
||||
So(query.Result[1].Email, ShouldEqual, "ac2@test.com")
|
||||
So(query.Result.Users[0].Email, ShouldEqual, "ac1@test.com")
|
||||
So(query.Result.Users[1].Email, ShouldEqual, "ac2@test.com")
|
||||
})
|
||||
|
||||
Convey("Given an added org user", func() {
|
||||
|
||||
@@ -29,6 +29,8 @@ type DatabaseConfig struct {
|
||||
ClientKeyPath string
|
||||
ClientCertPath string
|
||||
ServerCertName string
|
||||
MaxOpenConn int
|
||||
MaxIdleConn int
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -112,7 +114,7 @@ func getEngine() (*xorm.Engine, error) {
|
||||
protocol = "unix"
|
||||
}
|
||||
|
||||
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8",
|
||||
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8mb4",
|
||||
DbCfg.User, DbCfg.Pwd, protocol, DbCfg.Host, DbCfg.Name)
|
||||
|
||||
if DbCfg.SslMode == "true" || DbCfg.SslMode == "skip-verify" {
|
||||
@@ -150,7 +152,17 @@ func getEngine() (*xorm.Engine, error) {
|
||||
}
|
||||
|
||||
sqlog.Info("Initializing DB", "dbtype", DbCfg.Type)
|
||||
return xorm.NewEngine(DbCfg.Type, cnnstr)
|
||||
engine, err := xorm.NewEngine(DbCfg.Type, cnnstr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
engine.SetMaxOpenConns(DbCfg.MaxOpenConn)
|
||||
engine.SetMaxIdleConns(DbCfg.MaxIdleConn)
|
||||
// engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm")))
|
||||
// engine.ShowSQL = true
|
||||
// engine.ShowInfo = true
|
||||
}
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func LoadConfig() {
|
||||
@@ -177,6 +189,8 @@ func LoadConfig() {
|
||||
DbCfg.Host = sec.Key("host").String()
|
||||
DbCfg.Name = sec.Key("name").String()
|
||||
DbCfg.User = sec.Key("user").String()
|
||||
DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0)
|
||||
DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0)
|
||||
if len(DbCfg.Pwd) == 0 {
|
||||
DbCfg.Pwd = sec.Key("password").String()
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type TestDB struct {
|
||||
}
|
||||
|
||||
var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:?_loc=Local"}
|
||||
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?charset=utf8"}
|
||||
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?charset=utf8mb4"}
|
||||
var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
|
||||
|
||||
func CleanDB(x *xorm.Engine) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/events"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@@ -260,6 +262,20 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error {
|
||||
}
|
||||
|
||||
func SetUsingOrg(cmd *m.SetUsingOrgCommand) error {
|
||||
getOrgsForUserCmd := &m.GetUserOrgListQuery{UserId: cmd.UserId}
|
||||
GetUserOrgList(getOrgsForUserCmd)
|
||||
|
||||
valid := false
|
||||
for _, other := range getOrgsForUserCmd.Result {
|
||||
if other.OrgId == cmd.OrgId {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("user does not belong ot org")
|
||||
}
|
||||
|
||||
return inTransaction(func(sess *xorm.Session) error {
|
||||
user := m.User{}
|
||||
sess.Id(cmd.UserId).Get(&user)
|
||||
@@ -344,12 +360,30 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
|
||||
}
|
||||
|
||||
func SearchUsers(query *m.SearchUsersQuery) error {
|
||||
query.Result = make([]*m.UserSearchHitDTO, 0)
|
||||
query.Result = m.SearchUserQueryResult{
|
||||
Users: make([]*m.UserSearchHitDTO, 0),
|
||||
}
|
||||
queryWithWildcards := "%" + query.Query + "%"
|
||||
|
||||
sess := x.Table("user")
|
||||
sess.Where("email LIKE ?", query.Query+"%")
|
||||
sess.Limit(query.Limit, query.Limit*query.Page)
|
||||
if query.Query != "" {
|
||||
sess.Where("email LIKE ? OR name LIKE ? OR login like ?", queryWithWildcards, queryWithWildcards, queryWithWildcards)
|
||||
}
|
||||
offset := query.Limit * (query.Page - 1)
|
||||
sess.Limit(query.Limit, offset)
|
||||
sess.Cols("id", "email", "name", "login", "is_admin")
|
||||
err := sess.Find(&query.Result)
|
||||
if err := sess.Find(&query.Result.Users); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := m.User{}
|
||||
|
||||
countSess := x.Table("user")
|
||||
if query.Query != "" {
|
||||
countSess.Where("email LIKE ? OR name LIKE ? OR login like ?", queryWithWildcards, queryWithWildcards, queryWithWildcards)
|
||||
}
|
||||
count, err := countSess.Count(&user)
|
||||
query.Result.TotalCount = count
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestUserDataAccess(t *testing.T) {
|
||||
|
||||
Convey("Testing DB", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
var err error
|
||||
for i := 0; i < 5; i++ {
|
||||
err = CreateUser(&models.CreateUserCommand{
|
||||
Email: fmt.Sprint("user", i, "@test.com"),
|
||||
Name: fmt.Sprint("user", i),
|
||||
Login: fmt.Sprint("loginuser", i),
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
Convey("Can return the first page of users and a total count", func() {
|
||||
query := models.SearchUsersQuery{Query: "", Page: 1, Limit: 3}
|
||||
err = SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Users), ShouldEqual, 3)
|
||||
So(query.Result.TotalCount, ShouldEqual, 5)
|
||||
})
|
||||
|
||||
Convey("Can return the second page of users and a total count", func() {
|
||||
query := models.SearchUsersQuery{Query: "", Page: 2, Limit: 3}
|
||||
err = SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Users), ShouldEqual, 2)
|
||||
So(query.Result.TotalCount, ShouldEqual, 5)
|
||||
})
|
||||
|
||||
Convey("Can return list of users matching query on user name", func() {
|
||||
query := models.SearchUsersQuery{Query: "use", Page: 1, Limit: 3}
|
||||
err = SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Users), ShouldEqual, 3)
|
||||
So(query.Result.TotalCount, ShouldEqual, 5)
|
||||
|
||||
query = models.SearchUsersQuery{Query: "ser1", Page: 1, Limit: 3}
|
||||
err = SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Users), ShouldEqual, 1)
|
||||
So(query.Result.TotalCount, ShouldEqual, 1)
|
||||
|
||||
query = models.SearchUsersQuery{Query: "USER1", Page: 1, Limit: 3}
|
||||
err = SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Users), ShouldEqual, 1)
|
||||
So(query.Result.TotalCount, ShouldEqual, 1)
|
||||
|
||||
query = models.SearchUsersQuery{Query: "idontexist", Page: 1, Limit: 3}
|
||||
err = SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Users), ShouldEqual, 0)
|
||||
So(query.Result.TotalCount, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Can return list of users matching query on email", func() {
|
||||
query := models.SearchUsersQuery{Query: "ser1@test.com", Page: 1, Limit: 3}
|
||||
err = SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Users), ShouldEqual, 1)
|
||||
So(query.Result.TotalCount, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("Can return list of users matching query on login name", func() {
|
||||
query := models.SearchUsersQuery{Query: "loginuser1", Page: 1, Limit: 3}
|
||||
err = SearchUsers(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(query.Result.Users), ShouldEqual, 1)
|
||||
So(query.Result.TotalCount, ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
+3
-17
@@ -96,6 +96,7 @@ var (
|
||||
LoginHint string
|
||||
DefaultTheme string
|
||||
DisableLoginForm bool
|
||||
DisableSignoutMenu bool
|
||||
|
||||
// Http auth
|
||||
AdminUser string
|
||||
@@ -455,22 +456,6 @@ func validateStaticRootPath() error {
|
||||
return fmt.Errorf("Failed to detect generated css or javascript files in static root (%s), have you executed default grunt task?", StaticRootPath)
|
||||
}
|
||||
|
||||
// func readInstanceName() string {
|
||||
// hostname, _ := os.Hostname()
|
||||
// if hostname == "" {
|
||||
// hostname = "hostname_unknown"
|
||||
// }
|
||||
//
|
||||
// instanceName := Cfg.Section("").Key("instance_name").MustString("")
|
||||
// if instanceName = "" {
|
||||
// // set value as it might be used in other places
|
||||
// Cfg.Section("").Key("instance_name").SetValue(hostname)
|
||||
// instanceName = hostname
|
||||
// }
|
||||
//
|
||||
// return
|
||||
// }
|
||||
|
||||
func NewConfigContext(args *CommandLineArgs) error {
|
||||
setHomePath(args)
|
||||
loadConfiguration(args)
|
||||
@@ -544,6 +529,7 @@ func NewConfigContext(args *CommandLineArgs) error {
|
||||
// auth
|
||||
auth := Cfg.Section("auth")
|
||||
DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
|
||||
DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false)
|
||||
|
||||
// anonymous access
|
||||
AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)
|
||||
@@ -590,7 +576,7 @@ func NewConfigContext(args *CommandLineArgs) error {
|
||||
log.Warn("require_email_validation is enabled but smpt is disabled")
|
||||
}
|
||||
|
||||
GrafanaNetUrl = Cfg.Section("grafana_net").Key("url").MustString("https://grafana.net")
|
||||
GrafanaNetUrl = Cfg.Section("grafana_net").Key("url").MustString("https://grafana.com")
|
||||
|
||||
imageUploadingSection := Cfg.Section("external_image_storage")
|
||||
ImageUploadProvider = imageUploadingSection.Key("provider").MustString("internal")
|
||||
|
||||
@@ -8,6 +8,7 @@ type SmtpSettings struct {
|
||||
CertFile string
|
||||
KeyFile string
|
||||
FromAddress string
|
||||
FromName string
|
||||
SkipVerify bool
|
||||
|
||||
SendWelcomeEmailOnSignUp bool
|
||||
@@ -23,6 +24,7 @@ func readSmtpSettings() {
|
||||
Smtp.CertFile = sec.Key("cert_file").String()
|
||||
Smtp.KeyFile = sec.Key("key_file").String()
|
||||
Smtp.FromAddress = sec.Key("from_address").String()
|
||||
Smtp.FromName = sec.Key("from_name").String()
|
||||
Smtp.SkipVerify = sec.Key("skip_verify").MustBool(false)
|
||||
|
||||
emails := Cfg.Section("emails")
|
||||
|
||||
@@ -2,7 +2,6 @@ package social
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
@@ -21,11 +20,8 @@ type SocialGithub struct {
|
||||
}
|
||||
|
||||
var (
|
||||
ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations")
|
||||
ErrMissingTeamMembership = &Error{"User not a member of one of the required teams"}
|
||||
ErrMissingOrganizationMembership = &Error{"User not a member of one of the required organizations"}
|
||||
)
|
||||
|
||||
func (s *SocialGithub) Type() int {
|
||||
|
||||
@@ -29,6 +29,14 @@ type SocialConnector interface {
|
||||
Client(ctx context.Context, t *oauth2.Token) *http.Client
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.s
|
||||
}
|
||||
|
||||
var (
|
||||
SocialBaseUrl = "/login/"
|
||||
SocialMap = make(map[string]SocialConnector)
|
||||
|
||||
+3
-2
@@ -51,8 +51,9 @@ type QueryResult struct {
|
||||
}
|
||||
|
||||
type TimeSeries struct {
|
||||
Name string `json:"name"`
|
||||
Points TimeSeriesPoints `json:"points"`
|
||||
Name string `json:"name"`
|
||||
Points TimeSeriesPoints `json:"points"`
|
||||
Tags map[string]string `json:"tags"`
|
||||
}
|
||||
|
||||
type TimePoint [2]null.Float
|
||||
|
||||
@@ -86,7 +86,7 @@ func (e *apiClient) spawnWorker(ctx context.Context, id int, jobs chan QueryToSe
|
||||
return
|
||||
}
|
||||
|
||||
series, err := e.responseParser.Parse(resp, query.QueryRef)
|
||||
series, err := e.responseParser.Parse(resp, query)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
|
||||
@@ -40,6 +40,7 @@ func init() {
|
||||
|
||||
type QueryToSend struct {
|
||||
RawQuery string
|
||||
Metric Metric
|
||||
QueryRef *Query
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,13 @@ import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"fmt"
|
||||
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
@@ -18,6 +22,16 @@ func NewResponseParser() *ResponseParser {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
indexAliasPattern *regexp.Regexp
|
||||
wildcardAliasPattern *regexp.Regexp
|
||||
)
|
||||
|
||||
func init() {
|
||||
indexAliasPattern = regexp.MustCompile(`\$(\d)`)
|
||||
wildcardAliasPattern = regexp.MustCompile(`[*!]`)
|
||||
}
|
||||
|
||||
type MQEResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Name string `json:"name"`
|
||||
@@ -47,7 +61,7 @@ type ResponseParser struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (parser *ResponseParser) Parse(res *http.Response, queryRef *Query) ([]*tsdb.TimeSeries, error) {
|
||||
func (parser *ResponseParser) Parse(res *http.Response, queryRef QueryToSend) ([]*tsdb.TimeSeries, error) {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
if err != nil {
|
||||
@@ -73,22 +87,14 @@ func (parser *ResponseParser) Parse(res *http.Response, queryRef *Query) ([]*tsd
|
||||
var series []*tsdb.TimeSeries
|
||||
for _, body := range data.Body {
|
||||
for _, mqeSerie := range body.Series {
|
||||
namePrefix := ""
|
||||
|
||||
//append predefined tags to seriename
|
||||
for key, value := range mqeSerie.Tagset {
|
||||
if key == "cluster" && queryRef.AddClusterToAlias {
|
||||
namePrefix += value + " "
|
||||
}
|
||||
serie := &tsdb.TimeSeries{
|
||||
Tags: map[string]string{},
|
||||
Name: parser.formatLegend(body, mqeSerie, queryRef),
|
||||
}
|
||||
for key, value := range mqeSerie.Tagset {
|
||||
if key == "host" && queryRef.AddHostToAlias {
|
||||
namePrefix += value + " "
|
||||
}
|
||||
serie.Tags[key] = value
|
||||
}
|
||||
|
||||
serie := &tsdb.TimeSeries{Name: namePrefix + body.Name}
|
||||
|
||||
for i, value := range mqeSerie.Values {
|
||||
timestamp := body.TimeRange.Start + int64(i)*body.TimeRange.Resolution
|
||||
serie.Points = append(serie.Points, tsdb.NewTimePoint(value, float64(timestamp)))
|
||||
@@ -100,3 +106,72 @@ func (parser *ResponseParser) Parse(res *http.Response, queryRef *Query) ([]*tsd
|
||||
|
||||
return series, nil
|
||||
}
|
||||
|
||||
func (parser *ResponseParser) formatLegend(body MQEResponseSerie, mqeSerie MQESerie, queryToSend QueryToSend) string {
|
||||
namePrefix := ""
|
||||
|
||||
//append predefined tags to seriename
|
||||
for key, value := range mqeSerie.Tagset {
|
||||
if key == "cluster" && queryToSend.QueryRef.AddClusterToAlias {
|
||||
namePrefix += value + " "
|
||||
}
|
||||
}
|
||||
for key, value := range mqeSerie.Tagset {
|
||||
if key == "host" && queryToSend.QueryRef.AddHostToAlias {
|
||||
namePrefix += value + " "
|
||||
}
|
||||
}
|
||||
|
||||
return namePrefix + parser.formatName(body, queryToSend)
|
||||
}
|
||||
|
||||
func (parser *ResponseParser) formatName(body MQEResponseSerie, queryToSend QueryToSend) string {
|
||||
if indexAliasPattern.MatchString(queryToSend.Metric.Alias) {
|
||||
return parser.indexAlias(body, queryToSend)
|
||||
}
|
||||
|
||||
if wildcardAliasPattern.MatchString(queryToSend.Metric.Metric) && wildcardAliasPattern.MatchString(queryToSend.Metric.Alias) {
|
||||
return parser.wildcardAlias(body, queryToSend)
|
||||
}
|
||||
|
||||
return body.Name
|
||||
}
|
||||
|
||||
func (parser *ResponseParser) wildcardAlias(body MQEResponseSerie, queryToSend QueryToSend) string {
|
||||
regString := strings.Replace(queryToSend.Metric.Metric, `*`, `(.*)`, 1)
|
||||
reg, err := regexp.Compile(regString)
|
||||
if err != nil {
|
||||
return queryToSend.Metric.Alias
|
||||
}
|
||||
|
||||
matches := reg.FindAllStringSubmatch(queryToSend.RawQuery, -1)
|
||||
|
||||
if len(matches) == 0 || len(matches[0]) < 2 {
|
||||
return queryToSend.Metric.Alias
|
||||
}
|
||||
|
||||
return matches[0][1]
|
||||
}
|
||||
|
||||
func (parser *ResponseParser) indexAlias(body MQEResponseSerie, queryToSend QueryToSend) string {
|
||||
queryNameParts := strings.Split(queryToSend.Metric.Metric, `.`)
|
||||
|
||||
name := indexAliasPattern.ReplaceAllStringFunc(queryToSend.Metric.Alias, func(in string) string {
|
||||
positionName := strings.TrimSpace(strings.Replace(in, "$", "", 1))
|
||||
|
||||
pos, err := strconv.Atoi(positionName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for i, part := range queryNameParts {
|
||||
if i == pos-1 {
|
||||
return strings.TrimSpace(part)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
})
|
||||
|
||||
return strings.Replace(name, " ", ".", -1)
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@ import (
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
var (
|
||||
dummieJson string
|
||||
testJson string
|
||||
)
|
||||
|
||||
func TestMQEResponseParser(t *testing.T) {
|
||||
@@ -20,14 +21,17 @@ func TestMQEResponseParser(t *testing.T) {
|
||||
parser := NewResponseParser()
|
||||
|
||||
Convey("Can parse response", func() {
|
||||
queryRef := &Query{
|
||||
AddClusterToAlias: true,
|
||||
AddHostToAlias: true,
|
||||
queryRef := QueryToSend{
|
||||
QueryRef: &Query{
|
||||
AddClusterToAlias: true,
|
||||
AddHostToAlias: true,
|
||||
},
|
||||
Metric: Metric{Alias: ""},
|
||||
}
|
||||
|
||||
response := &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(strings.NewReader(dummieJson)),
|
||||
Body: ioutil.NopCloser(strings.NewReader(testJson)),
|
||||
}
|
||||
res, err := parser.Parse(response, queryRef)
|
||||
So(err, ShouldBeNil)
|
||||
@@ -39,12 +43,64 @@ func TestMQEResponseParser(t *testing.T) {
|
||||
So(res[0].Points[i][0].Float64, ShouldEqual, i+1)
|
||||
So(res[0].Points[i][1].Float64, ShouldEqual, startTime+(i*30000))
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
Convey("Can format legend", func() {
|
||||
mqeSerie := MQESerie{
|
||||
Tagset: map[string]string{
|
||||
"cluster": "demoapp",
|
||||
"host": "staples-lab-1",
|
||||
},
|
||||
Values: []null.Float{null.NewFloat(3, true)},
|
||||
}
|
||||
|
||||
Convey("with empty alias", func() {
|
||||
serie := MQEResponseSerie{Name: "os.disk.sda3.weighted_io_time"}
|
||||
queryRef := QueryToSend{
|
||||
QueryRef: &Query{
|
||||
AddClusterToAlias: true,
|
||||
AddHostToAlias: true,
|
||||
},
|
||||
Metric: Metric{Alias: ""},
|
||||
}
|
||||
legend := parser.formatLegend(serie, mqeSerie, queryRef)
|
||||
So(legend, ShouldEqual, "demoapp staples-lab-1 os.disk.sda3.weighted_io_time")
|
||||
})
|
||||
|
||||
Convey("with index alias (ex $2 $3)", func() {
|
||||
serie := MQEResponseSerie{Name: "os.disk.sda3.weighted_io_time"}
|
||||
queryRef := QueryToSend{
|
||||
QueryRef: &Query{
|
||||
AddClusterToAlias: true,
|
||||
AddHostToAlias: true,
|
||||
},
|
||||
Metric: Metric{Alias: "$2 $3", Metric: "os.disk.sda3.weighted_io_time"},
|
||||
}
|
||||
legend := parser.formatLegend(serie, mqeSerie, queryRef)
|
||||
So(legend, ShouldEqual, "demoapp staples-lab-1 disk.sda3")
|
||||
})
|
||||
|
||||
Convey("with wildcard alias", func() {
|
||||
serie := MQEResponseSerie{Name: "os.disk.sda3.weighted_io_time", Query: "os.disk.*"}
|
||||
|
||||
queryRef := QueryToSend{
|
||||
QueryRef: &Query{
|
||||
AddClusterToAlias: true,
|
||||
AddHostToAlias: true,
|
||||
},
|
||||
RawQuery: "os.disk.sda3.weighted_io_time",
|
||||
Metric: Metric{Alias: "*", Metric: "os.disk.*.weighted_io_time"},
|
||||
}
|
||||
legend := parser.formatLegend(serie, mqeSerie, queryRef)
|
||||
So(legend, ShouldEqual, "demoapp staples-lab-1 sda3")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func init() {
|
||||
dummieJson = `{
|
||||
testJson = `{
|
||||
"success": true,
|
||||
"name": "select",
|
||||
"body": [
|
||||
|
||||
@@ -53,6 +53,7 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) {
|
||||
queriesToSend = append(queriesToSend, QueryToSend{
|
||||
RawQuery: rawQuery,
|
||||
QueryRef: q,
|
||||
Metric: metric,
|
||||
})
|
||||
} else {
|
||||
m := strings.Replace(metric.Metric, "*", ".*", -1)
|
||||
@@ -70,6 +71,7 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) {
|
||||
queriesToSend = append(queriesToSend, QueryToSend{
|
||||
RawQuery: rawQuery,
|
||||
QueryRef: q,
|
||||
Metric: metric,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +190,10 @@ func (e *OpenTsdbExecutor) buildMetric(query *tsdb.Query) map[string]interface{}
|
||||
rateOptions["resetValue"] = resetValue.MustFloat64()
|
||||
}
|
||||
|
||||
if !counterMaxCheck && (!resetValueCheck || resetValue.MustFloat64() == 0) {
|
||||
rateOptions["dropcounter"] = true
|
||||
}
|
||||
|
||||
metric["rateOptions"] = rateOptions
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,18 @@ type PrometheusExecutor struct {
|
||||
Transport *http.Transport
|
||||
}
|
||||
|
||||
type basicAuthTransport struct {
|
||||
*http.Transport
|
||||
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func (bat basicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.SetBasicAuth(bat.username, bat.password)
|
||||
return bat.Transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func NewPrometheusExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) {
|
||||
transport, err := dsInfo.GetHttpTransport()
|
||||
if err != nil {
|
||||
@@ -51,6 +63,14 @@ func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) {
|
||||
Transport: e.Transport,
|
||||
}
|
||||
|
||||
if e.BasicAuth {
|
||||
cfg.Transport = basicAuthTransport{
|
||||
Transport: e.Transport,
|
||||
username: e.BasicAuthUser,
|
||||
password: e.BasicAuthPassword,
|
||||
}
|
||||
}
|
||||
|
||||
client, err := prometheus.New(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -157,6 +177,11 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb
|
||||
for _, v := range data {
|
||||
series := tsdb.TimeSeries{
|
||||
Name: formatLegend(v.Metric, query),
|
||||
Tags: map[string]string{},
|
||||
}
|
||||
|
||||
for k, v := range v.Metric {
|
||||
series.Tags[string(k)] = string(v)
|
||||
}
|
||||
|
||||
for _, k := range v.Values {
|
||||
|
||||
Reference in New Issue
Block a user