浏览代码

dashboards as cfg: read first cfg version

bergquist 8 年之前
父节点
当前提交
d69b63cbc0

+ 6 - 0
conf/dashboards/dashboards.yaml

@@ -0,0 +1,6 @@
+- name: 'default'
+  org_id: 1
+  folder: ''
+  type: file
+  options:
+    folder: /var/lib/grafana/dashboards

+ 3 - 0
conf/defaults.ini

@@ -23,6 +23,9 @@ plugins = data/plugins
 # Config files containing datasources that will be configured at startup
 datasources = conf/datasources
 
+# Config files containing folders to read dashboards from and insert into the database.
+dashboards = conf/dashboards
+
 #################################### Server ##############################
 [server]
 # Protocol (http, https, socket)

+ 3 - 0
conf/sample.ini

@@ -23,6 +23,9 @@
 # Config files containing datasources that will be configured at startup
 ;datasources = conf/datasources
 
+# Config files containing folders to read dashboards from and insert into the database.
+;dashboards = conf/dashboards
+
 #################################### Server ####################################
 [server]
 # Protocol (http, https, socket)

+ 1 - 1
pkg/cmd/grafana-server/server.go

@@ -68,7 +68,7 @@ func (g *GrafanaServerImpl) Start() {
 	social.NewOAuthService()
 	plugins.Init()
 
-	if err := provisioning.StartUp(setting.DatasourcesPath); err != nil {
+	if err := provisioning.Init(g.context, setting.HomePath, setting.Cfg); err != nil {
 		logger.Error("Failed to provision Grafana from config", "error", err)
 		g.Shutdown(1, "Startup failed")
 		return

+ 8 - 5
pkg/models/dashboards.go

@@ -11,11 +11,12 @@ import (
 
 // Typed errors
 var (
-	ErrDashboardNotFound           = errors.New("Dashboard not found")
-	ErrDashboardSnapshotNotFound   = errors.New("Dashboard snapshot not found")
-	ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
-	ErrDashboardVersionMismatch    = errors.New("The dashboard has been changed by someone else")
-	ErrDashboardTitleEmpty         = errors.New("Dashboard title cannot be empty")
+	ErrDashboardNotFound                 = errors.New("Dashboard not found")
+	ErrDashboardSnapshotNotFound         = errors.New("Dashboard snapshot not found")
+	ErrDashboardWithSameNameExists       = errors.New("A dashboard with the same name already exists")
+	ErrDashboardVersionMismatch          = errors.New("The dashboard has been changed by someone else")
+	ErrDashboardTitleEmpty               = errors.New("Dashboard title cannot be empty")
+	ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
 )
 
 type UpdatePluginDashboardError struct {
@@ -139,6 +140,8 @@ type SaveDashboardCommand struct {
 	RestoredFrom int              `json:"-"`
 	PluginId     string           `json:"-"`
 
+	UpdatedAt time.Time
+
 	Result *Dashboard
 }
 

+ 49 - 0
pkg/services/provisioning/dashboard/config_reader.go

@@ -0,0 +1,49 @@
+package dashboard
+
+import (
+	"io/ioutil"
+	"path/filepath"
+	"strings"
+
+	yaml "gopkg.in/yaml.v2"
+)
+
+type configReader struct {
+	path string
+}
+
+func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
+	files, err := ioutil.ReadDir(cr.path)
+	if err != nil {
+		return nil, err
+	}
+
+	var dashboards []*DashboardsAsConfig
+	for _, file := range files {
+		if !strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml") {
+			continue
+		}
+
+		filename, _ := filepath.Abs(filepath.Join(cr.path, file.Name()))
+		yamlFile, err := ioutil.ReadFile(filename)
+		if err != nil {
+			return nil, err
+		}
+
+		var datasource []*DashboardsAsConfig
+		err = yaml.Unmarshal(yamlFile, &datasource)
+		if err != nil {
+			return nil, err
+		}
+
+		dashboards = append(dashboards, datasource...)
+	}
+
+	for i := range dashboards {
+		if dashboards[i].OrgId == 0 {
+			dashboards[i].OrgId = 1
+		}
+	}
+
+	return dashboards, nil
+}

+ 51 - 0
pkg/services/provisioning/dashboard/dashboard.go

@@ -0,0 +1,51 @@
+package dashboard
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/grafana/grafana/pkg/log"
+)
+
+type DashboardProvisioner struct {
+	cfgReader *configReader
+	log       log.Logger
+	ctx       context.Context
+}
+
+func Provision(ctx context.Context, configDirectory string) (*DashboardProvisioner, error) {
+	d := &DashboardProvisioner{
+		cfgReader: &configReader{path: configDirectory},
+		log:       log.New("provisioning.dashboard"),
+		ctx:       ctx,
+	}
+
+	return d, d.Init(ctx)
+}
+
+func (provider *DashboardProvisioner) Init(ctx context.Context) error {
+	cfgs, err := provider.cfgReader.readConfig()
+	if err != nil {
+		return err
+	}
+
+	for _, cfg := range cfgs {
+		if cfg.Type == "file" {
+			fileReader, err := NewDashboardFilereader(cfg, provider.log.New("type", cfg.Type, "name", cfg.Name))
+			if err != nil {
+				return err
+			}
+
+			// err = fileReader.Init()
+			// if err != nil {
+			// 	provider.log.Error("Failed to load dashboards", "error", err)
+			// }
+
+			go fileReader.Listen(ctx)
+		} else {
+			return fmt.Errorf("type %s is not supported", cfg.Type)
+		}
+	}
+
+	return nil
+}

+ 47 - 0
pkg/services/provisioning/dashboard/dashboard_test.go

@@ -0,0 +1,47 @@
+package dashboard
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+var (
+	simpleDashboardConfig string = "./test-configs/dashboards-from-disk"
+)
+
+func TestDashboardsAsConfig(t *testing.T) {
+	Convey("Dashboards as configuration", t, func() {
+
+		Convey("Can read config file", func() {
+
+			cfgProvifer := configReader{path: simpleDashboardConfig}
+			cfg, err := cfgProvifer.readConfig()
+			if err != nil {
+				t.Fatalf("readConfig return an error %v", err)
+			}
+
+			So(len(cfg), ShouldEqual, 2)
+
+			ds := cfg[0]
+
+			So(ds.Name, ShouldEqual, "general dashboards")
+			So(ds.Type, ShouldEqual, "file")
+			So(ds.OrgId, ShouldEqual, 2)
+			So(ds.Folder, ShouldEqual, "developers")
+
+			So(len(ds.Options), ShouldEqual, 1)
+			So(ds.Options["folder"], ShouldEqual, "/var/lib/grafana/dashboards")
+
+			ds2 := cfg[1]
+
+			So(ds2.Name, ShouldEqual, "default")
+			So(ds2.Type, ShouldEqual, "file")
+			So(ds2.OrgId, ShouldEqual, 1)
+			So(ds2.Folder, ShouldEqual, "")
+
+			So(len(ds2.Options), ShouldEqual, 1)
+			So(ds2.Options["folder"], ShouldEqual, "/var/lib/grafana/dashboards")
+		})
+	})
+}

+ 212 - 0
pkg/services/provisioning/dashboard/file_reader.go

@@ -0,0 +1,212 @@
+package dashboard
+
+import (
+	"context"
+	"fmt"
+	"github.com/grafana/grafana/pkg/services/alerting"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+
+	"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"
+)
+
+type fileReader struct {
+	Cfg            *DashboardsAsConfig
+	Path           string
+	log            log.Logger
+	dashboardCache *dashboardCache
+}
+
+type dashboardCache struct {
+	mutex      *sync.Mutex
+	dashboards map[string]*DashboardJson
+}
+
+func newDashboardCache() *dashboardCache {
+	return &dashboardCache{
+		dashboards: map[string]*DashboardJson{},
+		mutex:      &sync.Mutex{},
+	}
+}
+
+func (dc *dashboardCache) addCache(json *DashboardJson) {
+	dc.mutex.Lock()
+	defer dc.mutex.Unlock()
+	dc.dashboards[json.Path] = json
+}
+
+func (dc *dashboardCache) getCache(path string) (*DashboardJson, bool) {
+	dc.mutex.Lock()
+	defer dc.mutex.Unlock()
+	v, exist := dc.dashboards[path]
+	return v, exist
+}
+
+func NewDashboardFilereader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
+	path, ok := cfg.Options["folder"].(string)
+	if !ok {
+		return nil, fmt.Errorf("Failed to load dashboards. folder param is not a string")
+	}
+
+	if _, err := os.Stat(path); os.IsNotExist(err) {
+		log.Error("Cannot read directory", "error", err)
+	}
+
+	return &fileReader{
+		Cfg:            cfg,
+		Path:           path,
+		log:            log,
+		dashboardCache: newDashboardCache(),
+	}, nil
+}
+
+func (fr *fileReader) Listen(ctx context.Context) error {
+	ticker := time.NewTicker(time.Second * 1)
+
+	if err := fr.walkFolder(); err != nil {
+		fr.log.Error("failed to search for dashboards", "error", err)
+	}
+
+	for {
+		select {
+		case <-ticker.C:
+			fr.walkFolder()
+		case <-ctx.Done():
+			return nil
+		}
+	}
+}
+
+func (fr *fileReader) walkFolder() error {
+	if _, err := os.Stat(fr.Path); err != nil {
+		if os.IsNotExist(err) {
+			return err
+		}
+	}
+
+	return filepath.Walk(fr.Path, func(path string, f os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if f.IsDir() {
+			if strings.HasPrefix(f.Name(), ".") {
+				return filepath.SkipDir
+			}
+			return nil
+		}
+
+		if !strings.HasSuffix(f.Name(), ".json") {
+			return nil
+		}
+
+		cachedDashboard, exist := fr.dashboardCache.getCache(path)
+		if exist && cachedDashboard.ModTime == f.ModTime() {
+			return nil
+		}
+
+		dash, err := fr.readDashboardFromFile(path)
+		if err != nil {
+			fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
+			return nil
+		}
+
+		cmd := &models.GetDashboardQuery{Slug: dash.Dashboard.Slug}
+		err = bus.Dispatch(cmd)
+
+		if err == models.ErrDashboardNotFound {
+			fr.log.Debug("saving new dashboard", "file", path)
+			return fr.saveDashboard(dash)
+		}
+
+		if err != nil {
+			fr.log.Error("failed to query for dashboard", "slug", dash.Dashboard.Slug, "error", err)
+			return nil
+		}
+
+		if cmd.Result.Updated.Unix() >= f.ModTime().Unix() {
+			fr.log.Debug("already using latest version", "dashboard", dash.Dashboard.Slug)
+			return nil
+		}
+
+		fr.log.Debug("no dashboard in cache. Loading dashboard from disk into database.", "file", path)
+		return fr.saveDashboard(dash)
+	})
+}
+
+func (fr *fileReader) readDashboardFromFile(path string) (*DashboardJson, error) {
+	reader, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer reader.Close()
+
+	data, err := simplejson.NewFromReader(reader)
+	if err != nil {
+		return nil, err
+	}
+
+	stat, _ := os.Stat(path)
+	dash := &DashboardJson{}
+	dash.Dashboard = models.NewDashboardFromJson(data)
+	dash.TitleLower = strings.ToLower(dash.Dashboard.Title)
+	dash.Path = path
+	dash.ModTime = stat.ModTime()
+	dash.OrgId = fr.Cfg.OrgId
+	dash.Folder = fr.Cfg.Folder
+
+	if dash.Dashboard.Title == "" {
+		return nil, models.ErrDashboardTitleEmpty
+	}
+
+	fr.dashboardCache.addCache(dash)
+
+	return dash, nil
+}
+
+func (fr *fileReader) saveDashboard(dashboardJson *DashboardJson) error {
+	dash := dashboardJson.Dashboard
+
+	if dash.Title == "" {
+		return models.ErrDashboardTitleEmpty
+	}
+
+	validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{
+		OrgId:     dashboardJson.OrgId,
+		Dashboard: dash,
+	}
+
+	if err := bus.Dispatch(&validateAlertsCmd); err != nil {
+		return models.ErrDashboardContainsInvalidAlertData
+	}
+
+	cmd := models.SaveDashboardCommand{
+		Dashboard: dash.Data,
+		Message:   "Dashboard created from file.",
+		OrgId:     dashboardJson.OrgId,
+		Overwrite: true,
+		UpdatedAt: dashboardJson.ModTime,
+	}
+
+	err := bus.Dispatch(&cmd)
+	if err != nil {
+		return err
+	}
+
+	alertCmd := alerting.UpdateDashboardAlertsCommand{
+		OrgId:     dashboardJson.OrgId,
+		Dashboard: cmd.Result,
+	}
+
+	if err := bus.Dispatch(&alertCmd); err != nil {
+		return err
+	}
+
+	return nil
+}

+ 88 - 0
pkg/services/provisioning/dashboard/file_reader_test.go

@@ -0,0 +1,88 @@
+package dashboard
+
+import (
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/alerting"
+	"testing"
+
+	"github.com/grafana/grafana/pkg/log"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+var (
+	defaultDashboards string = "./test-dashboards/folder-one"
+	brokenDashboards  string = "./test-dashboards/broken-dashboards"
+)
+
+func TestDashboardFileReader(t *testing.T) {
+	Convey("Reading dashboards from disk", t, func() {
+		bus.ClearBusHandlers()
+
+		bus.AddHandler("test", mockGetDashboardQuery)
+		bus.AddHandler("test", mockValidateDashboardAlertsCommand)
+		bus.AddHandler("test", mockSaveDashboardCommand)
+		bus.AddHandler("test", mockUpdateDashboardAlertsCommand)
+		logger := log.New("test.logger")
+
+		Convey("Can read default dashboard", func() {
+			cfg := &DashboardsAsConfig{
+				Name:   "Default",
+				Type:   "file",
+				OrgId:  1,
+				Folder: "",
+				Options: map[string]interface{}{
+					"folder": defaultDashboards,
+				},
+			}
+			reader, err := NewDashboardFilereader(cfg, logger)
+			So(err, ShouldBeNil)
+
+			err = reader.walkFolder()
+			So(err, ShouldBeNil)
+		})
+
+		Convey("Invalid configuration should return error", func() {
+			cfg := &DashboardsAsConfig{
+				Name:   "Default",
+				Type:   "file",
+				OrgId:  1,
+				Folder: "",
+			}
+
+			_, err := NewDashboardFilereader(cfg, logger)
+			So(err, ShouldNotBeNil)
+		})
+
+		Convey("Broken dashboards should not cause error", func() {
+			cfg := &DashboardsAsConfig{
+				Name:   "Default",
+				Type:   "file",
+				OrgId:  1,
+				Folder: "",
+				Options: map[string]interface{}{
+					"folder": brokenDashboards,
+				},
+			}
+
+			_, err := NewDashboardFilereader(cfg, logger)
+			So(err, ShouldBeNil)
+		})
+	})
+}
+
+func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
+	return models.ErrDashboardNotFound
+}
+
+func mockValidateDashboardAlertsCommand(cmd *alerting.ValidateDashboardAlertsCommand) error {
+	return nil
+}
+
+func mockSaveDashboardCommand(cmd *models.SaveDashboardCommand) error {
+	return nil
+}
+
+func mockUpdateDashboardAlertsCommand(cmd *alerting.UpdateDashboardAlertsCommand) error {
+	return nil
+}

+ 11 - 0
pkg/services/provisioning/dashboard/test-configs/dashboards-from-disk/dev-dashboards.yaml

@@ -0,0 +1,11 @@
+- name: 'general dashboards'
+  org_id: 2
+  folder: 'developers'
+  type: file
+  options:
+    folder: /var/lib/grafana/dashboards
+
+- name: 'default'
+  type: file
+  options:
+    folder: /var/lib/grafana/dashboards

+ 0 - 0
pkg/services/provisioning/dashboard/test-dashboards/broken-dashboards/empty-json.json


+ 174 - 0
pkg/services/provisioning/dashboard/test-dashboards/broken-dashboards/invalid.json

@@ -0,0 +1,174 @@
+[]
+{
+    "title": "Grafana",
+    "tags": [],
+    "style": "dark",
+    "timezone": "browser",
+    "editable": true,
+    "rows": [
+      {
+        "title": "New row",
+        "height": "150px",
+        "collapse": false,
+        "editable": true,
+        "panels": [
+          {
+            "id": 1,
+            "span": 12,
+            "editable": true,
+            "type": "text",
+            "mode": "html",
+            "content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
+            "style": {},
+            "title": "Welcome to"
+          }
+        ]
+      },
+      {
+        "title": "Welcome to Grafana",
+        "height": "210px",
+        "collapse": false,
+        "editable": true,
+        "panels": [
+          {
+            "id": 2,
+            "span": 6,
+            "type": "text",
+            "mode": "html",
+            "content": "<br/>\n\n<div class=\"row-fluid\">\n  <div class=\"span6\">\n    <ul>\n      <li>\n        <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a>  (Must read!)\n      </li>\n    </ul>\n  </div>\n  <div class=\"span6\">\n    <ul>\n      <li>\n        <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n      </li>\n    </ul>\n  </div>\n</div>",
+            "style": {},
+            "title": "Documentation Links"
+          },
+          {
+            "id": 3,
+            "span": 6,
+            "type": "text",
+            "mode": "html",
+            "content": "<br/>\n\n<div class=\"row-fluid\">\n  <div class=\"span12\">\n    <ul>\n      <li>Ctrl+S saves the current dashboard</li>\n      <li>Ctrl+F Opens the dashboard finder</li>\n      <li>Ctrl+H Hide/show row controls</li>\n      <li>Click and drag graph title to move panel</li>\n      <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n      <li>Click the colored icon in the legend to change series color</li>\n      <li>Ctrl or Shift + Click legend name to hide other series</li>\n    </ul>\n  </div>\n</div>\n",
+            "style": {},
+            "title": "Tips & Shortcuts"
+          }
+        ]
+      },
+      {
+        "title": "test",
+        "height": "250px",
+        "editable": true,
+        "collapse": false,
+        "panels": [
+          {
+            "id": 4,
+            "span": 12,
+            "type": "graph",
+            "x-axis": true,
+            "y-axis": true,
+            "scale": 1,
+            "y_formats": [
+              "short",
+              "short"
+            ],
+            "grid": {
+              "max": null,
+              "min": null,
+              "leftMax": null,
+              "rightMax": null,
+              "leftMin": null,
+              "rightMin": null,
+              "threshold1": null,
+              "threshold2": null,
+              "threshold1Color": "rgba(216, 200, 27, 0.27)",
+              "threshold2Color": "rgba(234, 112, 112, 0.22)"
+            },
+            "resolution": 100,
+            "lines": true,
+            "fill": 1,
+            "linewidth": 2,
+            "dashes": false,
+            "dashLength": 10,
+            "spaceLength": 10,
+            "points": false,
+            "pointradius": 5,
+            "bars": false,
+            "stack": true,
+            "spyable": true,
+            "options": false,
+            "legend": {
+              "show": true,
+              "values": false,
+              "min": false,
+              "max": false,
+              "current": false,
+              "total": false,
+              "avg": false
+            },
+            "interactive": true,
+            "legend_counts": true,
+            "timezone": "browser",
+            "percentage": false,
+            "nullPointMode": "connected",
+            "steppedLine": false,
+            "tooltip": {
+              "value_type": "cumulative",
+              "query_as_alias": true
+            },
+            "targets": [
+              {
+                "target": "randomWalk('random walk')",
+                "function": "mean",
+                "column": "value"
+              }
+            ],
+            "aliasColors": {},
+            "aliasYAxis": {},
+            "title": "First Graph (click title to edit)",
+            "datasource": "graphite",
+            "renderer": "flot",
+            "annotate": {
+              "enable": false
+            }
+          }
+        ]
+      }
+    ],
+    "nav": [
+      {
+        "type": "timepicker",
+        "collapse": false,
+        "enable": true,
+        "status": "Stable",
+        "time_options": [
+          "5m",
+          "15m",
+          "1h",
+          "6h",
+          "12h",
+          "24h",
+          "2d",
+          "7d",
+          "30d"
+        ],
+        "refresh_intervals": [
+          "5s",
+          "10s",
+          "30s",
+          "1m",
+          "5m",
+          "15m",
+          "30m",
+          "1h",
+          "2h",
+          "1d"
+        ],
+        "now": true
+      }
+    ],
+    "time": {
+      "from": "now-6h",
+      "to": "now"
+    },
+    "templating": {
+      "list": []
+    },
+    "version": 5
+  }
+  

+ 173 - 0
pkg/services/provisioning/dashboard/test-dashboards/folder-one/dashboard1.json

@@ -0,0 +1,173 @@
+{
+    "title": "Grafana",
+    "tags": [],
+    "style": "dark",
+    "timezone": "browser",
+    "editable": true,
+    "rows": [
+      {
+        "title": "New row",
+        "height": "150px",
+        "collapse": false,
+        "editable": true,
+        "panels": [
+          {
+            "id": 1,
+            "span": 12,
+            "editable": true,
+            "type": "text",
+            "mode": "html",
+            "content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
+            "style": {},
+            "title": "Welcome to"
+          }
+        ]
+      },
+      {
+        "title": "Welcome to Grafana",
+        "height": "210px",
+        "collapse": false,
+        "editable": true,
+        "panels": [
+          {
+            "id": 2,
+            "span": 6,
+            "type": "text",
+            "mode": "html",
+            "content": "<br/>\n\n<div class=\"row-fluid\">\n  <div class=\"span6\">\n    <ul>\n      <li>\n        <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a>  (Must read!)\n      </li>\n    </ul>\n  </div>\n  <div class=\"span6\">\n    <ul>\n      <li>\n        <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n      </li>\n    </ul>\n  </div>\n</div>",
+            "style": {},
+            "title": "Documentation Links"
+          },
+          {
+            "id": 3,
+            "span": 6,
+            "type": "text",
+            "mode": "html",
+            "content": "<br/>\n\n<div class=\"row-fluid\">\n  <div class=\"span12\">\n    <ul>\n      <li>Ctrl+S saves the current dashboard</li>\n      <li>Ctrl+F Opens the dashboard finder</li>\n      <li>Ctrl+H Hide/show row controls</li>\n      <li>Click and drag graph title to move panel</li>\n      <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n      <li>Click the colored icon in the legend to change series color</li>\n      <li>Ctrl or Shift + Click legend name to hide other series</li>\n    </ul>\n  </div>\n</div>\n",
+            "style": {},
+            "title": "Tips & Shortcuts"
+          }
+        ]
+      },
+      {
+        "title": "test",
+        "height": "250px",
+        "editable": true,
+        "collapse": false,
+        "panels": [
+          {
+            "id": 4,
+            "span": 12,
+            "type": "graph",
+            "x-axis": true,
+            "y-axis": true,
+            "scale": 1,
+            "y_formats": [
+              "short",
+              "short"
+            ],
+            "grid": {
+              "max": null,
+              "min": null,
+              "leftMax": null,
+              "rightMax": null,
+              "leftMin": null,
+              "rightMin": null,
+              "threshold1": null,
+              "threshold2": null,
+              "threshold1Color": "rgba(216, 200, 27, 0.27)",
+              "threshold2Color": "rgba(234, 112, 112, 0.22)"
+            },
+            "resolution": 100,
+            "lines": true,
+            "fill": 1,
+            "linewidth": 2,
+            "dashes": false,
+            "dashLength": 10,
+            "spaceLength": 10,
+            "points": false,
+            "pointradius": 5,
+            "bars": false,
+            "stack": true,
+            "spyable": true,
+            "options": false,
+            "legend": {
+              "show": true,
+              "values": false,
+              "min": false,
+              "max": false,
+              "current": false,
+              "total": false,
+              "avg": false
+            },
+            "interactive": true,
+            "legend_counts": true,
+            "timezone": "browser",
+            "percentage": false,
+            "nullPointMode": "connected",
+            "steppedLine": false,
+            "tooltip": {
+              "value_type": "cumulative",
+              "query_as_alias": true
+            },
+            "targets": [
+              {
+                "target": "randomWalk('random walk')",
+                "function": "mean",
+                "column": "value"
+              }
+            ],
+            "aliasColors": {},
+            "aliasYAxis": {},
+            "title": "First Graph (click title to edit)",
+            "datasource": "graphite",
+            "renderer": "flot",
+            "annotate": {
+              "enable": false
+            }
+          }
+        ]
+      }
+    ],
+    "nav": [
+      {
+        "type": "timepicker",
+        "collapse": false,
+        "enable": true,
+        "status": "Stable",
+        "time_options": [
+          "5m",
+          "15m",
+          "1h",
+          "6h",
+          "12h",
+          "24h",
+          "2d",
+          "7d",
+          "30d"
+        ],
+        "refresh_intervals": [
+          "5s",
+          "10s",
+          "30s",
+          "1m",
+          "5m",
+          "15m",
+          "30m",
+          "1h",
+          "2h",
+          "1d"
+        ],
+        "now": true
+      }
+    ],
+    "time": {
+      "from": "now-6h",
+      "to": "now"
+    },
+    "templating": {
+      "list": []
+    },
+    "version": 5
+  }
+  

+ 173 - 0
pkg/services/provisioning/dashboard/test-dashboards/folder-one/dashboard2.json

@@ -0,0 +1,173 @@
+{
+    "title": "Grafana",
+    "tags": [],
+    "style": "dark",
+    "timezone": "browser",
+    "editable": true,
+    "rows": [
+      {
+        "title": "New row",
+        "height": "150px",
+        "collapse": false,
+        "editable": true,
+        "panels": [
+          {
+            "id": 1,
+            "span": 12,
+            "editable": true,
+            "type": "text",
+            "mode": "html",
+            "content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
+            "style": {},
+            "title": "Welcome to"
+          }
+        ]
+      },
+      {
+        "title": "Welcome to Grafana",
+        "height": "210px",
+        "collapse": false,
+        "editable": true,
+        "panels": [
+          {
+            "id": 2,
+            "span": 6,
+            "type": "text",
+            "mode": "html",
+            "content": "<br/>\n\n<div class=\"row-fluid\">\n  <div class=\"span6\">\n    <ul>\n      <li>\n        <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a>  (Must read!)\n      </li>\n    </ul>\n  </div>\n  <div class=\"span6\">\n    <ul>\n      <li>\n        <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n      </li>\n      <li>\n        <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n      </li>\n    </ul>\n  </div>\n</div>",
+            "style": {},
+            "title": "Documentation Links"
+          },
+          {
+            "id": 3,
+            "span": 6,
+            "type": "text",
+            "mode": "html",
+            "content": "<br/>\n\n<div class=\"row-fluid\">\n  <div class=\"span12\">\n    <ul>\n      <li>Ctrl+S saves the current dashboard</li>\n      <li>Ctrl+F Opens the dashboard finder</li>\n      <li>Ctrl+H Hide/show row controls</li>\n      <li>Click and drag graph title to move panel</li>\n      <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n      <li>Click the colored icon in the legend to change series color</li>\n      <li>Ctrl or Shift + Click legend name to hide other series</li>\n    </ul>\n  </div>\n</div>\n",
+            "style": {},
+            "title": "Tips & Shortcuts"
+          }
+        ]
+      },
+      {
+        "title": "test",
+        "height": "250px",
+        "editable": true,
+        "collapse": false,
+        "panels": [
+          {
+            "id": 4,
+            "span": 12,
+            "type": "graph",
+            "x-axis": true,
+            "y-axis": true,
+            "scale": 1,
+            "y_formats": [
+              "short",
+              "short"
+            ],
+            "grid": {
+              "max": null,
+              "min": null,
+              "leftMax": null,
+              "rightMax": null,
+              "leftMin": null,
+              "rightMin": null,
+              "threshold1": null,
+              "threshold2": null,
+              "threshold1Color": "rgba(216, 200, 27, 0.27)",
+              "threshold2Color": "rgba(234, 112, 112, 0.22)"
+            },
+            "resolution": 100,
+            "lines": true,
+            "fill": 1,
+            "linewidth": 2,
+            "dashes": false,
+            "dashLength": 10,
+            "spaceLength": 10,
+            "points": false,
+            "pointradius": 5,
+            "bars": false,
+            "stack": true,
+            "spyable": true,
+            "options": false,
+            "legend": {
+              "show": true,
+              "values": false,
+              "min": false,
+              "max": false,
+              "current": false,
+              "total": false,
+              "avg": false
+            },
+            "interactive": true,
+            "legend_counts": true,
+            "timezone": "browser",
+            "percentage": false,
+            "nullPointMode": "connected",
+            "steppedLine": false,
+            "tooltip": {
+              "value_type": "cumulative",
+              "query_as_alias": true
+            },
+            "targets": [
+              {
+                "target": "randomWalk('random walk')",
+                "function": "mean",
+                "column": "value"
+              }
+            ],
+            "aliasColors": {},
+            "aliasYAxis": {},
+            "title": "First Graph (click title to edit)",
+            "datasource": "graphite",
+            "renderer": "flot",
+            "annotate": {
+              "enable": false
+            }
+          }
+        ]
+      }
+    ],
+    "nav": [
+      {
+        "type": "timepicker",
+        "collapse": false,
+        "enable": true,
+        "status": "Stable",
+        "time_options": [
+          "5m",
+          "15m",
+          "1h",
+          "6h",
+          "12h",
+          "24h",
+          "2d",
+          "7d",
+          "30d"
+        ],
+        "refresh_intervals": [
+          "5s",
+          "10s",
+          "30s",
+          "1m",
+          "5m",
+          "15m",
+          "30m",
+          "1h",
+          "2h",
+          "1d"
+        ],
+        "now": true
+      }
+    ],
+    "time": {
+      "from": "now-6h",
+      "to": "now"
+    },
+    "templating": {
+      "list": []
+    },
+    "version": 5
+  }
+  

+ 34 - 0
pkg/services/provisioning/dashboard/types.go

@@ -0,0 +1,34 @@
+package dashboard
+
+import (
+	"sync"
+	"time"
+
+	"github.com/grafana/grafana/pkg/models"
+)
+
+type DashboardsAsConfig struct {
+	Name    string                 `json:"name" yaml:"name"`
+	Type    string                 `json:"type" yaml:"type"`
+	OrgId   int64                  `json:"org_id" yaml:"org_id"`
+	Folder  string                 `json:"folder" yaml:"folder"`
+	Options map[string]interface{} `json:"options" yaml:"options"`
+}
+
+type DashboardJson struct {
+	TitleLower string
+	Path       string
+	OrgId      int64
+	Folder     string
+	ModTime    time.Time
+	Dashboard  *models.Dashboard
+}
+
+type DashboardIndex struct {
+	mutex *sync.Mutex
+
+	PathToDashboard map[string]*DashboardJson
+}
+
+type InsertDashboard func(cmd *models.Dashboard) error
+type UpdateDashboard func(cmd *models.SaveDashboardCommand) error

+ 35 - 2
pkg/services/provisioning/provisioning.go

@@ -1,14 +1,47 @@
 package provisioning
 
 import (
+	"context"
+	"path/filepath"
+
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/services/provisioning/dashboard"
 	"github.com/grafana/grafana/pkg/services/provisioning/datasources"
+	ini "gopkg.in/ini.v1"
 )
 
 var (
 	logger log.Logger = log.New("services.provisioning")
 )
 
-func StartUp(datasourcePath string) error {
-	return datasources.Provision(datasourcePath)
+type Provisioner struct {
+	datasourcePath string
+	dashboardPath  string
+	bgContext      context.Context
+}
+
+func Init(backgroundContext context.Context, homePath string, cfg *ini.File) error {
+	datasourcePath := makeAbsolute(cfg.Section("paths").Key("datasources").String(), homePath)
+	if err := datasources.Provision(datasourcePath); err != nil {
+		return err
+	}
+
+	dashboardPath := makeAbsolute(cfg.Section("paths").Key("dashboards").String(), homePath)
+	_, err := dashboard.Provision(backgroundContext, dashboardPath)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (p *Provisioner) Listen() error {
+	return nil
+}
+
+func makeAbsolute(path string, root string) string {
+	if filepath.IsAbs(path) {
+		return path
+	}
+	return filepath.Join(root, path)
 }

+ 5 - 0
pkg/services/sqlstore/dashboard.go

@@ -81,6 +81,11 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
 		} else {
 			dash.Version += 1
 			dash.Data.Set("version", dash.Version)
+
+			if !cmd.UpdatedAt.IsZero() {
+				dash.Updated = cmd.UpdatedAt
+			}
+
 			affectedRows, err = sess.Id(dash.Id).Update(dash)
 		}
 

+ 2 - 0
pkg/setting/setting.go

@@ -55,6 +55,7 @@ var (
 	DataPath        string
 	PluginsPath     string
 	DatasourcesPath string
+	DashboardsPath  string
 	CustomInitPath  = "conf/custom.ini"
 
 	// Log settings.
@@ -475,6 +476,7 @@ func NewConfigContext(args *CommandLineArgs) error {
 	InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name")
 	PluginsPath = makeAbsolute(Cfg.Section("paths").Key("plugins").String(), HomePath)
 	DatasourcesPath = makeAbsolute(Cfg.Section("paths").Key("datasources").String(), HomePath)
+	DashboardsPath = makeAbsolute(Cfg.Section("paths").Key("dashboards").String(), HomePath)
 
 	server := Cfg.Section("server")
 	AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server)