Просмотр исходного кода

Merge pull request #14229 from pbakulev/configurable-alert-notification

Configurable alert notification
Carl Bergquist 7 лет назад
Родитель
Сommit
c6f80ecec2
35 измененных файлов с 1467 добавлено и 116 удалено
  1. 25 0
      conf/provisioning/notifiers/sample.yaml
  2. 184 0
      docs/sources/administration/provisioning.md
  3. 2 0
      pkg/api/dtos/alerting.go
  4. 34 6
      pkg/models/alert_notifications.go
  5. 57 46
      pkg/services/alerting/extractor_test.go
  6. 1 1
      pkg/services/alerting/interfaces.go
  7. 6 6
      pkg/services/alerting/notifier.go
  8. 4 4
      pkg/services/alerting/notifiers/base.go
  9. 1 1
      pkg/services/alerting/notifiers/base_test.go
  10. 9 5
      pkg/services/alerting/rule.go
  11. 79 28
      pkg/services/alerting/rule_test.go
  12. 4 1
      pkg/services/alerting/testdata/dash-without-id.json
  13. 4 1
      pkg/services/alerting/testdata/influxdb-alert.json
  14. 180 0
      pkg/services/provisioning/notifiers/alert_notifications.go
  15. 163 0
      pkg/services/provisioning/notifiers/config_reader.go
  16. 313 0
      pkg/services/provisioning/notifiers/config_reader_test.go
  17. 9 0
      pkg/services/provisioning/notifiers/testdata/test-configs/broken-yaml/broken.yaml
  18. 6 0
      pkg/services/provisioning/notifiers/testdata/test-configs/broken-yaml/not.yaml.text
  19. 12 0
      pkg/services/provisioning/notifiers/testdata/test-configs/correct-properties-with-orgName/correct-properties-with-orgName.yaml
  20. 42 0
      pkg/services/provisioning/notifiers/testdata/test-configs/correct-properties/correct-properties.yaml
  21. 7 0
      pkg/services/provisioning/notifiers/testdata/test-configs/double-default/default-1.yml
  22. 7 0
      pkg/services/provisioning/notifiers/testdata/test-configs/double-default/default-2.yaml
  23. 0 0
      pkg/services/provisioning/notifiers/testdata/test-configs/empty/empty.yaml
  24. 4 0
      pkg/services/provisioning/notifiers/testdata/test-configs/empty_folder/.gitignore
  25. 10 0
      pkg/services/provisioning/notifiers/testdata/test-configs/incorrect-settings/incorrect-settings.yaml
  26. 35 0
      pkg/services/provisioning/notifiers/testdata/test-configs/no-required-fields/no-required-fields.yaml
  27. 12 0
      pkg/services/provisioning/notifiers/testdata/test-configs/two-notifications/two-notifications.yaml
  28. 4 0
      pkg/services/provisioning/notifiers/testdata/test-configs/unknown-notifier/notification.yaml
  29. 38 0
      pkg/services/provisioning/notifiers/types.go
  30. 6 0
      pkg/services/provisioning/provisioning.go
  31. 142 8
      pkg/services/sqlstore/alert_notification.go
  32. 30 3
      pkg/services/sqlstore/alert_notification_test.go
  33. 17 0
      pkg/services/sqlstore/migrations/alert_mig.go
  34. 19 5
      public/app/features/alerting/AlertTabCtrl.ts
  35. 1 1
      public/app/features/alerting/partials/alert_tab.html

+ 25 - 0
conf/provisioning/notifiers/sample.yaml

@@ -0,0 +1,25 @@
+# # config file version
+apiVersion: 1
+
+# notifiers:
+#   - name: default-slack-temp
+#     type: slack
+#     org_name: Main Org.
+#     is_default: true
+#     uid: notifier1
+#     settings:
+#       recipient: "XXX"
+#       token: "xoxb"
+#       uploadImage: true
+#       url: https://slack.com
+#   - name: default-email
+#     type: email
+#     org_id: 1
+#     uid: notifier2
+#     is_default: false  
+#     settings:
+#       addresses: example11111@example.com
+# delete_notifiers:
+#   - name: default-slack-temp
+#     org_name: Main Org.
+#     uid: notifier1

+ 184 - 0
docs/sources/administration/provisioning.md

@@ -231,3 +231,187 @@ By default Grafana will delete dashboards in the database if the file is removed
 > which leads to problems if you re-use settings that are supposed to be unique.
 > which leads to problems if you re-use settings that are supposed to be unique.
 > Be careful not to re-use the same `title` multiple times within a folder
 > Be careful not to re-use the same `title` multiple times within a folder
 > or `uid` within the same installation as this will cause weird behaviors.
 > or `uid` within the same installation as this will cause weird behaviors.
