浏览代码

Added alert_notification configuration

Pavel Bakulev 7 年之前
父节点
当前提交
6e3e9a337d

+ 20 - 0
conf/provisioning/alert_notifications/sample.yaml

@@ -0,0 +1,20 @@
+# # config file version
+apiVersion: 1
+
+# alert_notifications:
+#   - name: default-slack
+#     type: slack
+#     org_id: 1
+#     is_default: true
+#     settings:
+#       recipient: "XXX"
+#       token: "xoxb"
+#       uploadImage: true
+#   - name: default-email
+#     type: email
+#     org_id: 1
+#     is_default: false  
+# delete_alert_notifications:
+#   - name: default-slack
+#     org_id: 1  
+#   - name: default-email

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

@@ -230,4 +230,182 @@ By default Grafana will delete dashboards in the database if the file is removed
 > **Note.** Provisioning allows you to overwrite existing dashboards
 > 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
+<<<<<<< HEAD
 > or `uid` within the same installation as this will cause weird behaviors.
+=======
+> or `uid` within the same installation as this will cause weird behaviours.
+
+## Alert Notification Channels
+
+Alert Notification Channels can be provisionned by adding one or more yaml config files in the [`provisioning/alert_notifications`](/installation/configuration/#provisioning) directory.
+
+Each config file can contain the following top-level fields:
+- `alert_notifications`, 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_alert_notifications`, a list of alert notifications to be deleted before before inserting/updating those in the `alert_notifications` list.
+
+Provisionning looks up alert notifications by name, and will update any existing notification with the provided name.
+
+By default, exporting a dashboard as JSON will use a sequential identifier to refer to alert notifications. The field `name` can be optionally specified to specify a string identifier for the alert name.
+
+```json
+{
+  ...
+      "alert": {
+        ...,
+        "conditions": [...],
+        "frequency": "24h",
+        "noDataState": "ok",
+        "notifications": [
+           {"name": "notification-channel-1"},
+           {"name": "notification-channel-2"},
+        ]
+      }
+  ...
+}
+```
+
+### Example Alert Notification Channels Config File
+
+```yaml
+alert_notifications:
+  - name: notification-channel-1
+    type: slack
+    org_id: 2
+    is_default: true
+    # See `Supported Settings` section for settings supporter for each
+    # alert notification type.
+    settings:
+      recipient: "XXX"
+      token: "xoxb"
+      uploadImage: true
+
+delete_alert_notifications:
+  - name: notification-channel-1
+    org_id: 2
+  - name: notification-channel-2
+```
+
+### 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 |
+>>>>>>> Added alert_notification configuration

+ 16 - 2
pkg/services/alerting/rule.go

@@ -7,6 +7,8 @@ import (
 	"strconv"
 	"time"
 
+	"github.com/grafana/grafana/pkg/bus"
+
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
 )
@@ -128,9 +130,21 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
 		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}
+			notificationName, notificationNameErr := jsonModel.Get("name").String()
+			if notificationNameErr != nil {
+				return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
+			}
+			cmd := &m.GetAlertNotificationsQuery{OrgId: ruleDef.OrgId, Name: notificationName}
+			nameErr := bus.Dispatch(cmd)
+			if nameErr != nil || cmd.Result == nil {
+				errorMsg := fmt.Sprintf("Cannot lookup notification by name %s and orgId %d", notificationName, ruleDef.OrgId)
+				return nil, ValidationError{Reason: errorMsg, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
+			}
+			model.Notifications = append(model.Notifications, cmd.Result.Id)
+		} else {
+			model.Notifications = append(model.Notifications, id)
 		}
-		model.Notifications = append(model.Notifications, id)
+
 	}
 
 	for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {

+ 156 - 0
pkg/services/alerting/rule_test.go

@@ -3,11 +3,17 @@ package alerting
 import (
 	"testing"
 
+	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
+var (
+	fakeRepo *fakeRepository
+)
+
 type FakeCondition struct{}
 
 func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) {
@@ -129,5 +135,155 @@ func TestAlertRuleModel(t *testing.T) {
 			So(err, ShouldBeNil)
 			So(alertRule.Frequency, ShouldEqual, 60)
 		})
+		Convey("can construct alert rule model mixed notification ids and names", func() {
+			json := `
+			{
+				"name": "name2",
+				"description": "desc2",
+				"handler": 0,
+				"noDataMode": "critical",
+				"enabled": true,
+				"frequency": "60s",
+        "conditions": [
+          {
+            "type": "test",
+            "prop": 123
+					}
+        ],
+        "notifications": [
+					{"id": 1134},
+					{"id": 22},
+					{"name": "channel1"},
+					{"name": "channel2"}
+				]
+			}
+			`
+
+			alertJSON, jsonErr := simplejson.NewJson([]byte(json))
+			So(jsonErr, ShouldBeNil)
+
+			alert := &m.Alert{
+				Id:          1,
+				OrgId:       1,
+				DashboardId: 1,
+				PanelId:     1,
+
+				Settings: alertJSON,
+			}
+
+			fakeRepo = &fakeRepository{}
+			bus.ClearBusHandlers()
+			bus.AddHandler("test", mockGet)
+
+			fakeRepo.loadAll = []*models.AlertNotification{
+				{Name: "channel1", OrgId: 1, Id: 1},
+				{Name: "channel2", OrgId: 1, Id: 2},
+			}
+
+			alertRule, err := NewRuleFromDBAlert(alert)
+			So(err, ShouldBeNil)
+
+			Convey("Can read notifications", func() {
+				So(len(alertRule.Notifications), ShouldEqual, 4)
+				So(alertRule.Notifications, ShouldResemble, []int64{1134, 22, 1, 2})
+			})
+		})
+
+		Convey("raise error in case of left id", func() {
+			json := `
+			{
+				"name": "name2",
+				"description": "desc2",
+				"handler": 0,
+				"noDataMode": "critical",
+				"enabled": true,
+				"frequency": "60s",
+        "conditions": [
+          {
+            "type": "test",
+            "prop": 123
+					}
+        ],
+        "notifications": [
+					{"not_id": 1134}
+				]
+			}
+			`
+
+			alertJSON, jsonErr := simplejson.NewJson([]byte(json))
+			So(jsonErr, ShouldBeNil)
+
+			alert := &m.Alert{
+				Id:          1,
+				OrgId:       1,
+				DashboardId: 1,
+				PanelId:     1,
+
+				Settings: alertJSON,
+			}
+
+			_, err := NewRuleFromDBAlert(alert)
+			So(err, ShouldNotBeNil)
+		})
+
+		Convey("raise error in case of left id but existed name with alien orgId", func() {
+			json := `
+			{
+				"name": "name2",
+				"description": "desc2",
+				"handler": 0,
+				"noDataMode": "critical",
+				"enabled": true,
+				"frequency": "60s",
+        "conditions": [
+          {
+            "type": "test",
+            "prop": 123
+					}
+        ],
+        "notifications": [
+					{"not_id": 1134, "name": "channel1"}
+				]
+			}
+			`
+
+			alertJSON, jsonErr := simplejson.NewJson([]byte(json))
+			So(jsonErr, ShouldBeNil)
+
+			alert := &m.Alert{
+				Id:          1,
+				OrgId:       1,
+				DashboardId: 1,
+				PanelId:     1,
+
+				Settings: alertJSON,
+			}
+
+			fakeRepo = &fakeRepository{}
+			bus.ClearBusHandlers()
+			bus.AddHandler("test", mockGet)
+
+			fakeRepo.loadAll = []*models.AlertNotification{
+				{Name: "channel1", OrgId: 2, Id: 1},
+			}
+
+			_, err := NewRuleFromDBAlert(alert)
+
+			So(err, ShouldNotBeNil)
+		})
 	})
 }
+
+type fakeRepository struct {
+	loadAll []*models.AlertNotification
+}
+
+func mockGet(cmd *models.GetAlertNotificationsQuery) error {
+	for _, v := range fakeRepo.loadAll {
+		if cmd.Name == v.Name && cmd.OrgId == v.OrgId {
+			cmd.Result = v
+			return nil
+		}
+	}
+	return nil
+}

+ 151 - 0
pkg/services/provisioning/alert_notifications/alert_notifications.go

@@ -0,0 +1,151 @@
+package alert_notifications
+
+import (
+	"errors"
+
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"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")
+	ErrInvalidNotifierType         = errors.New("Unknown notifier type")
+)
+
+func Provision(configDirectory string) error {
+	dc := newNotificationProvisioner(log.New("provisioning.alert_notifications"))
+	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)
+
+		getNotification := &models.GetAlertNotificationsQuery{Name: notification.Name, OrgId: notification.OrgId}
+
+		if err := bus.Dispatch(getNotification); err != nil {
+			return err
+		}
+
+		if getNotification.Result != nil {
+			cmd := &models.DeleteAlertNotificationCommand{Id: getNotification.Result.Id, 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 {
+		cmd := &models.GetAlertNotificationsQuery{OrgId: notification.OrgId, Name: notification.Name}
+		err := bus.Dispatch(cmd)
+		if err != nil {
+			return err
+		}
+
+		settings := simplejson.New()
+		if len(notification.Settings) > 0 {
+			for k, v := range notification.Settings {
+				settings.Set(k, v)
+			}
+		}
+
+		if cmd.Result == nil {
+			dc.log.Info("Inserting alert notification from configuration ", "name", notification.Name)
+			insertCmd := &models.CreateAlertNotificationCommand{
+				Name:      notification.Name,
+				Type:      notification.Type,
+				IsDefault: notification.IsDefault,
+				Settings:  settings,
+				OrgId:     notification.OrgId,
+			}
+			if err := bus.Dispatch(insertCmd); err != nil {
+				return err
+			}
+		} else {
+			dc.log.Info("Updating alert notification from configuration", "name", notification.Name)
+			updateCmd := &models.UpdateAlertNotificationCommand{
+				Id:        cmd.Result.Id,
+				Name:      notification.Name,
+				Type:      notification.Type,
+				IsDefault: notification.IsDefault,
+				Settings:  settings,
+				OrgId:     notification.OrgId,
+			}
+			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{
+			OrgId:     notification.OrgId,
+			Name:      notification.Name,
+			Type:      notification.Type,
+			IsDefault: notification.IsDefault,
+			Settings:  notification.Settings,
+		})
+	}
+
+	for _, notification := range cfg.DeleteNotifications {
+		r.DeleteNotifications = append(r.DeleteNotifications, &deleteNotificationConfig{
+			OrgId: notification.OrgId,
+			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
+}

+ 115 - 0
pkg/services/provisioning/alert_notifications/config_reader.go

@@ -0,0 +1,115 @@
+package alert_notifications
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/grafana/grafana/pkg/log"
+	"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")
+	err = validateDefaultUniqueness(notifications)
+	if err != nil {
+		return nil, err
+	}
+
+	err = validateType(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 validateDefaultUniqueness(notifications []*notificationsAsConfig) error {
+	for i := range notifications {
+		for _, notification := range notifications[i].Notifications {
+			if notification.OrgId < 1 {
+				notification.OrgId = 1
+			}
+		}
+
+		for _, notification := range notifications[i].DeleteNotifications {
+			if notification.OrgId < 1 {
+				notification.OrgId = 1
+			}
+		}
+	}
+
+	return nil
+}
+
+func validateType(notifications []*notificationsAsConfig) error {
+	notifierTypes := alerting.GetNotifiers()
+
+	for i := range notifications {
+		if notifications[i].Notifications == nil {
+			continue
+		}
+
+		for _, notification := range notifications[i].Notifications {
+			foundNotifier := false
+
+			for _, notifier := range notifierTypes {
+				if notifier.Type == notification.Type {
+					foundNotifier = true
+					break
+				}
+			}
+
+			if !foundNotifier {
+				return ErrInvalidNotifierType
+			}
+		}
+	}
+
+	return nil
+}

+ 219 - 0
pkg/services/provisioning/alert_notifications/config_reader_test.go

@@ -0,0 +1,219 @@
+package alert_notifications
+
+import (
+	"testing"
+
+	"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"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+var (
+	logger = log.New("fake.log")
+
+	correct_properties        = "./test-configs/correct-properties"
+	brokenYaml                = "./test-configs/broken-yaml"
+	doubleNotificationsConfig = "./test-configs/double-default"
+	emptyFolder               = "./test-configs/empty_folder"
+	emptyFile                 = "./test-configs/empty"
+	twoNotificationsConfig    = "./test-configs/two-notifications"
+	unknownNotifier           = "./test-configs/unknown-notifier"
+
+	fakeRepo *fakeRepository
+)
+
+func TestNotificationAsConfig(t *testing.T) {
+	Convey("Testing notification as configuration", t, func() {
+		fakeRepo = &fakeRepository{}
+		bus.ClearBusHandlers()
+		bus.AddHandler("test", mockDelete)
+		bus.AddHandler("test", mockInsert)
+		bus.AddHandler("test", mockUpdate)
+		bus.AddHandler("test", mockGet)
+
+		alerting.RegisterNotifier(&alerting.NotifierPlugin{
+			Type: "slack",
+			Name: "slack",
+		})
+		alerting.RegisterNotifier(&alerting.NotifierPlugin{
+			Type: "email",
+			Name: "email",
+		})
+		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.IsDefault, ShouldBeTrue)
+			So(nt.Settings, ShouldResemble, map[string]interface{}{
+				"recipient": "XXX", "token": "xoxb", "uploadImage": true,
+			})
+
+			nt = nts[1]
+			So(nt.Name, ShouldEqual, "another-not-default-notification")
+			So(nt.Type, ShouldEqual, "email")
+			So(nt.OrgId, ShouldEqual, 3)
+			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.IsDefault, ShouldBeFalse)
+
+			nt = nts[3]
+			So(nt.Name, ShouldEqual, "Added notification with whitespaces in name")
+			So(nt.Type, ShouldEqual, "email")
+			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.OrgId, ShouldEqual, 2)
+
+			deleteNt = deleteNts[1]
+			So(deleteNt.Name, ShouldEqual, "deleted-notification-without-orgId")
+			So(deleteNt.OrgId, ShouldEqual, 1)
+
+			deleteNt = deleteNts[2]
+			So(deleteNt.Name, ShouldEqual, "deleted-notification-with-0-orgId")
+			So(deleteNt.OrgId, ShouldEqual, 1)
+
+			deleteNt = deleteNts[3]
+			So(deleteNt.Name, ShouldEqual, "Deleted notification with whitespaces in name")
+			So(deleteNt.OrgId, ShouldEqual, 1)
+		})
+
+		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)
+				}
+				So(len(fakeRepo.deleted), ShouldEqual, 0)
+				So(len(fakeRepo.inserted), ShouldEqual, 2)
+				So(len(fakeRepo.updated), ShouldEqual, 0)
+			})
+			Convey("One notification in database with same name", func() {
+				fakeRepo.loadAll = []*models.AlertNotification{
+					{Name: "channel1", OrgId: 1, Id: 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)
+					}
+					So(len(fakeRepo.deleted), ShouldEqual, 0)
+					So(len(fakeRepo.inserted), ShouldEqual, 1)
+					So(len(fakeRepo.updated), ShouldEqual, 1)
+				})
+			})
+			Convey("Two notifications with is_default", func() {
+				dc := newNotificationProvisioner(logger)
+				err := dc.applyChanges(doubleNotificationsConfig)
+				Convey("should both be inserted", func() {
+					So(err, ShouldBeNil)
+					So(len(fakeRepo.deleted), ShouldEqual, 0)
+					So(len(fakeRepo.inserted), ShouldEqual, 2)
+					So(len(fakeRepo.updated), ShouldEqual, 0)
+				})
+			})
+		})
+
+		Convey("Two configured notification", func() {
+			Convey("two other notifications in database", func() {
+				fakeRepo.loadAll = []*models.AlertNotification{
+					{Name: "channel1", OrgId: 1, Id: 1},
+					{Name: "channel3", OrgId: 1, Id: 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)
+					}
+					So(len(fakeRepo.deleted), ShouldEqual, 0)
+					So(len(fakeRepo.inserted), ShouldEqual, 1)
+					So(len(fakeRepo.updated), ShouldEqual, 1)
+				})
+			})
+		})
+		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)
+				}
+				So(len(fakeRepo.deleted), ShouldEqual, 0)
+				So(len(fakeRepo.inserted), ShouldEqual, 0)
+				So(len(fakeRepo.updated), ShouldEqual, 0)
+			})
+		})
+		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, ShouldEqual, ErrInvalidNotifierType)
+		})
+
+	})
+}
+
+type fakeRepository struct {
+	inserted []*models.CreateAlertNotificationCommand
+	deleted  []*models.DeleteAlertNotificationCommand
+	updated  []*models.UpdateAlertNotificationCommand
+	loadAll  []*models.AlertNotification
+}
+
+func mockDelete(cmd *models.DeleteAlertNotificationCommand) error {
+	fakeRepo.deleted = append(fakeRepo.deleted, cmd)
+	return nil
+}
+func mockUpdate(cmd *models.UpdateAlertNotificationCommand) error {
+	fakeRepo.updated = append(fakeRepo.updated, cmd)
+	return nil
+}
+func mockInsert(cmd *models.CreateAlertNotificationCommand) error {
+	fakeRepo.inserted = append(fakeRepo.inserted, cmd)
+	return nil
+}
+func mockGet(cmd *models.GetAlertNotificationsQuery) error {
+	for _, v := range fakeRepo.loadAll {
+		if cmd.Name == v.Name && cmd.OrgId == v.OrgId {
+			cmd.Result = v
+			return nil
+		}
+	}
+	return nil
+}

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

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

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

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

+ 26 - 0
pkg/services/provisioning/alert_notifications/test-configs/correct-properties/correct-properties.yaml

@@ -0,0 +1,26 @@
+alert_notifications:
+  - name: default-slack-notification
+    type: slack
+    org_id: 2
+    is_default: true
+    settings:
+      recipient: "XXX"
+      token: "xoxb"
+      uploadImage: true
+  - name: another-not-default-notification
+    type: email
+    org_id: 3
+    is_default: false
+  - name: check-unset-is_default-is-false
+    type: slack
+    org_id: 3
+  - name: Added notification with whitespaces in name
+    type: email
+    org_id: 3
+delete_alert_notifications:
+  - name: default-slack-notification
+    org_id: 2
+  - name: deleted-notification-without-orgId
+  - name: deleted-notification-with-0-orgId
+    org_id: 0
+  - name: Deleted notification with whitespaces in name

+ 4 - 0
pkg/services/provisioning/alert_notifications/test-configs/double-default/default-1.yml

@@ -0,0 +1,4 @@
+alert_notifications:
+  - name: first-default
+    type: slack
+    is_default: true

+ 4 - 0
pkg/services/provisioning/alert_notifications/test-configs/double-default/default-2.yaml

@@ -0,0 +1,4 @@
+alert_notifications:
+  - name: second-default
+    type: email
+    is_default: true

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


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

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

+ 5 - 0
pkg/services/provisioning/alert_notifications/test-configs/two-notifications/two-notifications.yaml

@@ -0,0 +1,5 @@
+alert_notifications:
+  - name: channel1
+    type: slack
+  - name: channel2
+    type: email

+ 3 - 0
pkg/services/provisioning/alert_notifications/test-configs/unknown-notifier/notification.yaml

@@ -0,0 +1,3 @@
+alert_notifications:
+  - name: unknown-notifier
+    type: nonexisting

+ 19 - 0
pkg/services/provisioning/alert_notifications/types.go

@@ -0,0 +1,19 @@
+package alert_notifications
+
+type notificationsAsConfig struct {
+	Notifications       []*notificationFromConfig   `json:"alert_notifications" yaml:"alert_notifications"`
+	DeleteNotifications []*deleteNotificationConfig `json:"delete_alert_notifications" yaml:"delete_alert_notifications"`
+}
+
+type deleteNotificationConfig struct {
+	Name  string `json:"name" yaml:"name"`
+	OrgId int64  `json:"org_id" yaml:"org_id"`
+}
+
+type notificationFromConfig struct {
+	OrgId     int64                  `json:"org_id" yaml:"org_id"`
+	Name      string                 `json:"name" yaml:"name"`
+	Type      string                 `json:"type" yaml:"type"`
+	IsDefault bool                   `json:"is_default" yaml:"is_default"`
+	Settings  map[string]interface{} `json:"settings" yaml:"settings"`
+}

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

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