+
+## Alert Notification Channels
+
+Alert Notification Channels can be provisioned by adding one or more yaml config files in the [`provisioning/notifiers`](/installation/configuration/#provisioning) directory.
+
+Each config file can contain the following top-level fields:
+- `notifiers`, a list of alert notifications that will be added or updated during start up. If the notification channel already exists, Grafana will update it to match the configuration file.
+- `delete_notifiers`, a list of alert notifications to be deleted before before inserting/updating those in the `notifiers` list.
+
+Provisioning looks up alert notifications by uid, and will update any existing notification with the provided uid.
+
+By default, exporting a dashboard as JSON will use a sequential identifier to refer to alert notifications. The field `uid` can be optionally specified to specify a string identifier for the alert name.
+
+```json
+{
+  ...
+      "alert": {
+        ...,
+        "conditions": [...],
+        "frequency": "24h",
+        "noDataState": "ok",
+        "notifications": [
+           {"uid": "notifier1"},
+           {"uid": "notifier2"},
+        ]
+      }
+  ...
+}
+```
+
+### Example Alert Notification Channels Config File
+
+```yaml
+notifiers:
+  - name: notification-channel-1
+    type: slack
+    uid: notifier1
+    # either
+    org_id: 2
+    # or
+    org_name: Main Org.
+    is_default: true
+    # See `Supported Settings` section for settings supporter for each
+    # alert notification type.
+    settings:
+      recipient: "XXX"
+      token: "xoxb"
+      uploadImage: true
+      url: https://slack.com
+
+delete_notifiers:
+  - name: notification-channel-1
+    uid: notifier1
+    # either
+    org_id: 2
+    # or 
+    org_name: Main Org.
+  - name: notification-channel-2
+    # default org_id: 1
+```
+
+### Supported Settings
+
+The following sections detail the supported settings for each alert notification type.
+
+#### Alert notification `pushover`
+
+| Name |
+| ---- |
+| apiToken |
+| userKey |
+| device |
+| retry |
+| expire |
+
+#### Alert notification `slack`
+
+| Name |
+| ---- |
+| url |
+| recipient |
+| username |
+| iconEmoji |
+| iconUrl |
+| uploadImage |
+| mention |
+| token |
+
+#### Alert notification `victorops`
+
+| Name |
+| ---- |
+| url |
+
+#### Alert notification `kafka`
+
+| Name |
+| ---- |
+| kafkaRestProxy |
+| kafkaTopic |
+
+#### Alert notification `LINE`
+
+| Name |
+| ---- |
+| token |
+
+#### Alert notification `pagerduty`
+
+| Name |
+| ---- |
+| integrationKey |
+
+#### Alert notification `sensu`
+
+| Name |
+| ---- |
+| url |
+| source |
+| handler |
+| username |
+| password |
+
+#### Alert notification `prometheus-alertmanager`
+
+| Name |
+| ---- |
+| url |
+
+#### Alert notification `teams`
+
+| Name |
+| ---- |
+| url |
+
+#### Alert notification `dingding`
+
+| Name |
+| ---- |
+| url |
+
+#### Alert notification `email`
+
+| Name |
+| ---- |
+| addresses |
+
+#### Alert notification `hipchat`
+
+| Name |
+| ---- |
+| url |
+| apikey |
+| roomid |
+
+#### Alert notification `opsgenie`
+
+| Name |
+| ---- |
+| apiKey |
+| apiUrl |
+
+#### Alert notification `telegram`
+
+| Name |
+| ---- |
+| bottoken |
+| chatid |
+
+#### Alert notification `threema`
+
+| Name |
+| ---- |
+| gateway_id |
+| recipient_id |
+| api_secret |
+
+#### Alert notification `webhook`
+
+| Name |
+| ---- |
+| url |
+| username |
+| password |

+ 2 - 0
pkg/api/dtos/alerting.go

@@ -50,6 +50,7 @@ func formatShort(interval time.Duration) string {
 func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
 func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
 	return &AlertNotification{
 	return &AlertNotification{
 		Id:                    notification.Id,
 		Id:                    notification.Id,
+		Uid:                   notification.Uid,
 		Name:                  notification.Name,
 		Name:                  notification.Name,
 		Type:                  notification.Type,
 		Type:                  notification.Type,
 		IsDefault:             notification.IsDefault,
 		IsDefault:             notification.IsDefault,
@@ -64,6 +65,7 @@ func NewAlertNotification(notification *models.AlertNotification) *AlertNotifica
 
 
 type AlertNotification struct {
 type AlertNotification struct {
 	Id                    int64            `json:"id"`
 	Id                    int64            `json:"id"`
+	Uid                   string           `json:"uid"`
 	Name                  string           `json:"name"`
 	Name                  string           `json:"name"`
 	Type                  string           `json:"type"`
 	Type                  string           `json:"type"`
 	IsDefault             bool             `json:"isDefault"`
 	IsDefault             bool             `json:"isDefault"`

+ 34 - 6
pkg/models/alert_notifications.go

@@ -8,10 +8,11 @@ import (
 )
 )
 
 
 var (
 var (
-	ErrNotificationFrequencyNotFound         = errors.New("Notification frequency not specified")
-	ErrAlertNotificationStateNotFound        = errors.New("alert notification state not found")
-	ErrAlertNotificationStateVersionConflict = errors.New("alert notification state update version conflict")
-	ErrAlertNotificationStateAlreadyExist    = errors.New("alert notification state already exists.")
+	ErrNotificationFrequencyNotFound            = errors.New("Notification frequency not specified")
+	ErrAlertNotificationStateNotFound           = errors.New("alert notification state not found")
+	ErrAlertNotificationStateVersionConflict    = errors.New("alert notification state update version conflict")
+	ErrAlertNotificationStateAlreadyExist       = errors.New("alert notification state already exists.")
+	ErrAlertNotificationFailedGenerateUniqueUid = errors.New("Failed to generate unique alert notification uid")
 )
 )
 
 
 type AlertNotificationStateType string
 type AlertNotificationStateType string
@@ -24,6 +25,7 @@ var (
 
 
 type AlertNotification struct {
 type AlertNotification struct {
 	Id                    int64            `json:"id"`
 	Id                    int64            `json:"id"`
+	Uid                   string           `json:"-"`
 	OrgId                 int64            `json:"-"`
 	OrgId                 int64            `json:"-"`
 	Name                  string           `json:"name"`
 	Name                  string           `json:"name"`
 	Type                  string           `json:"type"`
 	Type                  string           `json:"type"`
@@ -37,6 +39,7 @@ type AlertNotification struct {
 }
 }
 
 
 type CreateAlertNotificationCommand struct {
 type CreateAlertNotificationCommand struct {
+	Uid                   string           `json:"-"`
 	Name                  string           `json:"name"  binding:"Required"`
 	Name                  string           `json:"name"  binding:"Required"`
 	Type                  string           `json:"type"  binding:"Required"`
 	Type                  string           `json:"type"  binding:"Required"`
 	SendReminder          bool             `json:"sendReminder"`
 	SendReminder          bool             `json:"sendReminder"`
@@ -63,10 +66,28 @@ type UpdateAlertNotificationCommand struct {
 	Result *AlertNotification
 	Result *AlertNotification
 }
 }
 
 
+type UpdateAlertNotificationWithUidCommand struct {
+	Uid                   string
+	Name                  string
+	Type                  string
+	SendReminder          bool
+	DisableResolveMessage bool
+	Frequency             string
+	IsDefault             bool
+	Settings              *simplejson.Json
+
+	OrgId  int64
+	Result *AlertNotification
+}
+
 type DeleteAlertNotificationCommand struct {
 type DeleteAlertNotificationCommand struct {
 	Id    int64
 	Id    int64
 	OrgId int64
 	OrgId int64
 }
 }
+type DeleteAlertNotificationWithUidCommand struct {
+	Uid   string
+	OrgId int64
+}
 
 
 type GetAlertNotificationsQuery struct {
 type GetAlertNotificationsQuery struct {
 	Name  string
 	Name  string
@@ -76,8 +97,15 @@ type GetAlertNotificationsQuery struct {
 	Result *AlertNotification
 	Result *AlertNotification
 }
 }
 
 
-type GetAlertNotificationsToSendQuery struct {
-	Ids   []int64
+type GetAlertNotificationsWithUidQuery struct {
+	Uid   string
+	OrgId int64
+
+	Result *AlertNotification
+}
+
+type GetAlertNotificationsWithUidToSendQuery struct {
+	Uids  []string
 	OrgId int64
 	OrgId int64
 
 
 	Result []*AlertNotification
 	Result []*AlertNotification

+ 57 - 46
pkg/services/alerting/extractor_test.go

@@ -8,6 +8,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/sqlstore"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
@@ -197,74 +198,84 @@ func TestAlertRuleExtraction(t *testing.T) {
 			})
 			})
 		})
 		})
 
 
-		Convey("Parse and validate dashboard containing influxdb alert", func() {
-			json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
+		Convey("Alert notifications are in DB", func() {
+			sqlstore.InitTestDB(t)
+			firstNotification := m.CreateAlertNotificationCommand{Uid: "notifier1", OrgId: 1, Name: "1"}
+			err = sqlstore.CreateAlertNotificationCommand(&firstNotification)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
-
-			dashJson, err := simplejson.NewJson(json)
+			secondNotification := m.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"}
+			err = sqlstore.CreateAlertNotificationCommand(&secondNotification)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
-			dash := m.NewDashboardFromJson(dashJson)
-			extractor := NewDashAlertExtractor(dash, 1, nil)
 
 
-			alerts, err := extractor.GetAlerts()
+			Convey("Parse and validate dashboard containing influxdb alert", func() {
+				json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
+				So(err, ShouldBeNil)
 
 
-			Convey("Get rules without error", func() {
+				dashJson, err := simplejson.NewJson(json)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
-			})
+				dash := m.NewDashboardFromJson(dashJson)
+				extractor := NewDashAlertExtractor(dash, 1, nil)
 
 
-			Convey("should be able to read interval", func() {
-				So(len(alerts), ShouldEqual, 1)
+				alerts, err := extractor.GetAlerts()
 
 
-				for _, alert := range alerts {
-					So(alert.DashboardId, ShouldEqual, 4)
+				Convey("Get rules without error", func() {
+					So(err, ShouldBeNil)
+				})
 
 
-					conditions := alert.Settings.Get("conditions").MustArray()
-					cond := simplejson.NewFromAny(conditions[0])
+				Convey("should be able to read interval", func() {
+					So(len(alerts), ShouldEqual, 1)
 
 
-					So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s")
-				}
+					for _, alert := range alerts {
+						So(alert.DashboardId, ShouldEqual, 4)
+
+						conditions := alert.Settings.Get("conditions").MustArray()
+						cond := simplejson.NewFromAny(conditions[0])
+
+						So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s")
+					}
+				})
 			})
 			})
-		})
 
 
-		Convey("Should be able to extract collapsed panels", func() {
-			json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
-			So(err, ShouldBeNil)
+			Convey("Should be able to extract collapsed panels", func() {
+				json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
+				So(err, ShouldBeNil)
 
 
-			dashJson, err := simplejson.NewJson(json)
-			So(err, ShouldBeNil)
+				dashJson, err := simplejson.NewJson(json)
+				So(err, ShouldBeNil)
 
 
-			dash := m.NewDashboardFromJson(dashJson)
-			extractor := NewDashAlertExtractor(dash, 1, nil)
+				dash := m.NewDashboardFromJson(dashJson)
+				extractor := NewDashAlertExtractor(dash, 1, nil)
 
 
-			alerts, err := extractor.GetAlerts()
+				alerts, err := extractor.GetAlerts()
 
 
-			Convey("Get rules without error", func() {
-				So(err, ShouldBeNil)
-			})
+				Convey("Get rules without error", func() {
+					So(err, ShouldBeNil)
+				})
 
 
-			Convey("should be able to extract collapsed alerts", func() {
-				So(len(alerts), ShouldEqual, 4)
+				Convey("should be able to extract collapsed alerts", func() {
+					So(len(alerts), ShouldEqual, 4)
+				})
 			})
 			})
-		})
 
 
-		Convey("Parse and validate dashboard without id and containing an alert", func() {
-			json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
-			So(err, ShouldBeNil)
+			Convey("Parse and validate dashboard without id and containing an alert", func() {
+				json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
+				So(err, ShouldBeNil)
 
 
-			dashJSON, err := simplejson.NewJson(json)
-			So(err, ShouldBeNil)
-			dash := m.NewDashboardFromJson(dashJSON)
-			extractor := NewDashAlertExtractor(dash, 1, nil)
+				dashJSON, err := simplejson.NewJson(json)
+				So(err, ShouldBeNil)
+				dash := m.NewDashboardFromJson(dashJSON)
+				extractor := NewDashAlertExtractor(dash, 1, nil)
 
 
-			err = extractor.ValidateAlerts()
+				err = extractor.ValidateAlerts()
 
 
-			Convey("Should validate without error", func() {
-				So(err, ShouldBeNil)
-			})
+				Convey("Should validate without error", func() {
+					So(err, ShouldBeNil)
+				})
 
 
-			Convey("Should fail on save", func() {
-				_, err := extractor.GetAlerts()
-				So(err.Error(), ShouldEqual, "Alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1")
+				Convey("Should fail on save", func() {
+					_, err := extractor.GetAlerts()
+					So(err.Error(), ShouldEqual, "Alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1")
+				})
 			})
 			})
 		})
 		})
 	})
 	})

+ 1 - 1
pkg/services/alerting/interfaces.go

@@ -24,7 +24,7 @@ type Notifier interface {
 	// ShouldNotify checks this evaluation should send an alert notification
 	// ShouldNotify checks this evaluation should send an alert notification
 	ShouldNotify(ctx context.Context, evalContext *EvalContext, notificationState *models.AlertNotificationState) bool
 	ShouldNotify(ctx context.Context, evalContext *EvalContext, notificationState *models.AlertNotificationState) bool
 
 
-	GetNotifierId() int64
+	GetNotifierUid() string
 	GetIsDefault() bool
 	GetIsDefault() bool
 	GetSendReminder() bool
 	GetSendReminder() bool
 	GetDisableResolveMessage() bool
 	GetDisableResolveMessage() bool

+ 6 - 6
pkg/services/alerting/notifier.go

@@ -60,13 +60,13 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error {
 func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, notifierState *notifierState) error {
 func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, notifierState *notifierState) error {
 	notifier := notifierState.notifier
 	notifier := notifierState.notifier
 
 
-	n.log.Debug("Sending notification", "type", notifier.GetType(), "id", notifier.GetNotifierId(), "isDefault", notifier.GetIsDefault())
+	n.log.Debug("Sending notification", "type", notifier.GetType(), "uid", notifier.GetNotifierUid(), "isDefault", notifier.GetIsDefault())
 	metrics.M_Alerting_Notification_Sent.WithLabelValues(notifier.GetType()).Inc()
 	metrics.M_Alerting_Notification_Sent.WithLabelValues(notifier.GetType()).Inc()
 
 
 	err := notifier.Notify(evalContext)
 	err := notifier.Notify(evalContext)
 
 
 	if err != nil {
 	if err != nil {
-		n.log.Error("failed to send notification", "id", notifier.GetNotifierId(), "error", err)
+		n.log.Error("failed to send notification", "uid", notifier.GetNotifierUid(), "error", err)
 	}
 	}
 
 
 	if evalContext.IsTestRun {
 	if evalContext.IsTestRun {
@@ -110,7 +110,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi
 	for _, notifierState := range notifierStates {
 	for _, notifierState := range notifierStates {
 		err := n.sendNotification(evalContext, notifierState)
 		err := n.sendNotification(evalContext, notifierState)
 		if err != nil {
 		if err != nil {
-			n.log.Error("failed to send notification", "id", notifierState.notifier.GetNotifierId(), "error", err)
+			n.log.Error("failed to send notification", "uid", notifierState.notifier.GetNotifierUid(), "error", err)
 		}
 		}
 	}
 	}
 
 
@@ -157,8 +157,8 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
 	return nil
 	return nil
 }
 }
 
 
-func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (notifierStateSlice, error) {
-	query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
+func (n *notificationService) getNeededNotifiers(orgId int64, notificationUids []string, evalContext *EvalContext) (notifierStateSlice, error) {
+	query := &m.GetAlertNotificationsWithUidToSendQuery{OrgId: orgId, Uids: notificationUids}
 
 
 	if err := bus.Dispatch(query); err != nil {
 	if err := bus.Dispatch(query); err != nil {
 		return nil, err
 		return nil, err
@@ -168,7 +168,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []
 	for _, notification := range query.Result {
 	for _, notification := range query.Result {
 		not, err := InitNotifier(notification)
 		not, err := InitNotifier(notification)
 		if err != nil {
 		if err != nil {
-			n.log.Error("Could not create notifier", "notifier", notification.Id, "error", err)
+			n.log.Error("Could not create notifier", "notifier", notification.Uid, "error", err)
 			continue
 			continue
 		}
 		}
 
 

+ 4 - 4
pkg/services/alerting/notifiers/base.go

@@ -16,7 +16,7 @@ const (
 type NotifierBase struct {
 type NotifierBase struct {
 	Name                  string
 	Name                  string
 	Type                  string
 	Type                  string
-	Id                    int64
+	Uid                   string
 	IsDeault              bool
 	IsDeault              bool
 	UploadImage           bool
 	UploadImage           bool
 	SendReminder          bool
 	SendReminder          bool
@@ -34,7 +34,7 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase {
 	}
 	}
 
 
 	return NotifierBase{
 	return NotifierBase{
-		Id:                    model.Id,
+		Uid:                   model.Uid,
 		Name:                  model.Name,
 		Name:                  model.Name,
 		IsDeault:              model.IsDefault,
 		IsDeault:              model.IsDefault,
 		Type:                  model.Type,
 		Type:                  model.Type,
@@ -110,8 +110,8 @@ func (n *NotifierBase) NeedsImage() bool {
 	return n.UploadImage
 	return n.UploadImage
 }
 }
 
 
-func (n *NotifierBase) GetNotifierId() int64 {
-	return n.Id
+func (n *NotifierBase) GetNotifierUid() string {
+	return n.Uid
 }
 }
 
 
 func (n *NotifierBase) GetIsDefault() bool {
 func (n *NotifierBase) GetIsDefault() bool {

+ 1 - 1
pkg/services/alerting/notifiers/base_test.go

@@ -173,7 +173,7 @@ func TestBaseNotifier(t *testing.T) {
 		bJson := simplejson.New()
 		bJson := simplejson.New()
 
 
 		model := &m.AlertNotification{
 		model := &m.AlertNotification{
-			Id:       1,
+			Uid:      "1",
 			Name:     "name",
 			Name:     "name",
 			Type:     "email",
 			Type:     "email",
 			Settings: bJson,
 			Settings: bJson,

+ 9 - 5
pkg/services/alerting/rule.go

@@ -30,7 +30,7 @@ type Rule struct {
 	ExecutionErrorState m.ExecutionErrorOption
 	ExecutionErrorState m.ExecutionErrorOption
 	State               m.AlertStateType
 	State               m.AlertStateType
 	Conditions          []Condition
 	Conditions          []Condition
-	Notifications       []int64
+	Notifications       []string
 
 
 	StateChanges int64
 	StateChanges int64
 }
 }
@@ -126,11 +126,15 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
 
 
 	for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
 	for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
 		jsonModel := simplejson.NewFromAny(v)
 		jsonModel := simplejson.NewFromAny(v)
-		id, err := jsonModel.Get("id").Int64()
-		if err != nil {
-			return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
+		if id, err := jsonModel.Get("id").Int64(); err == nil {
+			model.Notifications = append(model.Notifications, fmt.Sprintf("%09d", id))
+		} else {
+			if uid, err := jsonModel.Get("uid").String(); err != nil {
+				return nil, ValidationError{Reason: "Neither id nor uid is specified, " + err.Error(), DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
+			} else {
+				model.Notifications = append(model.Notifications, uid)
+			}
 		}
 		}
-		model.Notifications = append(model.Notifications, id)
 	}
 	}
 
 
 	for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {
 	for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {

+ 79 - 28
pkg/services/alerting/rule_test.go

@@ -5,6 +5,7 @@ import (
 
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/sqlstore"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
@@ -45,6 +46,7 @@ func TestAlertRuleFrequencyParsing(t *testing.T) {
 }
 }
 
 
 func TestAlertRuleModel(t *testing.T) {
 func TestAlertRuleModel(t *testing.T) {
+	sqlstore.InitTestDB(t)
 	Convey("Testing alert rule", t, func() {
 	Convey("Testing alert rule", t, func() {
 
 
 		RegisterCondition("test", func(model *simplejson.Json, index int) (Condition, error) {
 		RegisterCondition("test", func(model *simplejson.Json, index int) (Condition, error) {
@@ -57,26 +59,71 @@ func TestAlertRuleModel(t *testing.T) {
 		})
 		})
 
 
 		Convey("can construct alert rule model", func() {
 		Convey("can construct alert rule model", func() {
+			firstNotification := m.CreateAlertNotificationCommand{OrgId: 1, Name: "1"}
+			err := sqlstore.CreateAlertNotificationCommand(&firstNotification)
+			So(err, ShouldBeNil)
+			secondNotification := m.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"}
+			err = sqlstore.CreateAlertNotificationCommand(&secondNotification)
+			So(err, ShouldBeNil)
+
+			Convey("with notification id and uid", func() {
+				json := `
+				{
+					"name": "name2",
+					"description": "desc2",
+					"handler": 0,
+					"noDataMode": "critical",
+					"enabled": true,
+					"frequency": "60s",
+					"conditions": [
+						{
+							"type": "test",
+							"prop": 123
+						}
+					],
+					"notifications": [
+						{"id": 1},
+						{"uid": "notifier2"}
+					]
+				}
+				`
+
+				alertJSON, jsonErr := simplejson.NewJson([]byte(json))
+				So(jsonErr, ShouldBeNil)
+
+				alert := &m.Alert{
+					Id:          1,
+					OrgId:       1,
+					DashboardId: 1,
+					PanelId:     1,
+
+					Settings: alertJSON,
+				}
+
+				alertRule, err := NewRuleFromDBAlert(alert)
+				So(err, ShouldBeNil)
+
+				So(len(alertRule.Conditions), ShouldEqual, 1)
+
+				Convey("Can read notifications", func() {
+					So(len(alertRule.Notifications), ShouldEqual, 2)
+					So(alertRule.Notifications, ShouldContain, "000000001")
+					So(alertRule.Notifications, ShouldContain, "notifier2")
+				})
+			})
+		})
+
+		Convey("can construct alert rule model with invalid frequency", func() {
 			json := `
 			json := `
 			{
 			{
 				"name": "name2",
 				"name": "name2",
 				"description": "desc2",
 				"description": "desc2",
-				"handler": 0,
 				"noDataMode": "critical",
 				"noDataMode": "critical",
 				"enabled": true,
 				"enabled": true,
-				"frequency": "60s",
-        "conditions": [
-          {
-            "type": "test",
-            "prop": 123
-					}
-        ],
-        "notifications": [
-					{"id": 1134},
-					{"id": 22}
-				]
-			}
-			`
+				"frequency": "0s",
+				"conditions": [ { "type": "test", "prop": 123 } ],
+				"notifications": []
+			}`
 
 
 			alertJSON, jsonErr := simplejson.NewJson([]byte(json))
 			alertJSON, jsonErr := simplejson.NewJson([]byte(json))
 			So(jsonErr, ShouldBeNil)
 			So(jsonErr, ShouldBeNil)
@@ -86,31 +133,35 @@ func TestAlertRuleModel(t *testing.T) {
 				OrgId:       1,
 				OrgId:       1,
 				DashboardId: 1,
 				DashboardId: 1,
 				PanelId:     1,
 				PanelId:     1,
+				Frequency:   0,
 
 
 				Settings: alertJSON,
 				Settings: alertJSON,
 			}
 			}
 
 
 			alertRule, err := NewRuleFromDBAlert(alert)
 			alertRule, err := NewRuleFromDBAlert(alert)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
-
-			So(len(alertRule.Conditions), ShouldEqual, 1)
-
-			Convey("Can read notifications", func() {
-				So(len(alertRule.Notifications), ShouldEqual, 2)
-			})
+			So(alertRule.Frequency, ShouldEqual, 60)
 		})
 		})
 
 
-		Convey("can construct alert rule model with invalid frequency", func() {
+		Convey("raise error in case of missing notification id and uid", func() {
 			json := `
 			json := `
 			{
 			{
 				"name": "name2",
 				"name": "name2",
 				"description": "desc2",
 				"description": "desc2",
 				"noDataMode": "critical",
 				"noDataMode": "critical",
 				"enabled": true,
 				"enabled": true,
-				"frequency": "0s",
-        		"conditions": [ { "type": "test", "prop": 123 } ],
-        		"notifications": []
-			}`
+				"frequency": "60s",
+				"conditions": [
+					{
+						"type": "test",
+						"prop": 123
+					}
+				],
+				"notifications": [
+					{"not_id_uid": "1134"}
+				]
+			}
+			`
 
 
 			alertJSON, jsonErr := simplejson.NewJson([]byte(json))
 			alertJSON, jsonErr := simplejson.NewJson([]byte(json))
 			So(jsonErr, ShouldBeNil)
 			So(jsonErr, ShouldBeNil)
@@ -125,9 +176,9 @@ func TestAlertRuleModel(t *testing.T) {
 				Settings: alertJSON,
 				Settings: alertJSON,
 			}
 			}
 
 
-			alertRule, err := NewRuleFromDBAlert(alert)
-			So(err, ShouldBeNil)
-			So(alertRule.Frequency, ShouldEqual, 60)
+			_, err := NewRuleFromDBAlert(alert)
+			So(err, ShouldNotBeNil)
+			So(err.Error(), ShouldEqual, "Alert validation error: Neither id nor uid is specified, type assertion to string failed AlertId: 1 PanelId: 1 DashboardId: 1")
 		})
 		})
 	})
 	})
 }
 }

+ 4 - 1
pkg/services/alerting/testdata/dash-without-id.json

@@ -44,7 +44,10 @@
               "noDataState": "no_data",
               "noDataState": "no_data",
               "notifications": [
               "notifications": [
                 {
                 {
-                  "id": 6
+                  "uid": "notifier1"
+                },
+                {
+                  "id": 2
                 }
                 }
               ]
               ]
             },
             },

+ 4 - 1
pkg/services/alerting/testdata/influxdb-alert.json

@@ -45,7 +45,10 @@
               "noDataState": "no_data",
               "noDataState": "no_data",
               "notifications": [
               "notifications": [
                 {
                 {
-                  "id": 6
+                  "id": 1
+                },
+                {
+                  "uid": "notifier2"
                 }
                 }
               ]
               ]
             },
             },

+ 180 - 0
pkg/services/provisioning/notifiers/alert_notifications.go

@@ -0,0 +1,180 @@
+package notifiers
+
+import (
+	"errors"
+
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
+)
+
+var (
+	ErrInvalidConfigTooManyDefault = errors.New("Alert notification provisioning config is invalid. Only one alert notification can be marked as default")
+)
+
+func Provision(configDirectory string) error {
+	dc := newNotificationProvisioner(log.New("provisioning.notifiers"))
+	return dc.applyChanges(configDirectory)
+}
+
+type NotificationProvisioner struct {
+	log         log.Logger
+	cfgProvider *configReader
+}
+
+func newNotificationProvisioner(log log.Logger) NotificationProvisioner {
+	return NotificationProvisioner{
+		log:         log,
+		cfgProvider: &configReader{log: log},
+	}
+}
+
+func (dc *NotificationProvisioner) apply(cfg *notificationsAsConfig) error {
+	if err := dc.deleteNotifications(cfg.DeleteNotifications); err != nil {
+		return err
+	}
+
+	if err := dc.mergeNotifications(cfg.Notifications); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (dc *NotificationProvisioner) deleteNotifications(notificationToDelete []*deleteNotificationConfig) error {
+	for _, notification := range notificationToDelete {
+		dc.log.Info("Deleting alert notification", "name", notification.Name, "uid", notification.Uid)
+
+		if notification.OrgId == 0 && notification.OrgName != "" {
+			getOrg := &models.GetOrgByNameQuery{Name: notification.OrgName}
+			if err := bus.Dispatch(getOrg); err != nil {
+				return err
+			}
+			notification.OrgId = getOrg.Result.Id
+		} else if notification.OrgId < 0 {
+			notification.OrgId = 1
+		}
+
+		getNotification := &models.GetAlertNotificationsWithUidQuery{Uid: notification.Uid, OrgId: notification.OrgId}
+
+		if err := bus.Dispatch(getNotification); err != nil {
+			return err
+		}
+
+		if getNotification.Result != nil {
+			cmd := &models.DeleteAlertNotificationWithUidCommand{Uid: getNotification.Result.Uid, OrgId: getNotification.OrgId}
+			if err := bus.Dispatch(cmd); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func (dc *NotificationProvisioner) mergeNotifications(notificationToMerge []*notificationFromConfig) error {
+	for _, notification := range notificationToMerge {
+
+		if notification.OrgId == 0 && notification.OrgName != "" {
+			getOrg := &models.GetOrgByNameQuery{Name: notification.OrgName}
+			if err := bus.Dispatch(getOrg); err != nil {
+				return err
+			}
+			notification.OrgId = getOrg.Result.Id
+		} else if notification.OrgId < 0 {
+			notification.OrgId = 1
+		}
+
+		cmd := &models.GetAlertNotificationsWithUidQuery{OrgId: notification.OrgId, Uid: notification.Uid}
+		err := bus.Dispatch(cmd)
+		if err != nil {
+			return err
+		}
+
+		if cmd.Result == nil {
+			dc.log.Info("Inserting alert notification from configuration ", "name", notification.Name, "uid", notification.Uid)
+			insertCmd := &models.CreateAlertNotificationCommand{
+				Uid:                   notification.Uid,
+				Name:                  notification.Name,
+				Type:                  notification.Type,
+				IsDefault:             notification.IsDefault,
+				Settings:              notification.SettingsToJson(),
+				OrgId:                 notification.OrgId,
+				DisableResolveMessage: notification.DisableResolveMessage,
+				Frequency:             notification.Frequency,
+				SendReminder:          notification.SendReminder,
+			}
+
+			if err := bus.Dispatch(insertCmd); err != nil {
+				return err
+			}
+		} else {
+			dc.log.Info("Updating alert notification from configuration", "name", notification.Name)
+			updateCmd := &models.UpdateAlertNotificationWithUidCommand{
+				Uid:                   notification.Uid,
+				Name:                  notification.Name,
+				Type:                  notification.Type,
+				IsDefault:             notification.IsDefault,
+				Settings:              notification.SettingsToJson(),
+				OrgId:                 notification.OrgId,
+				DisableResolveMessage: notification.DisableResolveMessage,
+				Frequency:             notification.Frequency,
+				SendReminder:          notification.SendReminder,
+			}
+
+			if err := bus.Dispatch(updateCmd); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func (cfg *notificationsAsConfig) mapToNotificationFromConfig() *notificationsAsConfig {
+	r := &notificationsAsConfig{}
+	if cfg == nil {
+		return r
+	}
+
+	for _, notification := range cfg.Notifications {
+		r.Notifications = append(r.Notifications, &notificationFromConfig{
+			Uid:                   notification.Uid,
+			OrgId:                 notification.OrgId,
+			OrgName:               notification.OrgName,
+			Name:                  notification.Name,
+			Type:                  notification.Type,
+			IsDefault:             notification.IsDefault,
+			Settings:              notification.Settings,
+			DisableResolveMessage: notification.DisableResolveMessage,
+			Frequency:             notification.Frequency,
+			SendReminder:          notification.SendReminder,
+		})
+	}
+
+	for _, notification := range cfg.DeleteNotifications {
+		r.DeleteNotifications = append(r.DeleteNotifications, &deleteNotificationConfig{
+			Uid:     notification.Uid,
+			OrgId:   notification.OrgId,
+			OrgName: notification.OrgName,
+			Name:    notification.Name,
+		})
+	}
+
+	return r
+}
+
+func (dc *NotificationProvisioner) applyChanges(configPath string) error {
+	configs, err := dc.cfgProvider.readConfig(configPath)
+	if err != nil {
+		return err
+	}
+
+	for _, cfg := range configs {
+		if err := dc.apply(cfg); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}

+ 163 - 0
pkg/services/provisioning/notifiers/config_reader.go

@@ -0,0 +1,163 @@
+package notifiers
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/grafana/grafana/pkg/log"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/alerting"
+	"gopkg.in/yaml.v2"
+)
+
+type configReader struct {
+	log log.Logger
+}
+
+func (cr *configReader) readConfig(path string) ([]*notificationsAsConfig, error) {
+	var notifications []*notificationsAsConfig
+	cr.log.Debug("Looking for alert notification provisioning files", "path", path)
+
+	files, err := ioutil.ReadDir(path)
+	if err != nil {
+		cr.log.Error("Can't read alert notification provisioning files from directory", "path", path)
+		return notifications, nil
+	}
+
+	for _, file := range files {
+		if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") {
+			cr.log.Debug("Parsing alert notifications provisioning file", "path", path, "file.Name", file.Name())
+			notifs, err := cr.parseNotificationConfig(path, file)
+			if err != nil {
+				return nil, err
+			}
+
+			if notifs != nil {
+				notifications = append(notifications, notifs)
+			}
+		}
+	}
+
+	cr.log.Debug("Validating alert notifications")
+	if err = validateRequiredField(notifications); err != nil {
+		return nil, err
+	}
+
+	checkOrgIdAndOrgName(notifications)
+
+	err = validateNotifications(notifications)
+	if err != nil {
+		return nil, err
+	}
+
+	return notifications, nil
+}
+
+func (cr *configReader) parseNotificationConfig(path string, file os.FileInfo) (*notificationsAsConfig, error) {
+	filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
+	yamlFile, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	var cfg *notificationsAsConfig
+	err = yaml.Unmarshal(yamlFile, &cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	return cfg.mapToNotificationFromConfig(), nil
+}
+
+func checkOrgIdAndOrgName(notifications []*notificationsAsConfig) {
+	for i := range notifications {
+		for _, notification := range notifications[i].Notifications {
+			if notification.OrgId < 1 {
+				if notification.OrgName == "" {
+					notification.OrgId = 1
+				} else {
+					notification.OrgId = 0
+				}
+			}
+		}
+
+		for _, notification := range notifications[i].DeleteNotifications {
+			if notification.OrgId < 1 {
+				if notification.OrgName == "" {
+					notification.OrgId = 1
+				} else {
+					notification.OrgId = 0
+				}
+			}
+		}
+	}
+}
+
+func validateRequiredField(notifications []*notificationsAsConfig) error {
+	for i := range notifications {
+		var errStrings []string
+		for index, notification := range notifications[i].Notifications {
+			if notification.Name == "" {
+				errStrings = append(
+					errStrings,
+					fmt.Sprintf("Added alert notification item %d in configuration doesn't contain required field name", index+1),
+				)
+			}
+
+			if notification.Uid == "" {
+				errStrings = append(
+					errStrings,
+					fmt.Sprintf("Added alert notification item %d in configuration doesn't contain required field uid", index+1),
+				)
+			}
+		}
+
+		for index, notification := range notifications[i].DeleteNotifications {
+			if notification.Name == "" {
+				errStrings = append(
+					errStrings,
+					fmt.Sprintf("Deleted alert notification item %d in configuration doesn't contain required field name", index+1),
+				)
+			}
+
+			if notification.Uid == "" {
+				errStrings = append(
+					errStrings,
+					fmt.Sprintf("Deleted alert notification item %d in configuration doesn't contain required field uid", index+1),
+				)
+			}
+		}
+
+		if len(errStrings) != 0 {
+			return fmt.Errorf(strings.Join(errStrings, "\n"))
+		}
+	}
+
+	return nil
+}
+
+func validateNotifications(notifications []*notificationsAsConfig) error {
+
+	for i := range notifications {
+		if notifications[i].Notifications == nil {
+			continue
+		}
+
+		for _, notification := range notifications[i].Notifications {
+			_, err := alerting.InitNotifier(&m.AlertNotification{
+				Name:     notification.Name,
+				Settings: notification.SettingsToJson(),
+				Type:     notification.Type,
+			})
+
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}

+ 313 - 0
pkg/services/provisioning/notifiers/config_reader_test.go

@@ -0,0 +1,313 @@
+package notifiers
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/log"
+	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/alerting"
+	"github.com/grafana/grafana/pkg/services/alerting/notifiers"
+	"github.com/grafana/grafana/pkg/services/sqlstore"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+var (
+	correct_properties              = "./testdata/test-configs/correct-properties"
+	incorrect_settings              = "./testdata/test-configs/incorrect-settings"
+	no_required_fields              = "./testdata/test-configs/no-required-fields"
+	correct_properties_with_orgName = "./testdata/test-configs/correct-properties-with-orgName"
+	brokenYaml                      = "./testdata/test-configs/broken-yaml"
+	doubleNotificationsConfig       = "./testdata/test-configs/double-default"
+	emptyFolder                     = "./testdata/test-configs/empty_folder"
+	emptyFile                       = "./testdata/test-configs/empty"
+	twoNotificationsConfig          = "./testdata/test-configs/two-notifications"
+	unknownNotifier                 = "./testdata/test-configs/unknown-notifier"
+)
+
+func TestNotificationAsConfig(t *testing.T) {
+	logger := log.New("fake.log")
+
+	Convey("Testing notification as configuration", t, func() {
+		sqlstore.InitTestDB(t)
+
+		alerting.RegisterNotifier(&alerting.NotifierPlugin{
+			Type:    "slack",
+			Name:    "slack",
+			Factory: notifiers.NewSlackNotifier,
+		})
+
+		alerting.RegisterNotifier(&alerting.NotifierPlugin{
+			Type:    "email",
+			Name:    "email",
+			Factory: notifiers.NewEmailNotifier,
+		})
+
+		Convey("Can read correct properties", func() {
+			cfgProvifer := &configReader{log: log.New("test logger")}
+			cfg, err := cfgProvifer.readConfig(correct_properties)
+			if err != nil {
+				t.Fatalf("readConfig return an error %v", err)
+			}
+			So(len(cfg), ShouldEqual, 1)
+
+			ntCfg := cfg[0]
+			nts := ntCfg.Notifications
+			So(len(nts), ShouldEqual, 4)
+
+			nt := nts[0]
+			So(nt.Name, ShouldEqual, "default-slack-notification")
+			So(nt.Type, ShouldEqual, "slack")
+			So(nt.OrgId, ShouldEqual, 2)
+			So(nt.Uid, ShouldEqual, "notifier1")
+			So(nt.IsDefault, ShouldBeTrue)
+			So(nt.Settings, ShouldResemble, map[string]interface{}{
+				"recipient": "XXX", "token": "xoxb", "uploadImage": true, "url": "https://slack.com",
+			})
+
+			nt = nts[1]
+			So(nt.Name, ShouldEqual, "another-not-default-notification")
+			So(nt.Type, ShouldEqual, "email")
+			So(nt.OrgId, ShouldEqual, 3)
+			So(nt.Uid, ShouldEqual, "notifier2")
+			So(nt.IsDefault, ShouldBeFalse)
+
+			nt = nts[2]
+			So(nt.Name, ShouldEqual, "check-unset-is_default-is-false")
+			So(nt.Type, ShouldEqual, "slack")
+			So(nt.OrgId, ShouldEqual, 3)
+			So(nt.Uid, ShouldEqual, "notifier3")
+			So(nt.IsDefault, ShouldBeFalse)
+
+			nt = nts[3]
+			So(nt.Name, ShouldEqual, "Added notification with whitespaces in name")
+			So(nt.Type, ShouldEqual, "email")
+			So(nt.Uid, ShouldEqual, "notifier4")
+			So(nt.OrgId, ShouldEqual, 3)
+
+			deleteNts := ntCfg.DeleteNotifications
+			So(len(deleteNts), ShouldEqual, 4)
+
+			deleteNt := deleteNts[0]
+			So(deleteNt.Name, ShouldEqual, "default-slack-notification")
+			So(deleteNt.Uid, ShouldEqual, "notifier1")
+			So(deleteNt.OrgId, ShouldEqual, 2)
+
+			deleteNt = deleteNts[1]
+			So(deleteNt.Name, ShouldEqual, "deleted-notification-without-orgId")
+			So(deleteNt.OrgId, ShouldEqual, 1)
+			So(deleteNt.Uid, ShouldEqual, "notifier2")
+
+			deleteNt = deleteNts[2]
+			So(deleteNt.Name, ShouldEqual, "deleted-notification-with-0-orgId")
+			So(deleteNt.OrgId, ShouldEqual, 1)
+			So(deleteNt.Uid, ShouldEqual, "notifier3")
+
+			deleteNt = deleteNts[3]
+			So(deleteNt.Name, ShouldEqual, "Deleted notification with whitespaces in name")
+			So(deleteNt.OrgId, ShouldEqual, 1)
+			So(deleteNt.Uid, ShouldEqual, "notifier4")
+		})
+
+		Convey("One configured notification", func() {
+			Convey("no notification in database", func() {
+				dc := newNotificationProvisioner(logger)
+				err := dc.applyChanges(twoNotificationsConfig)
+				if err != nil {
+					t.Fatalf("applyChanges return an error %v", err)
+				}
+				notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1}
+				err = sqlstore.GetAllAlertNotifications(&notificationsQuery)
+				So(err, ShouldBeNil)
+				So(notificationsQuery.Result, ShouldNotBeNil)
+				So(len(notificationsQuery.Result), ShouldEqual, 2)
+			})
+
+			Convey("One notification in database with same name and uid", func() {
+				existingNotificationCmd := m.CreateAlertNotificationCommand{
+					Name:  "channel1",
+					OrgId: 1,
+					Uid:   "notifier1",
+					Type:  "slack",
+				}
+				err := sqlstore.CreateAlertNotificationCommand(&existingNotificationCmd)
+				So(err, ShouldBeNil)
+				So(existingNotificationCmd.Result, ShouldNotBeNil)
+				notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1}
+				err = sqlstore.GetAllAlertNotifications(&notificationsQuery)
+				So(err, ShouldBeNil)
+				So(notificationsQuery.Result, ShouldNotBeNil)
+				So(len(notificationsQuery.Result), ShouldEqual, 1)
+
+				Convey("should update one notification", func() {
+					dc := newNotificationProvisioner(logger)
+					err = dc.applyChanges(twoNotificationsConfig)
+					if err != nil {
+						t.Fatalf("applyChanges return an error %v", err)
+					}
+					err = sqlstore.GetAllAlertNotifications(&notificationsQuery)
+					So(err, ShouldBeNil)
+					So(notificationsQuery.Result, ShouldNotBeNil)
+					So(len(notificationsQuery.Result), ShouldEqual, 2)
+
+					nts := notificationsQuery.Result
+					nt1 := nts[0]
+					So(nt1.Type, ShouldEqual, "email")
+					So(nt1.Name, ShouldEqual, "channel1")
+					So(nt1.Uid, ShouldEqual, "notifier1")
+
+					nt2 := nts[1]
+					So(nt2.Type, ShouldEqual, "slack")
+					So(nt2.Name, ShouldEqual, "channel2")
+					So(nt2.Uid, ShouldEqual, "notifier2")
+				})
+			})
+			Convey("Two notifications with is_default", func() {
+				dc := newNotificationProvisioner(logger)
+				err := dc.applyChanges(doubleNotificationsConfig)
+				Convey("should both be inserted", func() {
+					So(err, ShouldBeNil)
+					notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1}
+					err = sqlstore.GetAllAlertNotifications(&notificationsQuery)
+					So(err, ShouldBeNil)
+					So(notificationsQuery.Result, ShouldNotBeNil)
+					So(len(notificationsQuery.Result), ShouldEqual, 2)
+
+					So(notificationsQuery.Result[0].IsDefault, ShouldBeTrue)
+					So(notificationsQuery.Result[1].IsDefault, ShouldBeTrue)
+				})
+			})
+		})
+
+		Convey("Two configured notification", func() {
+			Convey("two other notifications in database", func() {
+				existingNotificationCmd := m.CreateAlertNotificationCommand{
+					Name:  "channel0",
+					OrgId: 1,
+					Uid:   "notifier0",
+					Type:  "slack",
+				}
+				err := sqlstore.CreateAlertNotificationCommand(&existingNotificationCmd)
+				So(err, ShouldBeNil)
+				existingNotificationCmd = m.CreateAlertNotificationCommand{
+					Name:  "channel3",
+					OrgId: 1,
+					Uid:   "notifier3",
+					Type:  "slack",
+				}
+				err = sqlstore.CreateAlertNotificationCommand(&existingNotificationCmd)
+				So(err, ShouldBeNil)
+
+				notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1}
+				err = sqlstore.GetAllAlertNotifications(&notificationsQuery)
+				So(err, ShouldBeNil)
+				So(notificationsQuery.Result, ShouldNotBeNil)
+				So(len(notificationsQuery.Result), ShouldEqual, 2)
+
+				Convey("should have two new notifications", func() {
+					dc := newNotificationProvisioner(logger)
+					err := dc.applyChanges(twoNotificationsConfig)
+					if err != nil {
+						t.Fatalf("applyChanges return an error %v", err)
+					}
+					notificationsQuery = m.GetAllAlertNotificationsQuery{OrgId: 1}
+					err = sqlstore.GetAllAlertNotifications(&notificationsQuery)
+					So(err, ShouldBeNil)
+					So(notificationsQuery.Result, ShouldNotBeNil)
+					So(len(notificationsQuery.Result), ShouldEqual, 4)
+				})
+			})
+		})
+
+		Convey("Can read correct properties with orgName instead of orgId", func() {
+			existingOrg1 := m.CreateOrgCommand{Name: "Main Org. 1"}
+			err := sqlstore.CreateOrg(&existingOrg1)
+			So(err, ShouldBeNil)
+			So(existingOrg1.Result, ShouldNotBeNil)
+			existingOrg2 := m.CreateOrgCommand{Name: "Main Org. 2"}
+			err = sqlstore.CreateOrg(&existingOrg2)
+			So(err, ShouldBeNil)
+			So(existingOrg2.Result, ShouldNotBeNil)
+
+			existingNotificationCmd := m.CreateAlertNotificationCommand{
+				Name:  "default-notification-delete",
+				OrgId: existingOrg2.Result.Id,
+				Uid:   "notifier2",
+				Type:  "slack",
+			}
+			err = sqlstore.CreateAlertNotificationCommand(&existingNotificationCmd)
+			So(err, ShouldBeNil)
+
+			dc := newNotificationProvisioner(logger)
+			err = dc.applyChanges(correct_properties_with_orgName)
+			if err != nil {
+				t.Fatalf("applyChanges return an error %v", err)
+			}
+
+			notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: existingOrg2.Result.Id}
+			err = sqlstore.GetAllAlertNotifications(&notificationsQuery)
+			So(err, ShouldBeNil)
+			So(notificationsQuery.Result, ShouldNotBeNil)
+			So(len(notificationsQuery.Result), ShouldEqual, 1)
+
+			nt := notificationsQuery.Result[0]
+			So(nt.Name, ShouldEqual, "default-notification-create")
+			So(nt.OrgId, ShouldEqual, existingOrg2.Result.Id)
+
+		})
+
+		Convey("Config doesn't contain required field", func() {
+			dc := newNotificationProvisioner(logger)
+			err := dc.applyChanges(no_required_fields)
+			So(err, ShouldNotBeNil)
+
+			errString := err.Error()
+			So(errString, ShouldContainSubstring, "Deleted alert notification item 1 in configuration doesn't contain required field uid")
+			So(errString, ShouldContainSubstring, "Deleted alert notification item 2 in configuration doesn't contain required field name")
+			So(errString, ShouldContainSubstring, "Added alert notification item 1 in configuration doesn't contain required field name")
+			So(errString, ShouldContainSubstring, "Added alert notification item 2 in configuration doesn't contain required field uid")
+		})
+
+		Convey("Empty yaml file", func() {
+			Convey("should have not changed repo", func() {
+				dc := newNotificationProvisioner(logger)
+				err := dc.applyChanges(emptyFile)
+				if err != nil {
+					t.Fatalf("applyChanges return an error %v", err)
+				}
+				notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1}
+				err = sqlstore.GetAllAlertNotifications(&notificationsQuery)
+				So(err, ShouldBeNil)
+				So(notificationsQuery.Result, ShouldBeEmpty)
+			})
+		})
+
+		Convey("Broken yaml should return error", func() {
+			reader := &configReader{log: log.New("test logger")}
+			_, err := reader.readConfig(brokenYaml)
+			So(err, ShouldNotBeNil)
+		})
+
+		Convey("Skip invalid directory", func() {
+			cfgProvifer := &configReader{log: log.New("test logger")}
+			cfg, err := cfgProvifer.readConfig(emptyFolder)
+			if err != nil {
+				t.Fatalf("readConfig return an error %v", err)
+			}
+			So(len(cfg), ShouldEqual, 0)
+		})
+
+		Convey("Unknown notifier should return error", func() {
+			cfgProvifer := &configReader{log: log.New("test logger")}
+			_, err := cfgProvifer.readConfig(unknownNotifier)
+			So(err, ShouldNotBeNil)
+			So(err.Error(), ShouldEqual, "Unsupported notification type")
+		})
+
+		Convey("Read incorrect properties", func() {
+			cfgProvifer := &configReader{log: log.New("test logger")}
+			_, err := cfgProvifer.readConfig(incorrect_settings)
+			So(err, ShouldNotBeNil)
+			So(err.Error(), ShouldEqual, "Alert validation error: Could not find url property in settings")
+		})
+	})
+}

+ 9 - 0
pkg/services/provisioning/notifiers/testdata/test-configs/broken-yaml/broken.yaml

@@ -0,0 +1,9 @@
+notifiers:
+  - name: notification-channel-1
+     type: slack
+    org_id: 2
+     is_default: true
+   settings:
+      recipient: "XXX"
+      token: "xoxb"
+      uploadImage: true

+ 6 - 0
pkg/services/provisioning/notifiers/testdata/test-configs/broken-yaml/not.yaml.text

@@ -0,0 +1,6 @@
+#sfxzgnsxzcvnbzcvn
+cvbn
+cvbn
+c
+vbn
+cvbncvbn

+ 12 - 0
pkg/services/provisioning/notifiers/testdata/test-configs/correct-properties-with-orgName/correct-properties-with-orgName.yaml

@@ -0,0 +1,12 @@
+notifiers:
+  - name: default-notification-create
+    type: email
+    uid: notifier2
+    settings:
+      addresses: example@example.com
+    org_name: Main Org. 2
+    is_default: false  
+delete_notifiers:
+  - name: default-notification-delete
+    org_name: Main Org. 2
+    uid: notifier2

+ 42 - 0
pkg/services/provisioning/notifiers/testdata/test-configs/correct-properties/correct-properties.yaml

@@ -0,0 +1,42 @@
+notifiers:
+  - name: default-slack-notification
+    type: slack
+    uid: notifier1
+    org_id: 2
+    uid: "notifier1"
+    is_default: true
+    settings:
+      recipient: "XXX"
+      token: "xoxb"
+      uploadImage: true
+      url: https://slack.com
+  - name: another-not-default-notification
+    type: email
+    settings:
+      addresses: example@exmaple.com
+    org_id: 3
+    uid: "notifier2"
+    is_default: false
+  - name: check-unset-is_default-is-false
+    type: slack
+    org_id: 3
+    uid: "notifier3"
+    settings:
+      url: https://slack.com
+  - name: Added notification with whitespaces in name
+    type: email
+    org_id: 3
+    uid: "notifier4"
+    settings:
+      addresses: example@exmaple.com
+delete_notifiers:
+  - name: default-slack-notification
+    org_id: 2
+    uid: notifier1
+  - name: deleted-notification-without-orgId
+    uid: "notifier2"
+  - name: deleted-notification-with-0-orgId
+    org_id: 0
+    uid: "notifier3"
+  - name: Deleted notification with whitespaces in name
+    uid: "notifier4"

+ 7 - 0
pkg/services/provisioning/notifiers/testdata/test-configs/double-default/default-1.yml

@@ -0,0 +1,7 @@
+notifiers:
+  - name: first-default
+    type: slack
+    uid: notifier1
+    is_default: true
+    settings:
+      url: https://slack.com

+ 7 - 0
pkg/services/provisioning/notifiers/testdata/test-configs/double-default/default-2.yaml

@@ -0,0 +1,7 @@
+notifiers:
+  - name: second-default
+    type: email
+    uid: notifier2
+    is_default: true
+    settings:
+      addresses: example@example.com

+ 0 - 0
pkg/services/provisioning/notifiers/testdata/test-configs/empty/empty.yaml


+ 4 - 0
pkg/services/provisioning/notifiers/testdata/test-configs/empty_folder/.gitignore

@@ -0,0 +1,4 @@
+# Ignore everything in this directory
+*
+# Except this file
+!.gitignore

+ 10 - 0
pkg/services/provisioning/notifiers/testdata/test-configs/incorrect-settings/incorrect-settings.yaml

@@ -0,0 +1,10 @@
+notifiers:
+  - name: slack-notification-without-url-in-settings
+    type: slack
+    org_id: 2
+    uid: notifier1
+    is_default: true
+    settings:
+      recipient: "XXX"
+      token: "xoxb"
+      uploadImage: true

+ 35 - 0
pkg/services/provisioning/notifiers/testdata/test-configs/no-required-fields/no-required-fields.yaml

@@ -0,0 +1,35 @@
+notifiers:
+  - type: slack
+    org_id: 2
+    uid: no-name_added-notification
+    is_default: true
+    settings:
+      recipient: "XXX"
+      token: "xoxb"
+      uploadImage: true
+  - name: no-uid 
+    type: slack
+    org_id: 2    
+    is_default: true
+    settings:
+      recipient: "XXX"
+      token: "xoxb"
+      uploadImage: true
+delete_notifiers:
+  - name: no-uid 
+    type: slack
+    org_id: 2    
+    is_default: true
+    settings:
+      recipient: "XXX"
+      token: "xoxb"
+      uploadImage: true
+  - type: slack
+    org_id: 2
+    uid: no-name_added-notification
+    is_default: true
+    settings:
+      recipient: "XXX"
+      token: "xoxb"
+      uploadImage: true
+      

+ 12 - 0
pkg/services/provisioning/notifiers/testdata/test-configs/two-notifications/two-notifications.yaml

@@ -0,0 +1,12 @@
+notifiers:  
+  - name: channel1
+    type: email
+    uid: notifier1
+    org_id: 1
+    settings:
+      addresses: example@example.com
+  - name: channel2
+    type: slack
+    uid: notifier2
+    settings:
+      url: http://slack.com

+ 4 - 0
pkg/services/provisioning/notifiers/testdata/test-configs/unknown-notifier/notification.yaml

@@ -0,0 +1,4 @@
+notifiers:
+  - name: unknown-notifier
+    type: nonexisting
+    uid: notifier1

+ 38 - 0
pkg/services/provisioning/notifiers/types.go

@@ -0,0 +1,38 @@
+package notifiers
+
+import "github.com/grafana/grafana/pkg/components/simplejson"
+
+type notificationsAsConfig struct {
+	Notifications       []*notificationFromConfig   `json:"notifiers" yaml:"notifiers"`
+	DeleteNotifications []*deleteNotificationConfig `json:"delete_notifiers" yaml:"delete_notifiers"`
+}
+
+type deleteNotificationConfig struct {
+	Uid     string `json:"uid" yaml:"uid"`
+	Name    string `json:"name" yaml:"name"`
+	OrgId   int64  `json:"org_id" yaml:"org_id"`
+	OrgName string `json:"org_name" yaml:"org_name"`
+}
+
+type notificationFromConfig struct {
+	Uid                   string                 `json:"uid" yaml:"uid"`
+	OrgId                 int64                  `json:"org_id" yaml:"org_id"`
+	OrgName               string                 `json:"org_name" yaml:"org_name"`
+	Name                  string                 `json:"name" yaml:"name"`
+	Type                  string                 `json:"type" yaml:"type"`
+	SendReminder          bool                   `json:"send_reminder" yaml:"send_reminder"`
+	DisableResolveMessage bool                   `json:"disable_resolve_message" yaml:"disable_resolve_message"`
+	Frequency             string                 `json:"frequency" yaml:"frequency"`
+	IsDefault             bool                   `json:"is_default" yaml:"is_default"`
+	Settings              map[string]interface{} `json:"settings" yaml:"settings"`
+}
+
+func (notification notificationFromConfig) SettingsToJson() *simplejson.Json {
+	settings := simplejson.New()
+	if len(notification.Settings) > 0 {
+		for k, v := range notification.Settings {
+			settings.Set(k, v)
+		}
+	}
+	return settings
+}

+ 6 - 0
pkg/services/provisioning/provisioning.go

@@ -8,6 +8,7 @@ import (
 	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
 	"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
 	"github.com/grafana/grafana/pkg/services/provisioning/datasources"
 	"github.com/grafana/grafana/pkg/services/provisioning/datasources"
+	"github.com/grafana/grafana/pkg/services/provisioning/notifiers"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
@@ -25,6 +26,11 @@ func (ps *ProvisioningService) Init() error {
 		return fmt.Errorf("Datasource provisioning error: %v", err)
 		return fmt.Errorf("Datasource provisioning error: %v", err)
 	}
 	}
 
 
+	alertNotificationsPath := path.Join(ps.Cfg.ProvisioningPath, "notifiers")
+	if err := notifiers.Provision(alertNotificationsPath); err != nil {
+		return fmt.Errorf("Alert notification provisioning error: %v", err)
+	}
+
 	return nil
 	return nil
 }
 }
 
 

+ 142 - 8
pkg/services/sqlstore/alert_notification.go

@@ -10,6 +10,7 @@ import (
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
 func init() {
 func init() {
@@ -17,11 +18,15 @@ func init() {
 	bus.AddHandler("sql", CreateAlertNotificationCommand)
 	bus.AddHandler("sql", CreateAlertNotificationCommand)
 	bus.AddHandler("sql", UpdateAlertNotification)
 	bus.AddHandler("sql", UpdateAlertNotification)
 	bus.AddHandler("sql", DeleteAlertNotification)
 	bus.AddHandler("sql", DeleteAlertNotification)
-	bus.AddHandler("sql", GetAlertNotificationsToSend)
 	bus.AddHandler("sql", GetAllAlertNotifications)
 	bus.AddHandler("sql", GetAllAlertNotifications)
 	bus.AddHandlerCtx("sql", GetOrCreateAlertNotificationState)
 	bus.AddHandlerCtx("sql", GetOrCreateAlertNotificationState)
 	bus.AddHandlerCtx("sql", SetAlertNotificationStateToCompleteCommand)
 	bus.AddHandlerCtx("sql", SetAlertNotificationStateToCompleteCommand)
 	bus.AddHandlerCtx("sql", SetAlertNotificationStateToPendingCommand)
 	bus.AddHandlerCtx("sql", SetAlertNotificationStateToPendingCommand)
+
+	bus.AddHandler("sql", GetAlertNotificationsWithUid)
+	bus.AddHandler("sql", UpdateAlertNotificationWithUid)
+	bus.AddHandler("sql", DeleteAlertNotificationWithUid)
+	bus.AddHandler("sql", GetAlertNotificationsWithUidToSend)
 }
 }
 
 
 func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
 func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
@@ -39,10 +44,33 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
 	})
 	})
 }
 }
 
 
+func DeleteAlertNotificationWithUid(cmd *m.DeleteAlertNotificationWithUidCommand) error {
+	existingNotification := &m.GetAlertNotificationsWithUidQuery{OrgId: cmd.OrgId, Uid: cmd.Uid}
+	if err := getAlertNotificationWithUidInternal(existingNotification, newSession()); err != nil {
+		return err
+	}
+
+	if existingNotification.Result != nil {
+		deleteCommand := &m.DeleteAlertNotificationCommand{
+			Id:    existingNotification.Result.Id,
+			OrgId: existingNotification.Result.OrgId,
+		}
+		if err := bus.Dispatch(deleteCommand); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 func GetAlertNotifications(query *m.GetAlertNotificationsQuery) error {
 func GetAlertNotifications(query *m.GetAlertNotificationsQuery) error {
 	return getAlertNotificationInternal(query, newSession())
 	return getAlertNotificationInternal(query, newSession())
 }
 }
 
 
+func GetAlertNotificationsWithUid(query *m.GetAlertNotificationsWithUidQuery) error {
+	return getAlertNotificationWithUidInternal(query, newSession())
+}
+
 func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error {
 func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error {
 	results := make([]*m.AlertNotification, 0)
 	results := make([]*m.AlertNotification, 0)
 	if err := x.Where("org_id = ?", query.OrgId).Find(&results); err != nil {
 	if err := x.Where("org_id = ?", query.OrgId).Find(&results); err != nil {
@@ -53,12 +81,13 @@ func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error {
 	return nil
 	return nil
 }
 }
 
 
-func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) error {
+func GetAlertNotificationsWithUidToSend(query *m.GetAlertNotificationsWithUidToSendQuery) error {
 	var sql bytes.Buffer
 	var sql bytes.Buffer
 	params := make([]interface{}, 0)
 	params := make([]interface{}, 0)
 
 
-	sql.WriteString(`SELECT
+	sql.WriteString(`SELECT										
 										alert_notification.id,
 										alert_notification.id,
+										alert_notification.uid,
 										alert_notification.org_id,
 										alert_notification.org_id,
 										alert_notification.name,
 										alert_notification.name,
 										alert_notification.type,
 										alert_notification.type,
@@ -77,9 +106,10 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro
 
 
 	sql.WriteString(` AND ((alert_notification.is_default = ?)`)
 	sql.WriteString(` AND ((alert_notification.is_default = ?)`)
 	params = append(params, dialect.BooleanStr(true))
 	params = append(params, dialect.BooleanStr(true))
-	if len(query.Ids) > 0 {
-		sql.WriteString(` OR alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")")
-		for _, v := range query.Ids {
+
+	if len(query.Uids) > 0 {
+		sql.WriteString(` OR alert_notification.uid IN (?` + strings.Repeat(",?", len(query.Uids)-1) + ")")
+		for _, v := range query.Uids {
 			params = append(params, v)
 			params = append(params, v)
 		}
 		}
 	}
 	}
@@ -142,16 +172,70 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS
 	return nil
 	return nil
 }
 }
 
 
+func getAlertNotificationWithUidInternal(query *m.GetAlertNotificationsWithUidQuery, sess *DBSession) error {
+	var sql bytes.Buffer
+	params := make([]interface{}, 0)
+
+	sql.WriteString(`SELECT
+										alert_notification.id,
+										alert_notification.uid,
+										alert_notification.org_id,
+										alert_notification.name,
+										alert_notification.type,
+										alert_notification.created,
+										alert_notification.updated,
+										alert_notification.settings,
+										alert_notification.is_default,
+										alert_notification.disable_resolve_message,
+										alert_notification.send_reminder,
+										alert_notification.frequency
+										FROM alert_notification
+	  							`)
+
+	sql.WriteString(` WHERE alert_notification.org_id = ? AND alert_notification.uid = ?`)
+	params = append(params, query.OrgId, query.Uid)
+
+	results := make([]*m.AlertNotification, 0)
+	if err := sess.SQL(sql.String(), params...).Find(&results); err != nil {
+		return err
+	}
+
+	if len(results) == 0 {
+		query.Result = nil
+	} else {
+		query.Result = results[0]
+	}
+
+	return nil
+}
+
 func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error {
 func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
-		existingQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
-		err := getAlertNotificationInternal(existingQuery, sess)
+		if cmd.Uid == "" {
+			if uid, uidGenerationErr := generateNewAlertNotificationUid(sess, cmd.OrgId); uidGenerationErr != nil {
+				return uidGenerationErr
+			} else {
+				cmd.Uid = uid
+			}
+		}
+		existingQuery := &m.GetAlertNotificationsWithUidQuery{OrgId: cmd.OrgId, Uid: cmd.Uid}
+		err := getAlertNotificationWithUidInternal(existingQuery, sess)
 
 
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
 		if existingQuery.Result != nil {
 		if existingQuery.Result != nil {
+			return fmt.Errorf("Alert notification uid %s already exists", cmd.Uid)
+		}
+
+		// check if name exists
+		sameNameQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
+		if err := getAlertNotificationInternal(sameNameQuery, sess); err != nil {
+			return err
+		}
+
+		if sameNameQuery.Result != nil {
 			return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
 			return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
 		}
 		}
 
 
@@ -168,6 +252,7 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
 		}
 		}
 
 
 		alertNotification := &m.AlertNotification{
 		alertNotification := &m.AlertNotification{
+			Uid:                   cmd.Uid,
 			OrgId:                 cmd.OrgId,
 			OrgId:                 cmd.OrgId,
 			Name:                  cmd.Name,
 			Name:                  cmd.Name,
 			Type:                  cmd.Type,
 			Type:                  cmd.Type,
@@ -189,6 +274,22 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
 	})
 	})
 }
 }
 
 
+func generateNewAlertNotificationUid(sess *DBSession, orgId int64) (string, error) {
+	for i := 0; i < 3; i++ {
+		uid := util.GenerateShortUid()
+		exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&m.AlertNotification{})
+		if err != nil {
+			return "", err
+		}
+
+		if !exists {
+			return uid, nil
+		}
+	}
+
+	return "", m.ErrAlertNotificationFailedGenerateUniqueUid
+}
+
 func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
 func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
 	return inTransaction(func(sess *DBSession) (err error) {
 	return inTransaction(func(sess *DBSession) (err error) {
 		current := m.AlertNotification{}
 		current := m.AlertNotification{}
@@ -241,6 +342,39 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
 	})
 	})
 }
 }
 
 
+func UpdateAlertNotificationWithUid(cmd *m.UpdateAlertNotificationWithUidCommand) error {
+	getAlertNotificationWithUidQuery := &m.GetAlertNotificationsWithUidQuery{OrgId: cmd.OrgId, Uid: cmd.Uid}
+
+	if err := getAlertNotificationWithUidInternal(getAlertNotificationWithUidQuery, newSession()); err != nil {
+		return err
+	}
+
+	current := getAlertNotificationWithUidQuery.Result
+
+	if current == nil {
+		return fmt.Errorf("Cannot update, alert notification uid %s doesn't exist", cmd.Uid)
+	}
+
+	updateNotification := &m.UpdateAlertNotificationCommand{
+		Id:                    current.Id,
+		Name:                  cmd.Name,
+		Type:                  cmd.Type,
+		SendReminder:          cmd.SendReminder,
+		DisableResolveMessage: cmd.DisableResolveMessage,
+		Frequency:             cmd.Frequency,
+		IsDefault:             cmd.IsDefault,
+		Settings:              cmd.Settings,
+
+		OrgId: cmd.OrgId,
+	}
+
+	if err := bus.Dispatch(updateNotification); err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToCompleteCommand) error {
 func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToCompleteCommand) error {
 	return inTransactionCtx(ctx, func(sess *DBSession) error {
 	return inTransactionCtx(ctx, func(sess *DBSession) error {
 		version := cmd.Version
 		version := cmd.Version

+ 30 - 3
pkg/services/sqlstore/alert_notification_test.go

@@ -220,11 +220,38 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
 			So(cmd.Result.Type, ShouldEqual, "email")
 			So(cmd.Result.Type, ShouldEqual, "email")
 			So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
 			So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
 			So(cmd.Result.DisableResolveMessage, ShouldBeFalse)
 			So(cmd.Result.DisableResolveMessage, ShouldBeFalse)
+			So(cmd.Result.Uid, ShouldNotBeEmpty)
 
 
 			Convey("Cannot save Alert Notification with the same name", func() {
 			Convey("Cannot save Alert Notification with the same name", func() {
 				err = CreateAlertNotificationCommand(cmd)
 				err = CreateAlertNotificationCommand(cmd)
 				So(err, ShouldNotBeNil)
 				So(err, ShouldNotBeNil)
 			})
 			})
+			Convey("Cannot save Alert Notification with the same name and another uid", func() {
+				anotherUidCmd := &models.CreateAlertNotificationCommand{
+					Name:         cmd.Name,
+					Type:         cmd.Type,
+					OrgId:        1,
+					SendReminder: cmd.SendReminder,
+					Frequency:    cmd.Frequency,
+					Settings:     cmd.Settings,
+					Uid:          "notifier1",
+				}
+				err = CreateAlertNotificationCommand(anotherUidCmd)
+				So(err, ShouldNotBeNil)
+			})
+			Convey("Can save Alert Notification with another name and another uid", func() {
+				anotherUidCmd := &models.CreateAlertNotificationCommand{
+					Name:         "another ops",
+					Type:         cmd.Type,
+					OrgId:        1,
+					SendReminder: cmd.SendReminder,
+					Frequency:    cmd.Frequency,
+					Settings:     cmd.Settings,
+					Uid:          "notifier2",
+				}
+				err = CreateAlertNotificationCommand(anotherUidCmd)
+				So(err, ShouldBeNil)
+			})
 
 
 			Convey("Can update alert notification", func() {
 			Convey("Can update alert notification", func() {
 				newCmd := &models.UpdateAlertNotificationCommand{
 				newCmd := &models.UpdateAlertNotificationCommand{
@@ -274,12 +301,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
 			So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil)
 			So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil)
 
 
 			Convey("search", func() {
 			Convey("search", func() {
-				query := &models.GetAlertNotificationsToSendQuery{
-					Ids:   []int64{cmd1.Result.Id, cmd2.Result.Id, 112341231},
+				query := &models.GetAlertNotificationsWithUidToSendQuery{
+					Uids:  []string{cmd1.Result.Uid, cmd2.Result.Uid, "112341231"},
 					OrgId: 1,
 					OrgId: 1,
 				}
 				}
 
 
-				err := GetAlertNotificationsToSend(query)
+				err := GetAlertNotificationsWithUidToSend(query)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 				So(len(query.Result), ShouldEqual, 3)
 				So(len(query.Result), ShouldEqual, 3)
 			})
 			})

+ 17 - 0
pkg/services/sqlstore/migrations/alert_mig.go

@@ -137,4 +137,21 @@ func addAlertMigrations(mg *Migrator) {
 	mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{
 	mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{
 		Name: "for", Type: DB_BigInt, Nullable: true,
 		Name: "for", Type: DB_BigInt, Nullable: true,
 	}))
 	}))
+
+	mg.AddMigration("Add column uid in alert_notification", NewAddColumnMigration(alert_notification, &Column{
+		Name: "uid", Type: DB_NVarchar, Length: 40, Nullable: true,
+	}))
+
+	mg.AddMigration("Update uid column values in alert_notification", new(RawSqlMigration).
+		Sqlite("UPDATE alert_notification SET uid=printf('%09d',id) WHERE uid IS NULL;").
+		Postgres("UPDATE alert_notification SET uid=lpad('' || id,9,'0') WHERE uid IS NULL;").
+		Mysql("UPDATE alert_notification SET uid=lpad(id,9,'0') WHERE uid IS NULL;"))
+
+	mg.AddMigration("Add unique index alert_notification_org_id_uid", NewAddIndexMigration(alert_notification, &Index{
+		Cols: []string{"org_id", "uid"}, Type: UniqueIndex,
+	}))
+
+	mg.AddMigration("Remove unique index org_id_name", NewDropIndexMigration(alert_notification, &Index{
+		Cols: []string{"org_id", "name"}, Type: UniqueIndex,
+	}))
 }
 }

+ 19 - 5
public/app/features/alerting/AlertTabCtrl.ts

@@ -140,8 +140,13 @@ export class AlertTabCtrl {
       name: model.name,
       name: model.name,
       iconClass: this.getNotificationIcon(model.type),
       iconClass: this.getNotificationIcon(model.type),
       isDefault: false,
       isDefault: false,
+      uid: model.uid
     });
     });
-    this.alert.notifications.push({ id: model.id });
+
+    // avoid duplicates using both id and uid to be backwards compatible.
+    if (!_.find(this.alert.notifications, n => n.id === model.id || n.uid === model.uid)) {
+      this.alert.notifications.push({ uid: model.uid });
+    }
 
 
     // reset plus button
     // reset plus button
     this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value;
     this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value;
@@ -149,9 +154,11 @@ export class AlertTabCtrl {
     this.addNotificationSegment.fake = true;
     this.addNotificationSegment.fake = true;
   }
   }
 
 
-  removeNotification(index) {
-    this.alert.notifications.splice(index, 1);
-    this.alertNotifications.splice(index, 1);
+  removeNotification(an) {
+    // remove notifiers refeered to by id and uid to support notifiers added
+    // before and after we added support for uid
+    _.remove(this.alert.notifications, n =>  n.uid === an.uid || n.id === an.id);
+    _.remove(this.alertNotifications, n =>  n.uid === an.uid || n.id === an.id);
   }
   }
 
 
   initModel() {
   initModel() {
@@ -187,7 +194,14 @@ export class AlertTabCtrl {
     ThresholdMapper.alertToGraphThresholds(this.panel);
     ThresholdMapper.alertToGraphThresholds(this.panel);
 
 
     for (const addedNotification of alert.notifications) {
     for (const addedNotification of alert.notifications) {
-      const model = _.find(this.notifications, { id: addedNotification.id });
+      // lookup notifier type by uid
+      let model = _.find(this.notifications, { uid: addedNotification.uid });
+
+      // fallback to using id if uid is missing
+      if (!model) {
+        model = _.find(this.notifications, { id: addedNotification.id });
+      }
+
       if (model && model.isDefault === false) {
       if (model && model.isDefault === false) {
         model.iconClass = this.getNotificationIcon(model.type);
         model.iconClass = this.getNotificationIcon(model.type);
         this.alertNotifications.push(model);
         this.alertNotifications.push(model);

+ 1 - 1
public/app/features/alerting/partials/alert_tab.html

@@ -135,7 +135,7 @@
         <div class="gf-form" ng-repeat="nc in ctrl.alertNotifications">
         <div class="gf-form" ng-repeat="nc in ctrl.alertNotifications">
           <span class="gf-form-label" ng-style="{'background-color': nc.bgColor }">
           <span class="gf-form-label" ng-style="{'background-color': nc.bgColor }">
             <i class="{{nc.iconClass}}"></i>&nbsp;{{nc.name}}&nbsp;
             <i class="{{nc.iconClass}}"></i>&nbsp;{{nc.name}}&nbsp;
-            <i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
+            <i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification(nc)" ng-if="nc.isDefault === false"></i>
           </span>
           </span>
         </div>
         </div>
         <div class="gf-form">
         <div class="gf-form">