Pārlūkot izejas kodu

Merge branch 'master' into emails

bergquist 9 gadi atpakaļ
vecāks
revīzija
f26824049f
100 mainītis faili ar 4099 papildinājumiem un 2117 dzēšanām
  1. 1 1
      .editorconfig
  2. 2 1
      .gitignore
  3. 5 0
      CHANGELOG.md
  4. 3 3
      Godeps/Godeps.json
  5. 2 0
      Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore
  6. 16 0
      Godeps/_workspace/src/gopkg.in/ini.v1/.travis.yml
  7. 12 0
      Godeps/_workspace/src/gopkg.in/ini.v1/Makefile
  8. 339 14
      Godeps/_workspace/src/gopkg.in/ini.v1/README.md
  9. 331 8
      Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md
  10. 32 0
      Godeps/_workspace/src/gopkg.in/ini.v1/error.go
  11. 177 693
      Godeps/_workspace/src/gopkg.in/ini.v1/ini.go
  12. 0 456
      Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go
  13. 633 0
      Godeps/_workspace/src/gopkg.in/ini.v1/key.go
  14. 325 0
      Godeps/_workspace/src/gopkg.in/ini.v1/parser.go
  15. 206 0
      Godeps/_workspace/src/gopkg.in/ini.v1/section.go
  16. 248 37
      Godeps/_workspace/src/gopkg.in/ini.v1/struct.go
  17. 0 181
      Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go
  18. 0 2
      Godeps/_workspace/src/gopkg.in/ini.v1/testdata/conf.ini
  19. 1 1
      README.md
  20. 1 1
      build.go
  21. 1 1
      circle.yml
  22. 14 1
      conf/defaults.ini
  23. 15 2
      conf/sample.ini
  24. 1 1
      docs/sources/datasources/cloudwatch.md
  25. 19 0
      docs/sources/installation/configuration.md
  26. 1 1
      packaging/deb/systemd/grafana-server.service
  27. 1 1
      packaging/rpm/systemd/grafana-server.service
  28. 92 7
      pkg/api/alerting.go
  29. 6 1
      pkg/api/api.go
  30. 1 1
      pkg/api/datasources.go
  31. 24 5
      pkg/api/dtos/alerting.go
  32. 2 0
      pkg/api/login.go
  33. 1 1
      pkg/cmd/grafana-cli/commands/commands.go
  34. 10 2
      pkg/cmd/grafana-cli/commands/upgrade_all_command.go
  35. 3 3
      pkg/components/imguploader/imguploader.go
  36. 12 3
      pkg/components/imguploader/imguploader_test.go
  37. 21 5
      pkg/components/imguploader/s3uploader.go
  38. 23 0
      pkg/components/imguploader/s3uploader_test.go
  39. 0 2
      pkg/components/imguploader/webdavuploader.go
  40. 37 32
      pkg/metrics/metrics.go
  41. 2 2
      pkg/models/alert.go
  42. 29 14
      pkg/models/alert_notifications.go
  43. 1 0
      pkg/models/models.go
  44. 22 20
      pkg/services/alerting/conditions/evaluator.go
  45. 1 14
      pkg/services/alerting/conditions/evaluator_test.go
  46. 18 7
      pkg/services/alerting/conditions/query.go
  47. 39 19
      pkg/services/alerting/conditions/reducer.go
  48. 39 21
      pkg/services/alerting/eval_context.go
  49. 27 1
      pkg/services/alerting/eval_handler.go
  50. 0 1
      pkg/services/alerting/eval_handler_test.go
  51. 6 1
      pkg/services/alerting/interfaces.go
  52. 5 4
      pkg/services/alerting/models.go
  53. 28 11
      pkg/services/alerting/notifier.go
  54. 80 112
      pkg/services/alerting/notifier_test.go
  55. 28 2
      pkg/services/alerting/notifiers/base.go
  56. 36 0
      pkg/services/alerting/notifiers/base_test.go
  57. 0 1
      pkg/services/alerting/notifiers/common.go
  58. 4 7
      pkg/services/alerting/notifiers/email.go
  59. 18 8
      pkg/services/alerting/notifiers/slack.go
  60. 5 8
      pkg/services/alerting/notifiers/webhook.go
  61. 26 12
      pkg/services/alerting/result_handler.go
  62. 2 0
      pkg/services/alerting/rule.go
  63. 7 2
      pkg/services/alerting/rule_test.go
  64. 24 5
      pkg/services/alerting/scheduler.go
  65. 93 0
      pkg/services/alerting/test_notification.go
  66. 9 0
      pkg/services/annotations/annotations.go
  67. 1 1
      pkg/services/sqlstore/alert.go
  68. 86 32
      pkg/services/sqlstore/alert_notification.go
  69. 19 4
      pkg/services/sqlstore/alert_notification_test.go
  70. 37 0
      pkg/services/sqlstore/annotation.go
  71. 4 0
      pkg/services/sqlstore/migrations/alert_mig.go
  72. 1 1
      pkg/setting/setting.go
  73. 3 2
      pkg/setting/setting_oauth.go
  74. 20 0
      pkg/social/common.go
  75. 205 0
      pkg/social/generic_oauth.go
  76. 213 0
      pkg/social/github_oauth.go
  77. 52 0
      pkg/social/google_oauth.go
  78. 14 268
      pkg/social/social.go
  79. 60 11
      pkg/tsdb/graphite/graphite.go
  80. 2 2
      pkg/tsdb/graphite/types.go
  81. 3 3
      pkg/tsdb/models.go
  82. 3 1
      public/app/core/controllers/login_ctrl.js
  83. 1 1
      public/app/core/directives/metric_segment.js
  84. 8 9
      public/app/core/services/alert_srv.ts
  85. 10 2
      public/app/features/alerting/alert_def.ts
  86. 1 1
      public/app/features/alerting/alert_list_ctrl.ts
  87. 41 5
      public/app/features/alerting/alert_tab_ctrl.ts
  88. 25 2
      public/app/features/alerting/notification_edit_ctrl.ts
  89. 45 8
      public/app/features/alerting/partials/alert_tab.html
  90. 43 5
      public/app/features/alerting/partials/notification_edit.html
  91. 5 2
      public/app/features/alerting/partials/notifications_list.html
  92. 5 10
      public/app/features/dashboard/dashnav/dashnav.ts
  93. 1 1
      public/app/features/dashboard/import/dash_import.html
  94. 8 0
      public/app/features/dashboard/saveDashboardAsCtrl.js
  95. 3 4
      public/app/partials/confirm_modal.html
  96. 4 0
      public/app/partials/login.html
  97. 9 0
      public/app/plugins/datasource/influxdb/query_part.ts
  98. 0 1
      public/app/plugins/panel/graph/graph.js
  99. 0 1
      public/sass/_variables.dark.scss
  100. 0 1
      public/sass/_variables.light.scss

+ 1 - 1
.editorconfig

@@ -2,7 +2,7 @@
 root = true
 root = true
 
 
 [*.go]
 [*.go]
-indent_style = tabs
+indent_style = tab
 indent_size = 2
 indent_size = 2
 charset = utf-8
 charset = utf-8
 trim_trailing_whitespace = true
 trim_trailing_whitespace = true

+ 2 - 1
.gitignore

@@ -25,6 +25,7 @@ public/css/*.min.css
 *.swp
 *.swp
 .idea/
 .idea/
 *.iml
 *.iml
+.vscode/
 
 
 /data/*
 /data/*
 /bin/*
 /bin/*
@@ -37,4 +38,4 @@ profile.cov
 .notouch
 .notouch
 /pkg/cmd/grafana-cli/grafana-cli
 /pkg/cmd/grafana-cli/grafana-cli
 /pkg/cmd/grafana-server/grafana-server
 /pkg/cmd/grafana-server/grafana-server
-/examples/*/dist
+/examples/*/dist

+ 5 - 0
CHANGELOG.md

@@ -9,12 +9,17 @@
 * **Navigation**: Add search to org swithcer, closes [#2609](https://github.com/grafana/grafana/issues/2609)
 * **Navigation**: Add search to org swithcer, closes [#2609](https://github.com/grafana/grafana/issues/2609)
 * **Database**: Allow database config using one propertie, closes [#5456](https://github.com/grafana/grafana/pull/5456)
 * **Database**: Allow database config using one propertie, closes [#5456](https://github.com/grafana/grafana/pull/5456)
 * **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613)
 * **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613)
+* **Influxdb**: Add support for elapsed(), closes [#5827](https://github.com/grafana/grafana/pull/5827)
+
+### Breaking changes
+* **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971)
 
 
 # 3.1.2 (unreleased)
 # 3.1.2 (unreleased)
 * **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)
 * **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)
 * **Drag&Drop**: Fixed issue with drag and drop in latest Chrome(51+), fixes [#5767](https://github.com/grafana/grafana/issues/5767)
 * **Drag&Drop**: Fixed issue with drag and drop in latest Chrome(51+), fixes [#5767](https://github.com/grafana/grafana/issues/5767)
 * **Internal Metrics**: Fixed issue with dots in instance_name when sending internal metrics to Graphite, fixes [#5739](https://github.com/grafana/grafana/issues/5739)
 * **Internal Metrics**: Fixed issue with dots in instance_name when sending internal metrics to Graphite, fixes [#5739](https://github.com/grafana/grafana/issues/5739)
 * **Grafana-CLI**: Add default plugin path for MAC OS, fixes [#5806](https://github.com/grafana/grafana/issues/5806)
 * **Grafana-CLI**: Add default plugin path for MAC OS, fixes [#5806](https://github.com/grafana/grafana/issues/5806)
+* **Grafana-CLI**: Improve error message for upgrade-all command, fixes [#5885](https://github.com/grafana/grafana/issues/5885)
 
 
 # 3.1.1 (2016-08-01)
 # 3.1.1 (2016-08-01)
 * **IFrame embedding**: Fixed issue of using full iframe height, fixes [#5605](https://github.com/grafana/grafana/issues/5606)
 * **IFrame embedding**: Fixed issue of using full iframe height, fixes [#5605](https://github.com/grafana/grafana/issues/5606)

+ 3 - 3
Godeps/Godeps.json

@@ -1,6 +1,6 @@
 {
 {
 	"ImportPath": "github.com/grafana/grafana",
 	"ImportPath": "github.com/grafana/grafana",
-	"GoVersion": "go1.5.1",
+	"GoVersion": "go1.6.2",
 	"GodepVersion": "v60",
 	"GodepVersion": "v60",
 	"Packages": [
 	"Packages": [
 		"./pkg/..."
 		"./pkg/..."
@@ -368,8 +368,8 @@
 		},
 		},
 		{
 		{
 			"ImportPath": "gopkg.in/ini.v1",
 			"ImportPath": "gopkg.in/ini.v1",
-			"Comment": "v0-16-g1772191",
-			"Rev": "177219109c97e7920c933e21c9b25f874357b237"
+			"Comment": "v1.21.1",
+			"Rev": "6e4869b434bd001f6983749881c7ead3545887d8"
 		},
 		},
 		{
 		{
 			"ImportPath": "gopkg.in/macaron.v1",
 			"ImportPath": "gopkg.in/macaron.v1",

+ 2 - 0
Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore

@@ -1,3 +1,5 @@
 testdata/conf_out.ini
 testdata/conf_out.ini
 ini.sublime-project
 ini.sublime-project
 ini.sublime-workspace
 ini.sublime-workspace
+testdata/conf_reflect.ini
+.idea

+ 16 - 0
Godeps/_workspace/src/gopkg.in/ini.v1/.travis.yml

@@ -0,0 +1,16 @@
+sudo: false
+language: go
+
+go:
+  - 1.4
+  - 1.5
+  - 1.6
+  - tip
+
+script: 
+  - go get -v github.com/smartystreets/goconvey
+  - go test -v -cover -race
+
+notifications:
+  email:
+    - u@gogs.io

+ 12 - 0
Godeps/_workspace/src/gopkg.in/ini.v1/Makefile

@@ -0,0 +1,12 @@
+.PHONY: build test bench vet
+
+build: vet bench
+
+test:
+	go test -v -cover -race
+
+bench:
+	go test -v -cover -race -test.bench=. -test.benchmem
+
+vet:
+	go vet

+ 339 - 14
Godeps/_workspace/src/gopkg.in/ini.v1/README.md

@@ -1,6 +1,8 @@
-ini [![Build Status](https://drone.io/github.com/go-ini/ini/status.png)](https://drone.io/github.com/go-ini/ini/latest) [![](http://gocover.io/_badge/github.com/go-ini/ini)](http://gocover.io/github.com/go-ini/ini)
+INI [![Build Status](https://travis-ci.org/go-ini/ini.svg?branch=master)](https://travis-ci.org/go-ini/ini)
 ===
 ===
 
 
+![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200)
+
 Package ini provides INI file read and write functionality in Go.
 Package ini provides INI file read and write functionality in Go.
 
 
 [简体中文](README_ZH.md)
 [简体中文](README_ZH.md)
@@ -20,13 +22,29 @@ Package ini provides INI file read and write functionality in Go.
 
 
 ## Installation
 ## Installation
 
 
+To use a tagged revision:
+
 	go get gopkg.in/ini.v1
 	go get gopkg.in/ini.v1
 
 
+To use with latest changes:
+
+	go get github.com/go-ini/ini
+
+Please add `-u` flag to update in the future.
+
+### Testing
+
+If you want to test on your machine, please apply `-t` flag:
+
+	go get -t gopkg.in/ini.v1
+
+Please add `-u` flag to update in the future.
+
 ## Getting Started
 ## Getting Started
 
 
 ### Loading from data sources
 ### Loading from data sources
 
 
-A **Data Source** is either raw data in type `[]byte` or a file name with type `string` and you can load **as many as** data sources you want. Passing other types will simply return an error.
+A **Data Source** is either raw data in type `[]byte` or a file name with type `string` and you can load **as many data sources as you want**. Passing other types will simply return an error.
 
 
 ```go
 ```go
 cfg, err := ini.Load([]byte("raw data"), "filename")
 cfg, err := ini.Load([]byte("raw data"), "filename")
@@ -38,12 +56,56 @@ Or start with an empty object:
 cfg := ini.Empty()
 cfg := ini.Empty()
 ```
 ```
 
 
-When you cannot decide how many data sources to load at the beginning, you still able to **Append()** them later.
+When you cannot decide how many data sources to load at the beginning, you will still be able to **Append()** them later.
 
 
 ```go
 ```go
 err := cfg.Append("other file", []byte("other raw data"))
 err := cfg.Append("other file", []byte("other raw data"))
 ```
 ```
 
 
+If you have a list of files with possibilities that some of them may not available at the time, and you don't know exactly which ones, you can use `LooseLoad` to ignore nonexistent files without returning error.
+
+```go
+cfg, err := ini.LooseLoad("filename", "filename_404")
+```
+
+The cool thing is, whenever the file is available to load while you're calling `Reload` method, it will be counted as usual.
+
+#### Ignore cases of key name
+
+When you do not care about cases of section and key names, you can use `InsensitiveLoad` to force all names to be lowercased while parsing.
+
+```go
+cfg, err := ini.InsensitiveLoad("filename")
+//...
+
+// sec1 and sec2 are the exactly same section object
+sec1, err := cfg.GetSection("Section")
+sec2, err := cfg.GetSection("SecTIOn")
+
+// key1 and key2 are the exactly same key object
+key1, err := cfg.GetKey("Key")
+key2, err := cfg.GetKey("KeY")
+```
+
+#### MySQL-like boolean key 
+
+MySQL's configuration allows a key without value as follows:
+
+```ini
+[mysqld]
+...
+skip-host-cache
+skip-name-resolve
+```
+
+By default, this is considered as missing value. But if you know you're going to deal with those cases, you can assign advanced load options:
+
+```go
+cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf"))
+```
+
+The value of those keys are always `true`, and when you save to a file, it will keep in the same foramt as you read.
+
 ### Working with sections
 ### Working with sections
 
 
 To get a section, you would need to:
 To get a section, you would need to:
@@ -93,6 +155,12 @@ Same rule applies to key operations:
 key := cfg.Section("").Key("key name")
 key := cfg.Section("").Key("key name")
 ```
 ```
 
 
+To check if a key exists:
+
+```go
+yes := cfg.Section("").HasKey("key name")
+```
+
 To create a new key:
 To create a new key:
 
 
 ```go
 ```go
@@ -102,14 +170,14 @@ err := cfg.Section("").NewKey("name", "value")
 To get a list of keys or key names:
 To get a list of keys or key names:
 
 
 ```go
 ```go
-keys := cfg.Section().Keys()
-names := cfg.Section().KeyStrings()
+keys := cfg.Section("").Keys()
+names := cfg.Section("").KeyStrings()
 ```
 ```
 
 
 To get a clone hash of keys and corresponding values:
 To get a clone hash of keys and corresponding values:
 
 
 ```go
 ```go
-hash := cfg.GetSection("").KeysHash()
+hash := cfg.Section("").KeysHash()
 ```
 ```
 
 
 ### Working with values
 ### Working with values
@@ -120,16 +188,41 @@ To get a string value:
 val := cfg.Section("").Key("key name").String()
 val := cfg.Section("").Key("key name").String()
 ```
 ```
 
 
+To validate key value on the fly:
+
+```go
+val := cfg.Section("").Key("key name").Validate(func(in string) string {
+	if len(in) == 0 {
+		return "default"
+	}
+	return in
+})
+```
+
+If you do not want any auto-transformation (such as recursive read) for the values, you can get raw value directly (this way you get much better performance):
+
+```go
+val := cfg.Section("").Key("key name").Value()
+```
+
+To check if raw value exists:
+
+```go
+yes := cfg.Section("").HasValue("test value")
+```
+
 To get value with types:
 To get value with types:
 
 
 ```go
 ```go
 // For boolean values:
 // For boolean values:
-// true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On
-// false when value is: 0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off
+// true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On
+// false when value is: 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off
 v, err = cfg.Section("").Key("BOOL").Bool()
 v, err = cfg.Section("").Key("BOOL").Bool()
 v, err = cfg.Section("").Key("FLOAT64").Float64()
 v, err = cfg.Section("").Key("FLOAT64").Float64()
 v, err = cfg.Section("").Key("INT").Int()
 v, err = cfg.Section("").Key("INT").Int()
 v, err = cfg.Section("").Key("INT64").Int64()
 v, err = cfg.Section("").Key("INT64").Int64()
+v, err = cfg.Section("").Key("UINT").Uint()
+v, err = cfg.Section("").Key("UINT64").Uint64()
 v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339)
 v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339)
 v, err = cfg.Section("").Key("TIME").Time() // RFC3339
 v, err = cfg.Section("").Key("TIME").Time() // RFC3339
 
 
@@ -137,6 +230,8 @@ v = cfg.Section("").Key("BOOL").MustBool()
 v = cfg.Section("").Key("FLOAT64").MustFloat64()
 v = cfg.Section("").Key("FLOAT64").MustFloat64()
 v = cfg.Section("").Key("INT").MustInt()
 v = cfg.Section("").Key("INT").MustInt()
 v = cfg.Section("").Key("INT64").MustInt64()
 v = cfg.Section("").Key("INT64").MustInt64()
+v = cfg.Section("").Key("UINT").MustUint()
+v = cfg.Section("").Key("UINT64").MustUint64()
 v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339)
 v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339)
 v = cfg.Section("").Key("TIME").MustTime() // RFC3339
 v = cfg.Section("").Key("TIME").MustTime() // RFC3339
 
 
@@ -144,11 +239,13 @@ v = cfg.Section("").Key("TIME").MustTime() // RFC3339
 // when key not found or fail to parse value to given type.
 // when key not found or fail to parse value to given type.
 // Except method MustString, which you have to pass a default value.
 // Except method MustString, which you have to pass a default value.
 
 
-v = cfg.Seciont("").Key("String").MustString("default")
+v = cfg.Section("").Key("String").MustString("default")
 v = cfg.Section("").Key("BOOL").MustBool(true)
 v = cfg.Section("").Key("BOOL").MustBool(true)
 v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25)
 v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25)
 v = cfg.Section("").Key("INT").MustInt(10)
 v = cfg.Section("").Key("INT").MustInt(10)
 v = cfg.Section("").Key("INT64").MustInt64(99)
 v = cfg.Section("").Key("INT64").MustInt64(99)
+v = cfg.Section("").Key("UINT").MustUint(3)
+v = cfg.Section("").Key("UINT64").MustUint64(6)
 v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now())
 v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now())
 v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
 v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
 ```
 ```
@@ -174,6 +271,42 @@ Earth
 ------  end  --- */
 ------  end  --- */
 ```
 ```
 
 
+That's cool, how about continuation lines?
+
+```ini
+[advance]
+two_lines = how about \
+	continuation lines?
+lots_of_lines = 1 \
+	2 \
+	3 \
+	4
+```
+
+Piece of cake!
+
+```go
+cfg.Section("advance").Key("two_lines").String() // how about continuation lines?
+cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4
+```
+
+Well, I hate continuation lines, how do I disable that?
+
+```go
+cfg, err := ini.LoadSources(ini.LoadOptions{
+	IgnoreContinuation: true,
+}, "filename")
+```
+
+Holy crap! 
+
+Note that single quotes around values will be stripped:
+
+```ini
+foo = "some value" // foo: some value
+bar = 'some value' // bar: some value
+```
+
 That's all? Hmm, no.
 That's all? Hmm, no.
 
 
 #### Helper methods of working with values
 #### Helper methods of working with values
@@ -185,6 +318,8 @@ v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"})
 v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
 v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
 v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
 v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
 v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
 v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
+v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9})
+v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9})
 v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
 v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
 v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
 v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
 ```
 ```
@@ -197,20 +332,74 @@ To validate value in a given range:
 vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
 vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
 vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
 vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
 vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
 vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
+vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9)
+vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9)
 vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
 vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
 vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
 vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
 ```
 ```
 
 
-To auto-split value into slice:
+##### Auto-split values into a slice
+
+To use zero value of type for invalid inputs:
 
 
 ```go
 ```go
+// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
+// Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0]
 vals = cfg.Section("").Key("STRINGS").Strings(",")
 vals = cfg.Section("").Key("STRINGS").Strings(",")
 vals = cfg.Section("").Key("FLOAT64S").Float64s(",")
 vals = cfg.Section("").Key("FLOAT64S").Float64s(",")
 vals = cfg.Section("").Key("INTS").Ints(",")
 vals = cfg.Section("").Key("INTS").Ints(",")
 vals = cfg.Section("").Key("INT64S").Int64s(",")
 vals = cfg.Section("").Key("INT64S").Int64s(",")
+vals = cfg.Section("").Key("UINTS").Uints(",")
+vals = cfg.Section("").Key("UINT64S").Uint64s(",")
 vals = cfg.Section("").Key("TIMES").Times(",")
 vals = cfg.Section("").Key("TIMES").Times(",")
 ```
 ```
 
 
+To exclude invalid values out of result slice:
+
+```go
+// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
+// Input: how, 2.2, are, you -> [2.2]
+vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",")
+vals = cfg.Section("").Key("INTS").ValidInts(",")
+vals = cfg.Section("").Key("INT64S").ValidInt64s(",")
+vals = cfg.Section("").Key("UINTS").ValidUints(",")
+vals = cfg.Section("").Key("UINT64S").ValidUint64s(",")
+vals = cfg.Section("").Key("TIMES").ValidTimes(",")
+```
+
+Or to return nothing but error when have invalid inputs:
+
+```go
+// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
+// Input: how, 2.2, are, you -> error
+vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",")
+vals = cfg.Section("").Key("INTS").StrictInts(",")
+vals = cfg.Section("").Key("INT64S").StrictInt64s(",")
+vals = cfg.Section("").Key("UINTS").StrictUints(",")
+vals = cfg.Section("").Key("UINT64S").StrictUint64s(",")
+vals = cfg.Section("").Key("TIMES").StrictTimes(",")
+```
+
+### Save your configuration
+
+Finally, it's time to save your configuration to somewhere.
+
+A typical way to save configuration is writing it to a file:
+
+```go
+// ...
+err = cfg.SaveTo("my.ini")
+err = cfg.SaveToIndent("my.ini", "\t")
+```
+
+Another way to save is writing to a `io.Writer` interface:
+
+```go
+// ...
+cfg.WriteTo(writer)
+cfg.WriteToIndent(writer, "\t")
+```
+
 ## Advanced Usage
 ## Advanced Usage
 
 
 ### Recursive Values
 ### Recursive Values
@@ -252,6 +441,12 @@ CLONE_URL = https://%(IMPORT_PATH)s
 cfg.Section("package.sub").Key("CLONE_URL").String()	// https://gopkg.in/ini.v1
 cfg.Section("package.sub").Key("CLONE_URL").String()	// https://gopkg.in/ini.v1
 ```
 ```
 
 
+#### Retrieve parent keys available to a child section
+
+```go
+cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"]
+```
+
 ### Auto-increment Key Names
 ### Auto-increment Key Names
 
 
 If key name is `-` in data source, then it would be seen as special syntax for auto-increment key name start from 1, and every section is independent on counter.
 If key name is `-` in data source, then it would be seen as special syntax for auto-increment key name start from 1, and every section is independent on counter.
@@ -327,9 +522,57 @@ p := &Person{
 // ...
 // ...
 ```
 ```
 
 
+It's really cool, but what's the point if you can't give me my file back from struct?
+
+### Reflect From Struct
+
+Why not?
+
+```go
+type Embeded struct {
+	Dates  []time.Time `delim:"|"`
+	Places []string    `ini:"places,omitempty"`
+	None   []int       `ini:",omitempty"`
+}
+
+type Author struct {
+	Name      string `ini:"NAME"`
+	Male      bool
+	Age       int
+	GPA       float64
+	NeverMind string `ini:"-"`
+	*Embeded
+}
+
+func main() {
+	a := &Author{"Unknwon", true, 21, 2.8, "",
+		&Embeded{
+			[]time.Time{time.Now(), time.Now()},
+			[]string{"HangZhou", "Boston"},
+			[]int{},
+		}}
+	cfg := ini.Empty()
+	err = ini.ReflectFrom(cfg, a)
+	// ...
+}
+```
+
+So, what do I get?
+
+```ini
+NAME = Unknwon
+Male = true
+Age = 21
+GPA = 2.8
+
+[Embeded]
+Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00
+places = HangZhou,Boston
+```
+
 #### Name Mapper
 #### Name Mapper
 
 
-To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual secion and key name.
+To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual section and key name.
 
 
 There are 2 built-in name mappers:
 There are 2 built-in name mappers:
 
 
@@ -339,15 +582,15 @@ There are 2 built-in name mappers:
 To use them:
 To use them:
 
 
 ```go
 ```go
-type Info struct{
+type Info struct {
 	PackageName string
 	PackageName string
 }
 }
 
 
 func main() {
 func main() {
-	err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("packag_name=ini"))
+	err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini"))
 	// ...
 	// ...
 
 
-	cfg, err := ini.Load("PACKAGE_NAME=ini")
+	cfg, err := ini.Load([]byte("PACKAGE_NAME=ini"))
 	// ...
 	// ...
 	info := new(Info)
 	info := new(Info)
 	cfg.NameMapper = ini.AllCapsUnderscore
 	cfg.NameMapper = ini.AllCapsUnderscore
@@ -356,6 +599,88 @@ func main() {
 }
 }
 ```
 ```
 
 
+Same rules of name mapper apply to `ini.ReflectFromWithMapper` function.
+
+#### Value Mapper
+
+To expand values (e.g. from environment variables), you can use the `ValueMapper` to transform values:
+
+```go
+type Env struct {
+	Foo string `ini:"foo"`
+}
+
+func main() {
+	cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n")
+	cfg.ValueMapper = os.ExpandEnv
+	// ...
+	env := &Env{}
+	err = cfg.Section("env").MapTo(env)
+}
+```
+
+This would set the value of `env.Foo` to the value of the environment variable `MY_VAR`.
+
+#### Other Notes On Map/Reflect
+
+Any embedded struct is treated as a section by default, and there is no automatic parent-child relations in map/reflect feature:
+
+```go
+type Child struct {
+	Age string
+}
+
+type Parent struct {
+	Name string
+	Child
+}
+
+type Config struct {
+	City string
+	Parent
+}
+```
+
+Example configuration:
+
+```ini
+City = Boston
+
+[Parent]
+Name = Unknwon
+
+[Child]
+Age = 21
+```
+
+What if, yes, I'm paranoid, I want embedded struct to be in the same section. Well, all roads lead to Rome.
+
+```go
+type Child struct {
+	Age string
+}
+
+type Parent struct {
+	Name string
+	Child `ini:"Parent"`
+}
+
+type Config struct {
+	City string
+	Parent
+}
+```
+
+Example configuration:
+
+```ini
+City = Boston
+
+[Parent]
+Name = Unknwon
+Age = 21
+```
+
 ## Getting Help
 ## Getting Help
 
 
 - [API Documentation](https://gowalker.org/gopkg.in/ini.v1)
 - [API Documentation](https://gowalker.org/gopkg.in/ini.v1)

+ 331 - 8
Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md

@@ -15,8 +15,24 @@
 
 
 ## 下载安装
 ## 下载安装
 
 
+使用一个特定版本:
+
     go get gopkg.in/ini.v1
     go get gopkg.in/ini.v1
 
 
+使用最新版:
+
+	go get github.com/go-ini/ini
+
+如需更新请添加 `-u` 选项。
+
+### 测试安装
+
+如果您想要在自己的机器上运行测试,请使用 `-t` 标记:
+
+	go get -t gopkg.in/ini.v1
+
+如需更新请添加 `-u` 选项。
+
 ## 开始使用
 ## 开始使用
 
 
 ### 从数据源加载
 ### 从数据源加载
@@ -39,6 +55,50 @@ cfg := ini.Empty()
 err := cfg.Append("other file", []byte("other raw data"))
 err := cfg.Append("other file", []byte("other raw data"))
 ```
 ```
 
 
+当您想要加载一系列文件,但是不能够确定其中哪些文件是不存在的,可以通过调用函数 `LooseLoad` 来忽略它们(`Load` 会因为文件不存在而返回错误):
+
+```go
+cfg, err := ini.LooseLoad("filename", "filename_404")
+```
+
+更牛逼的是,当那些之前不存在的文件在重新调用 `Reload` 方法的时候突然出现了,那么它们会被正常加载。
+
+#### 忽略键名的大小写
+
+有时候分区和键的名称大小写混合非常烦人,这个时候就可以通过 `InsensitiveLoad` 将所有分区和键名在读取里强制转换为小写:
+
+```go
+cfg, err := ini.InsensitiveLoad("filename")
+//...
+
+// sec1 和 sec2 指向同一个分区对象
+sec1, err := cfg.GetSection("Section")
+sec2, err := cfg.GetSection("SecTIOn")
+
+// key1 和 key2 指向同一个键对象
+key1, err := cfg.GetKey("Key")
+key2, err := cfg.GetKey("KeY")
+```
+
+#### 类似 MySQL 配置中的布尔值键
+
+MySQL 的配置文件中会出现没有具体值的布尔类型的键:
+
+```ini
+[mysqld]
+...
+skip-host-cache
+skip-name-resolve
+```
+
+默认情况下这被认为是缺失值而无法完成解析,但可以通过高级的加载选项对它们进行处理:
+
+```go
+cfg, err := LoadSources(LoadOptions{AllowBooleanKeys: true}, "my.cnf"))
+```
+
+这些键的值永远为 `true`,且在保存到文件时也只会输出键名。
+
 ### 操作分区(Section)
 ### 操作分区(Section)
 
 
 获取指定分区:
 获取指定分区:
@@ -88,6 +148,12 @@ key, err := cfg.Section("").GetKey("key name")
 key := cfg.Section("").Key("key name")
 key := cfg.Section("").Key("key name")
 ```
 ```
 
 
+判断某个键是否存在:
+
+```go
+yes := cfg.Section("").HasKey("key name")
+```
+
 创建一个新的键:
 创建一个新的键:
 
 
 ```go
 ```go
@@ -97,14 +163,14 @@ err := cfg.Section("").NewKey("name", "value")
 获取分区下的所有键或键名:
 获取分区下的所有键或键名:
 
 
 ```go
 ```go
-keys := cfg.Section().Keys()
-names := cfg.Section().KeyStrings()
+keys := cfg.Section("").Keys()
+names := cfg.Section("").KeyStrings()
 ```
 ```
 
 
 获取分区下的所有键值对的克隆:
 获取分区下的所有键值对的克隆:
 
 
 ```go
 ```go
-hash := cfg.GetSection("").KeysHash()
+hash := cfg.Section("").KeysHash()
 ```
 ```
 
 
 ### 操作键值(Value)
 ### 操作键值(Value)
@@ -115,16 +181,41 @@ hash := cfg.GetSection("").KeysHash()
 val := cfg.Section("").Key("key name").String()
 val := cfg.Section("").Key("key name").String()
 ```
 ```
 
 
+获取值的同时通过自定义函数进行处理验证:
+
+```go
+val := cfg.Section("").Key("key name").Validate(func(in string) string {
+	if len(in) == 0 {
+		return "default"
+	}
+	return in
+})
+```
+
+如果您不需要任何对值的自动转变功能(例如递归读取),可以直接获取原值(这种方式性能最佳):
+
+```go
+val := cfg.Section("").Key("key name").Value()
+```
+
+判断某个原值是否存在:
+
+```go
+yes := cfg.Section("").HasValue("test value")
+```
+
 获取其它类型的值:
 获取其它类型的值:
 
 
 ```go
 ```go
 // 布尔值的规则:
 // 布尔值的规则:
-// true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On
-// false 当值为:0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off
+// true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On
+// false 当值为:0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off
 v, err = cfg.Section("").Key("BOOL").Bool()
 v, err = cfg.Section("").Key("BOOL").Bool()
 v, err = cfg.Section("").Key("FLOAT64").Float64()
 v, err = cfg.Section("").Key("FLOAT64").Float64()
 v, err = cfg.Section("").Key("INT").Int()
 v, err = cfg.Section("").Key("INT").Int()
 v, err = cfg.Section("").Key("INT64").Int64()
 v, err = cfg.Section("").Key("INT64").Int64()
+v, err = cfg.Section("").Key("UINT").Uint()
+v, err = cfg.Section("").Key("UINT64").Uint64()
 v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339)
 v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339)
 v, err = cfg.Section("").Key("TIME").Time() // RFC3339
 v, err = cfg.Section("").Key("TIME").Time() // RFC3339
 
 
@@ -132,6 +223,8 @@ v = cfg.Section("").Key("BOOL").MustBool()
 v = cfg.Section("").Key("FLOAT64").MustFloat64()
 v = cfg.Section("").Key("FLOAT64").MustFloat64()
 v = cfg.Section("").Key("INT").MustInt()
 v = cfg.Section("").Key("INT").MustInt()
 v = cfg.Section("").Key("INT64").MustInt64()
 v = cfg.Section("").Key("INT64").MustInt64()
+v = cfg.Section("").Key("UINT").MustUint()
+v = cfg.Section("").Key("UINT64").MustUint64()
 v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339)
 v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339)
 v = cfg.Section("").Key("TIME").MustTime() // RFC3339
 v = cfg.Section("").Key("TIME").MustTime() // RFC3339
 
 
@@ -144,6 +237,8 @@ v = cfg.Section("").Key("BOOL").MustBool(true)
 v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25)
 v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25)
 v = cfg.Section("").Key("INT").MustInt(10)
 v = cfg.Section("").Key("INT").MustInt(10)
 v = cfg.Section("").Key("INT64").MustInt64(99)
 v = cfg.Section("").Key("INT64").MustInt64(99)
+v = cfg.Section("").Key("UINT").MustUint(3)
+v = cfg.Section("").Key("UINT64").MustUint64(6)
 v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now())
 v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now())
 v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
 v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
 ```
 ```
@@ -169,6 +264,42 @@ Earth
 ------  end  --- */
 ------  end  --- */
 ```
 ```
 
 
+赞爆了!那要是我属于一行的内容写不下想要写到第二行怎么办?
+
+```ini
+[advance]
+two_lines = how about \
+	continuation lines?
+lots_of_lines = 1 \
+	2 \
+	3 \
+	4
+```
+
+简直是小菜一碟!
+
+```go
+cfg.Section("advance").Key("two_lines").String() // how about continuation lines?
+cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4
+```
+
+可是我有时候觉得两行连在一起特别没劲,怎么才能不自动连接两行呢?
+
+```go
+cfg, err := ini.LoadSources(ini.LoadOptions{
+	IgnoreContinuation: true,
+}, "filename")
+```
+
+哇靠给力啊!
+
+需要注意的是,值两侧的单引号会被自动剔除:
+
+```ini
+foo = "some value" // foo: some value
+bar = 'some value' // bar: some value
+```
+
 这就是全部了?哈哈,当然不是。
 这就是全部了?哈哈,当然不是。
 
 
 #### 操作键值的辅助方法
 #### 操作键值的辅助方法
@@ -180,6 +311,8 @@ v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"})
 v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
 v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
 v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
 v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
 v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
 v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
+v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9})
+v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9})
 v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
 v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
 v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
 v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
 ```
 ```
@@ -192,20 +325,74 @@ v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, tim
 vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
 vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
 vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
 vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
 vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
 vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
+vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9)
+vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9)
 vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
 vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
 vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
 vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
 ```
 ```
 
 
-自动分割键值为切片(slice):
+##### 自动分割键值到切片(slice)
+
+当存在无效输入时,使用零值代替:
 
 
 ```go
 ```go
+// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
+// Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0]
 vals = cfg.Section("").Key("STRINGS").Strings(",")
 vals = cfg.Section("").Key("STRINGS").Strings(",")
 vals = cfg.Section("").Key("FLOAT64S").Float64s(",")
 vals = cfg.Section("").Key("FLOAT64S").Float64s(",")
 vals = cfg.Section("").Key("INTS").Ints(",")
 vals = cfg.Section("").Key("INTS").Ints(",")
 vals = cfg.Section("").Key("INT64S").Int64s(",")
 vals = cfg.Section("").Key("INT64S").Int64s(",")
+vals = cfg.Section("").Key("UINTS").Uints(",")
+vals = cfg.Section("").Key("UINT64S").Uint64s(",")
 vals = cfg.Section("").Key("TIMES").Times(",")
 vals = cfg.Section("").Key("TIMES").Times(",")
 ```
 ```
 
 
+从结果切片中剔除无效输入:
+
+```go
+// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
+// Input: how, 2.2, are, you -> [2.2]
+vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",")
+vals = cfg.Section("").Key("INTS").ValidInts(",")
+vals = cfg.Section("").Key("INT64S").ValidInt64s(",")
+vals = cfg.Section("").Key("UINTS").ValidUints(",")
+vals = cfg.Section("").Key("UINT64S").ValidUint64s(",")
+vals = cfg.Section("").Key("TIMES").ValidTimes(",")
+```
+
+当存在无效输入时,直接返回错误:
+
+```go
+// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
+// Input: how, 2.2, are, you -> error
+vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",")
+vals = cfg.Section("").Key("INTS").StrictInts(",")
+vals = cfg.Section("").Key("INT64S").StrictInt64s(",")
+vals = cfg.Section("").Key("UINTS").StrictUints(",")
+vals = cfg.Section("").Key("UINT64S").StrictUint64s(",")
+vals = cfg.Section("").Key("TIMES").StrictTimes(",")
+```
+
+### 保存配置
+
+终于到了这个时刻,是时候保存一下配置了。
+
+比较原始的做法是输出配置到某个文件:
+
+```go
+// ...
+err = cfg.SaveTo("my.ini")
+err = cfg.SaveToIndent("my.ini", "\t")
+```
+
+另一个比较高级的做法是写入到任何实现 `io.Writer` 接口的对象中:
+
+```go
+// ...
+cfg.WriteTo(writer)
+cfg.WriteToIndent(writer, "\t")
+```
+
 ### 高级用法
 ### 高级用法
 
 
 #### 递归读取键值
 #### 递归读取键值
@@ -247,6 +434,12 @@ CLONE_URL = https://%(IMPORT_PATH)s
 cfg.Section("package.sub").Key("CLONE_URL").String()	// https://gopkg.in/ini.v1
 cfg.Section("package.sub").Key("CLONE_URL").String()	// https://gopkg.in/ini.v1
 ```
 ```
 
 
+#### 获取上级父分区下的所有键名
+
+```go
+cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"]
+```
+
 #### 读取自增键名
 #### 读取自增键名
 
 
 如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 1 开始,并且分区之间是相互独立的。
 如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 1 开始,并且分区之间是相互独立的。
@@ -320,6 +513,54 @@ p := &Person{
 // ...
 // ...
 ```
 ```
 
 
+这样玩 INI 真的好酷啊!然而,如果不能还给我原来的配置文件,有什么卵用?
+
+### 从结构反射
+
+可是,我有说不能吗?
+
+```go
+type Embeded struct {
+	Dates  []time.Time `delim:"|"`
+	Places []string    `ini:"places,omitempty"`
+	None   []int       `ini:",omitempty"`
+}
+
+type Author struct {
+	Name      string `ini:"NAME"`
+	Male      bool
+	Age       int
+	GPA       float64
+	NeverMind string `ini:"-"`
+	*Embeded
+}
+
+func main() {
+	a := &Author{"Unknwon", true, 21, 2.8, "",
+		&Embeded{
+			[]time.Time{time.Now(), time.Now()},
+			[]string{"HangZhou", "Boston"},
+			[]int{},
+		}}
+	cfg := ini.Empty()
+	err = ini.ReflectFrom(cfg, a)
+	// ...
+}
+```
+
+瞧瞧,奇迹发生了。
+
+```ini
+NAME = Unknwon
+Male = true
+Age = 21
+GPA = 2.8
+
+[Embeded]
+Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00
+places = HangZhou,Boston
+```
+
 #### 名称映射器(Name Mapper)
 #### 名称映射器(Name Mapper)
 
 
 为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。
 为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。
@@ -337,10 +578,10 @@ type Info struct{
 }
 }
 
 
 func main() {
 func main() {
-	err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("packag_name=ini"))
+	err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini"))
 	// ...
 	// ...
 
 
-	cfg, err := ini.Load("PACKAGE_NAME=ini")
+	cfg, err := ini.Load([]byte("PACKAGE_NAME=ini"))
 	// ...
 	// ...
 	info := new(Info)
 	info := new(Info)
 	cfg.NameMapper = ini.AllCapsUnderscore
 	cfg.NameMapper = ini.AllCapsUnderscore
@@ -349,6 +590,88 @@ func main() {
 }
 }
 ```
 ```
 
 
+使用函数 `ini.ReflectFromWithMapper` 时也可应用相同的规则。
+
+#### 值映射器(Value Mapper)
+
+值映射器允许使用一个自定义函数自动展开值的具体内容,例如:运行时获取环境变量:
+
+```go
+type Env struct {
+	Foo string `ini:"foo"`
+}
+
+func main() {
+	cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n")
+	cfg.ValueMapper = os.ExpandEnv
+	// ...
+	env := &Env{}
+	err = cfg.Section("env").MapTo(env)
+}
+```
+
+本例中,`env.Foo` 将会是运行时所获取到环境变量 `MY_VAR` 的值。
+
+#### 映射/反射的其它说明
+
+任何嵌入的结构都会被默认认作一个不同的分区,并且不会自动产生所谓的父子分区关联:
+
+```go
+type Child struct {
+	Age string
+}
+
+type Parent struct {
+	Name string
+	Child
+}
+
+type Config struct {
+	City string
+	Parent
+}
+```
+
+示例配置文件:
+
+```ini
+City = Boston
+
+[Parent]
+Name = Unknwon
+
+[Child]
+Age = 21
+```
+
+很好,但是,我就是要嵌入结构也在同一个分区。好吧,你爹是李刚!
+
+```go
+type Child struct {
+	Age string
+}
+
+type Parent struct {
+	Name string
+	Child `ini:"Parent"`
+}
+
+type Config struct {
+	City string
+	Parent
+}
+```
+
+示例配置文件:
+
+```ini
+City = Boston
+
+[Parent]
+Name = Unknwon
+Age = 21
+```
+
 ## 获取帮助
 ## 获取帮助
 
 
 - [API 文档](https://gowalker.org/gopkg.in/ini.v1)
 - [API 文档](https://gowalker.org/gopkg.in/ini.v1)

+ 32 - 0
Godeps/_workspace/src/gopkg.in/ini.v1/error.go

@@ -0,0 +1,32 @@
+// Copyright 2016 Unknwon
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package ini
+
+import (
+	"fmt"
+)
+
+type ErrDelimiterNotFound struct {
+	Line string
+}
+
+func IsErrDelimiterNotFound(err error) bool {
+	_, ok := err.(ErrDelimiterNotFound)
+	return ok
+}
+
+func (err ErrDelimiterNotFound) Error() string {
+	return fmt.Sprintf("key-value delimiter not found: %s", err.Line)
+}

+ 177 - 693
Godeps/_workspace/src/gopkg.in/ini.v1/ini.go

@@ -16,7 +16,6 @@
 package ini
 package ini
 
 
 import (
 import (
-	"bufio"
 	"bytes"
 	"bytes"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
@@ -31,25 +30,35 @@ import (
 )
 )
 
 
 const (
 const (
+	// Name for default section. You can use this constant or the string literal.
+	// In most of cases, an empty string is all you need to access the section.
 	DEFAULT_SECTION = "DEFAULT"
 	DEFAULT_SECTION = "DEFAULT"
+
 	// Maximum allowed depth when recursively substituing variable names.
 	// Maximum allowed depth when recursively substituing variable names.
 	_DEPTH_VALUES = 99
 	_DEPTH_VALUES = 99
-
-	_VERSION = "1.2.6"
+	_VERSION      = "1.21.1"
 )
 )
 
 
+// Version returns current package version literal.
 func Version() string {
 func Version() string {
 	return _VERSION
 	return _VERSION
 }
 }
 
 
 var (
 var (
+	// Delimiter to determine or compose a new line.
+	// This variable will be changed to "\r\n" automatically on Windows
+	// at package init time.
 	LineBreak = "\n"
 	LineBreak = "\n"
 
 
 	// Variable regexp pattern: %(variable)s
 	// Variable regexp pattern: %(variable)s
 	varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`)
 	varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`)
 
 
-	// Write spaces around "=" to look better.
+	// Indicate whether to align "=" sign with spaces to produce pretty output
+	// or reduce all possible spaces for compact format.
 	PrettyFormat = true
 	PrettyFormat = true
+
+	// Explicitly write DEFAULT section header
+	DefaultHeader = false
 )
 )
 
 
 func init() {
 func init() {
@@ -67,501 +76,41 @@ func inSlice(str string, s []string) bool {
 	return false
 	return false
 }
 }
 
 
-// dataSource is a interface that returns file content.
+// dataSource is an interface that returns object which can be read and closed.
 type dataSource interface {
 type dataSource interface {
-	Reader() (io.Reader, error)
+	ReadCloser() (io.ReadCloser, error)
 }
 }
 
 
+// sourceFile represents an object that contains content on the local file system.
 type sourceFile struct {
 type sourceFile struct {
 	name string
 	name string
 }
 }
 
 
-func (s sourceFile) Reader() (io.Reader, error) {
+func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) {
 	return os.Open(s.name)
 	return os.Open(s.name)
 }
 }
 
 
-type sourceData struct {
-	data []byte
-}
-
-func (s *sourceData) Reader() (io.Reader, error) {
-	return bytes.NewReader(s.data), nil
-}
-
-//  ____  __.
-// |    |/ _|____ ___.__.
-// |      <_/ __ <   |  |
-// |    |  \  ___/\___  |
-// |____|__ \___  > ____|
-//         \/   \/\/
-
-// Key represents a key under a section.
-type Key struct {
-	s          *Section
-	Comment    string
-	name       string
-	value      string
-	isAutoIncr bool
-}
-
-// Name returns name of key.
-func (k *Key) Name() string {
-	return k.name
-}
-
-// Value returns raw value of key for performance purpose.
-func (k *Key) Value() string {
-	return k.value
-}
-
-// String returns string representation of value.
-func (k *Key) String() string {
-	val := k.value
-	if strings.Index(val, "%") == -1 {
-		return val
-	}
-
-	for i := 0; i < _DEPTH_VALUES; i++ {
-		vr := varPattern.FindString(val)
-		if len(vr) == 0 {
-			break
-		}
-
-		// Take off leading '%(' and trailing ')s'.
-		noption := strings.TrimLeft(vr, "%(")
-		noption = strings.TrimRight(noption, ")s")
-
-		// Search in the same section.
-		nk, err := k.s.GetKey(noption)
-		if err != nil {
-			// Search again in default section.
-			nk, _ = k.s.f.Section("").GetKey(noption)
-		}
-
-		// Substitute by new value and take off leading '%(' and trailing ')s'.
-		val = strings.Replace(val, vr, nk.value, -1)
-	}
-	return val
-}
-
-// parseBool returns the boolean value represented by the string.
-//
-// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On,
-// 0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off.
-// Any other value returns an error.
-func parseBool(str string) (value bool, err error) {
-	switch str {
-	case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "ON", "on", "On":
-		return true, nil
-	case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "OFF", "off", "Off":
-		return false, nil
-	}
-	return false, fmt.Errorf("parsing \"%s\": invalid syntax", str)
-}
-
-// Bool returns bool type value.
-func (k *Key) Bool() (bool, error) {
-	return parseBool(k.String())
-}
-
-// Float64 returns float64 type value.
-func (k *Key) Float64() (float64, error) {
-	return strconv.ParseFloat(k.String(), 64)
-}
-
-// Int returns int type value.
-func (k *Key) Int() (int, error) {
-	return strconv.Atoi(k.String())
-}
-
-// Int64 returns int64 type value.
-func (k *Key) Int64() (int64, error) {
-	return strconv.ParseInt(k.String(), 10, 64)
-}
-
-// TimeFormat parses with given format and returns time.Time type value.
-func (k *Key) TimeFormat(format string) (time.Time, error) {
-	return time.Parse(format, k.String())
-}
-
-// Time parses with RFC3339 format and returns time.Time type value.
-func (k *Key) Time() (time.Time, error) {
-	return k.TimeFormat(time.RFC3339)
-}
-
-// MustString returns default value if key value is empty.
-func (k *Key) MustString(defaultVal string) string {
-	val := k.String()
-	if len(val) == 0 {
-		return defaultVal
-	}
-	return val
-}
-
-// MustBool always returns value without error,
-// it returns false if error occurs.
-func (k *Key) MustBool(defaultVal ...bool) bool {
-	val, err := k.Bool()
-	if len(defaultVal) > 0 && err != nil {
-		return defaultVal[0]
-	}
-	return val
-}
-
-// MustFloat64 always returns value without error,
-// it returns 0.0 if error occurs.
-func (k *Key) MustFloat64(defaultVal ...float64) float64 {
-	val, err := k.Float64()
-	if len(defaultVal) > 0 && err != nil {
-		return defaultVal[0]
-	}
-	return val
-}
-
-// MustInt always returns value without error,
-// it returns 0 if error occurs.
-func (k *Key) MustInt(defaultVal ...int) int {
-	val, err := k.Int()
-	if len(defaultVal) > 0 && err != nil {
-		return defaultVal[0]
-	}
-	return val
-}
-
-// MustInt64 always returns value without error,
-// it returns 0 if error occurs.
-func (k *Key) MustInt64(defaultVal ...int64) int64 {
-	val, err := k.Int64()
-	if len(defaultVal) > 0 && err != nil {
-		return defaultVal[0]
-	}
-	return val
-}
-
-// MustTimeFormat always parses with given format and returns value without error,
-// it returns zero value if error occurs.
-func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time {
-	val, err := k.TimeFormat(format)
-	if len(defaultVal) > 0 && err != nil {
-		return defaultVal[0]
-	}
-	return val
-}
-
-// MustTime always parses with RFC3339 format and returns value without error,
-// it returns zero value if error occurs.
-func (k *Key) MustTime(defaultVal ...time.Time) time.Time {
-	return k.MustTimeFormat(time.RFC3339, defaultVal...)
-}
-
-// In always returns value without error,
-// it returns default value if error occurs or doesn't fit into candidates.
-func (k *Key) In(defaultVal string, candidates []string) string {
-	val := k.String()
-	for _, cand := range candidates {
-		if val == cand {
-			return val
-		}
-	}
-	return defaultVal
-}
-
-// InFloat64 always returns value without error,
-// it returns default value if error occurs or doesn't fit into candidates.
-func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 {
-	val := k.MustFloat64()
-	for _, cand := range candidates {
-		if val == cand {
-			return val
-		}
-	}
-	return defaultVal
-}
-
-// InInt always returns value without error,
-// it returns default value if error occurs or doesn't fit into candidates.
-func (k *Key) InInt(defaultVal int, candidates []int) int {
-	val := k.MustInt()
-	for _, cand := range candidates {
-		if val == cand {
-			return val
-		}
-	}
-	return defaultVal
-}
-
-// InInt64 always returns value without error,
-// it returns default value if error occurs or doesn't fit into candidates.
-func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 {
-	val := k.MustInt64()
-	for _, cand := range candidates {
-		if val == cand {
-			return val
-		}
-	}
-	return defaultVal
-}
-
-// InTimeFormat always parses with given format and returns value without error,
-// it returns default value if error occurs or doesn't fit into candidates.
-func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time {
-	val := k.MustTimeFormat(format)
-	for _, cand := range candidates {
-		if val == cand {
-			return val
-		}
-	}
-	return defaultVal
-}
-
-// InTime always parses with RFC3339 format and returns value without error,
-// it returns default value if error occurs or doesn't fit into candidates.
-func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time {
-	return k.InTimeFormat(time.RFC3339, defaultVal, candidates)
-}
-
-// RangeFloat64 checks if value is in given range inclusively,
-// and returns default value if it's not.
-func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 {
-	val := k.MustFloat64()
-	if val < min || val > max {
-		return defaultVal
-	}
-	return val
-}
-
-// RangeInt checks if value is in given range inclusively,
-// and returns default value if it's not.
-func (k *Key) RangeInt(defaultVal, min, max int) int {
-	val := k.MustInt()
-	if val < min || val > max {
-		return defaultVal
-	}
-	return val
-}
-
-// RangeInt64 checks if value is in given range inclusively,
-// and returns default value if it's not.
-func (k *Key) RangeInt64(defaultVal, min, max int64) int64 {
-	val := k.MustInt64()
-	if val < min || val > max {
-		return defaultVal
-	}
-	return val
-}
-
-// RangeTimeFormat checks if value with given format is in given range inclusively,
-// and returns default value if it's not.
-func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time {
-	val := k.MustTimeFormat(format)
-	if val.Unix() < min.Unix() || val.Unix() > max.Unix() {
-		return defaultVal
-	}
-	return val
-}
-
-// RangeTime checks if value with RFC3339 format is in given range inclusively,
-// and returns default value if it's not.
-func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time {
-	return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max)
-}
-
-// Strings returns list of string devide by given delimiter.
-func (k *Key) Strings(delim string) []string {
-	str := k.String()
-	if len(str) == 0 {
-		return []string{}
-	}
-
-	vals := strings.Split(str, delim)
-	for i := range vals {
-		vals[i] = strings.TrimSpace(vals[i])
-	}
-	return vals
-}
-
-// Float64s returns list of float64 devide by given delimiter.
-func (k *Key) Float64s(delim string) []float64 {
-	strs := k.Strings(delim)
-	vals := make([]float64, len(strs))
-	for i := range strs {
-		vals[i], _ = strconv.ParseFloat(strs[i], 64)
-	}
-	return vals
-}
-
-// Ints returns list of int devide by given delimiter.
-func (k *Key) Ints(delim string) []int {
-	strs := k.Strings(delim)
-	vals := make([]int, len(strs))
-	for i := range strs {
-		vals[i], _ = strconv.Atoi(strs[i])
-	}
-	return vals
-}
-
-// Int64s returns list of int64 devide by given delimiter.
-func (k *Key) Int64s(delim string) []int64 {
-	strs := k.Strings(delim)
-	vals := make([]int64, len(strs))
-	for i := range strs {
-		vals[i], _ = strconv.ParseInt(strs[i], 10, 64)
-	}
-	return vals
-}
-
-// TimesFormat parses with given format and returns list of time.Time devide by given delimiter.
-func (k *Key) TimesFormat(format, delim string) []time.Time {
-	strs := k.Strings(delim)
-	vals := make([]time.Time, len(strs))
-	for i := range strs {
-		vals[i], _ = time.Parse(format, strs[i])
-	}
-	return vals
-}
-
-// Times parses with RFC3339 format and returns list of time.Time devide by given delimiter.
-func (k *Key) Times(delim string) []time.Time {
-	return k.TimesFormat(time.RFC3339, delim)
-}
-
-// SetValue changes key value.
-func (k *Key) SetValue(v string) {
-	k.value = v
-}
-
-//   _________              __  .__
-//  /   _____/ ____   _____/  |_|__| ____   ____
-//  \_____  \_/ __ \_/ ___\   __\  |/  _ \ /    \
-//  /        \  ___/\  \___|  | |  (  <_> )   |  \
-// /_______  /\___  >\___  >__| |__|\____/|___|  /
-//         \/     \/     \/                    \/
-
-// Section represents a config section.
-type Section struct {
-	f        *File
-	Comment  string
-	name     string
-	keys     map[string]*Key
-	keyList  []string
-	keysHash map[string]string
-}
-
-func newSection(f *File, name string) *Section {
-	return &Section{f, "", name, make(map[string]*Key), make([]string, 0, 10), make(map[string]string)}
-}
-
-// Name returns name of Section.
-func (s *Section) Name() string {
-	return s.name
-}
-
-// NewKey creates a new key to given section.
-func (s *Section) NewKey(name, val string) (*Key, error) {
-	if len(name) == 0 {
-		return nil, errors.New("error creating new key: empty key name")
-	}
-
-	if s.f.BlockMode {
-		s.f.lock.Lock()
-		defer s.f.lock.Unlock()
-	}
-
-	if inSlice(name, s.keyList) {
-		s.keys[name].value = val
-		return s.keys[name], nil
-	}
-
-	s.keyList = append(s.keyList, name)
-	s.keys[name] = &Key{s, "", name, val, false}
-	s.keysHash[name] = val
-	return s.keys[name], nil
-}
-
-// GetKey returns key in section by given name.
-func (s *Section) GetKey(name string) (*Key, error) {
-	// FIXME: change to section level lock?
-	if s.f.BlockMode {
-		s.f.lock.RLock()
-		defer s.f.lock.RUnlock()
-	}
-
-	key := s.keys[name]
-	if key == nil {
-		// Check if it is a child-section.
-		if i := strings.LastIndex(s.name, "."); i > -1 {
-			return s.f.Section(s.name[:i]).GetKey(name)
-		}
-		return nil, fmt.Errorf("error when getting key of section '%s': key '%s' not exists", s.name, name)
-	}
-	return key, nil
-}
-
-// Key assumes named Key exists in section and returns a zero-value when not.
-func (s *Section) Key(name string) *Key {
-	key, err := s.GetKey(name)
-	if err != nil {
-		// It's OK here because the only possible error is empty key name,
-		// but if it's empty, this piece of code won't be executed.
-		key, _ = s.NewKey(name, "")
-		return key
-	}
-	return key
+type bytesReadCloser struct {
+	reader io.Reader
 }
 }
 
 
-// Keys returns list of keys of section.
-func (s *Section) Keys() []*Key {
-	keys := make([]*Key, len(s.keyList))
-	for i := range s.keyList {
-		keys[i] = s.Key(s.keyList[i])
-	}
-	return keys
+func (rc *bytesReadCloser) Read(p []byte) (n int, err error) {
+	return rc.reader.Read(p)
 }
 }
 
 
-// KeyStrings returns list of key names of section.
-func (s *Section) KeyStrings() []string {
-	list := make([]string, len(s.keyList))
-	copy(list, s.keyList)
-	return list
+func (rc *bytesReadCloser) Close() error {
+	return nil
 }
 }
 
 
-// KeysHash returns keys hash consisting of names and values.
-func (s *Section) KeysHash() map[string]string {
-	if s.f.BlockMode {
-		s.f.lock.RLock()
-		defer s.f.lock.RUnlock()
-	}
-
-	hash := map[string]string{}
-	for key, value := range s.keysHash {
-		hash[key] = value
-	}
-	return hash
+// sourceData represents an object that contains content in memory.
+type sourceData struct {
+	data []byte
 }
 }
 
 
-// DeleteKey deletes a key from section.
-func (s *Section) DeleteKey(name string) {
-	if s.f.BlockMode {
-		s.f.lock.Lock()
-		defer s.f.lock.Unlock()
-	}
-
-	for i, k := range s.keyList {
-		if k == name {
-			s.keyList = append(s.keyList[:i], s.keyList[i+1:]...)
-			delete(s.keys, name)
-			return
-		}
-	}
+func (s *sourceData) ReadCloser() (io.ReadCloser, error) {
+	return &bytesReadCloser{bytes.NewReader(s.data)}, nil
 }
 }
 
 
-// ___________.__.__
-// \_   _____/|__|  |   ____
-//  |    __)  |  |  | _/ __ \
-//  |     \   |  |  |_\  ___/
-//  \___  /   |__|____/\___  >
-//      \/                 \/
-
 // File represents a combination of a or more INI file(s) in memory.
 // File represents a combination of a or more INI file(s) in memory.
 type File struct {
 type File struct {
 	// Should make things safe, but sometimes doesn't matter.
 	// Should make things safe, but sometimes doesn't matter.
@@ -577,16 +126,20 @@ type File struct {
 	// To keep data in order.
 	// To keep data in order.
 	sectionList []string
 	sectionList []string
 
 
+	options LoadOptions
+
 	NameMapper
 	NameMapper
+	ValueMapper
 }
 }
 
 
 // newFile initializes File object with given data sources.
 // newFile initializes File object with given data sources.
-func newFile(dataSources []dataSource) *File {
+func newFile(dataSources []dataSource, opts LoadOptions) *File {
 	return &File{
 	return &File{
 		BlockMode:   true,
 		BlockMode:   true,
 		dataSources: dataSources,
 		dataSources: dataSources,
 		sections:    make(map[string]*Section),
 		sections:    make(map[string]*Section),
 		sectionList: make([]string, 0, 10),
 		sectionList: make([]string, 0, 10),
+		options:     opts,
 	}
 	}
 }
 }
 
 
@@ -601,9 +154,19 @@ func parseDataSource(source interface{}) (dataSource, error) {
 	}
 	}
 }
 }
 
 
-// Load loads and parses from INI data sources.
-// Arguments can be mixed of file name with string type, or raw data in []byte.
-func Load(source interface{}, others ...interface{}) (_ *File, err error) {
+type LoadOptions struct {
+	// Loose indicates whether the parser should ignore nonexistent files or return error.
+	Loose bool
+	// Insensitive indicates whether the parser forces all section and key names to lowercase.
+	Insensitive bool
+	// IgnoreContinuation indicates whether to ignore continuation lines while parsing.
+	IgnoreContinuation bool
+	// AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing.
+	// This type of keys are mostly used in my.cnf.
+	AllowBooleanKeys bool
+}
+
+func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) {
 	sources := make([]dataSource, len(others)+1)
 	sources := make([]dataSource, len(others)+1)
 	sources[0], err = parseDataSource(source)
 	sources[0], err = parseDataSource(source)
 	if err != nil {
 	if err != nil {
@@ -615,8 +178,30 @@ func Load(source interface{}, others ...interface{}) (_ *File, err error) {
 			return nil, err
 			return nil, err
 		}
 		}
 	}
 	}
-	f := newFile(sources)
-	return f, f.Reload()
+	f := newFile(sources, opts)
+	if err = f.Reload(); err != nil {
+		return nil, err
+	}
+	return f, nil
+}
+
+// Load loads and parses from INI data sources.
+// Arguments can be mixed of file name with string type, or raw data in []byte.
+// It will return error if list contains nonexistent files.
+func Load(source interface{}, others ...interface{}) (*File, error) {
+	return LoadSources(LoadOptions{}, source, others...)
+}
+
+// LooseLoad has exactly same functionality as Load function
+// except it ignores nonexistent files instead of returning error.
+func LooseLoad(source interface{}, others ...interface{}) (*File, error) {
+	return LoadSources(LoadOptions{Loose: true}, source, others...)
+}
+
+// InsensitiveLoad has exactly same functionality as Load function
+// except it forces all section and key names to be lowercased.
+func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) {
+	return LoadSources(LoadOptions{Insensitive: true}, source, others...)
 }
 }
 
 
 // Empty returns an empty file object.
 // Empty returns an empty file object.
@@ -630,6 +215,8 @@ func Empty() *File {
 func (f *File) NewSection(name string) (*Section, error) {
 func (f *File) NewSection(name string) (*Section, error) {
 	if len(name) == 0 {
 	if len(name) == 0 {
 		return nil, errors.New("error creating new section: empty section name")
 		return nil, errors.New("error creating new section: empty section name")
+	} else if f.options.Insensitive && name != DEFAULT_SECTION {
+		name = strings.ToLower(name)
 	}
 	}
 
 
 	if f.BlockMode {
 	if f.BlockMode {
@@ -660,6 +247,8 @@ func (f *File) NewSections(names ...string) (err error) {
 func (f *File) GetSection(name string) (*Section, error) {
 func (f *File) GetSection(name string) (*Section, error) {
 	if len(name) == 0 {
 	if len(name) == 0 {
 		name = DEFAULT_SECTION
 		name = DEFAULT_SECTION
+	} else if f.options.Insensitive {
+		name = strings.ToLower(name)
 	}
 	}
 
 
 	if f.BlockMode {
 	if f.BlockMode {
@@ -669,7 +258,7 @@ func (f *File) GetSection(name string) (*Section, error) {
 
 
 	sec := f.sections[name]
 	sec := f.sections[name]
 	if sec == nil {
 	if sec == nil {
-		return nil, fmt.Errorf("error when getting section: section '%s' not exists", name)
+		return nil, fmt.Errorf("section '%s' does not exist", name)
 	}
 	}
 	return sec, nil
 	return sec, nil
 }
 }
@@ -678,7 +267,7 @@ func (f *File) GetSection(name string) (*Section, error) {
 func (f *File) Section(name string) *Section {
 func (f *File) Section(name string) *Section {
 	sec, err := f.GetSection(name)
 	sec, err := f.GetSection(name)
 	if err != nil {
 	if err != nil {
-		// It's OK here because the only possible error is empty section name,
+		// Note: It's OK here because the only possible error is empty section name,
 		// but if it's empty, this piece of code won't be executed.
 		// but if it's empty, this piece of code won't be executed.
 		sec, _ = f.NewSection(name)
 		sec, _ = f.NewSection(name)
 		return sec
 		return sec
@@ -722,200 +311,25 @@ func (f *File) DeleteSection(name string) {
 	}
 	}
 }
 }
 
 
-func cutComment(str string) string {
-	i := strings.Index(str, "#")
-	if i == -1 {
-		return str
-	}
-	return str[:i]
-}
-
-// parse parses data through an io.Reader.
-func (f *File) parse(reader io.Reader) error {
-	buf := bufio.NewReader(reader)
-
-	// Handle BOM-UTF8.
-	// http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding
-	mask, err := buf.Peek(3)
-	if err == nil && len(mask) >= 3 && mask[0] == 239 && mask[1] == 187 && mask[2] == 191 {
-		buf.Read(mask)
-	}
-
-	count := 1
-	comments := ""
-	isEnd := false
-
-	section, err := f.NewSection(DEFAULT_SECTION)
+func (f *File) reload(s dataSource) error {
+	r, err := s.ReadCloser()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
+	defer r.Close()
 
 
-	for {
-		line, err := buf.ReadString('\n')
-		line = strings.TrimSpace(line)
-		length := len(line)
-
-		// Check error and ignore io.EOF just for a moment.
-		if err != nil {
-			if err != io.EOF {
-				return fmt.Errorf("error reading next line: %v", err)
-			}
-			// The last line of file could be an empty line.
-			if length == 0 {
-				break
-			}
-			isEnd = true
-		}
-
-		// Skip empty lines.
-		if length == 0 {
-			continue
-		}
-
-		switch {
-		case line[0] == '#' || line[0] == ';': // Comments.
-			if len(comments) == 0 {
-				comments = line
-			} else {
-				comments += LineBreak + line
-			}
-			continue
-		case line[0] == '[' && line[length-1] == ']': // New sction.
-			name := strings.TrimSpace(line[1 : length-1])
-			section, err = f.NewSection(name)
-			if err != nil {
-				return err
-			}
-
-			if len(comments) > 0 {
-				section.Comment = comments
-				comments = ""
-			}
-			// Reset counter.
-			count = 1
-			continue
-		}
-
-		// Other possibilities.
-		var (
-			i        int
-			keyQuote string
-			kname    string
-			valQuote string
-			val      string
-		)
-
-		// Key name surrounded by quotes.
-		if line[0] == '"' {
-			if length > 6 && line[0:3] == `"""` {
-				keyQuote = `"""`
-			} else {
-				keyQuote = `"`
-			}
-		} else if line[0] == '`' {
-			keyQuote = "`"
-		}
-		if len(keyQuote) > 0 {
-			qLen := len(keyQuote)
-			pos := strings.Index(line[qLen:], keyQuote)
-			if pos == -1 {
-				return fmt.Errorf("error parsing line: missing closing key quote: %s", line)
-			}
-			pos = pos + qLen
-			i = strings.IndexAny(line[pos:], "=:")
-			if i < 0 {
-				return fmt.Errorf("error parsing line: key-value delimiter not found: %s", line)
-			} else if i == pos {
-				return fmt.Errorf("error parsing line: key is empty: %s", line)
-			}
-			i = i + pos
-			kname = line[qLen:pos] // Just keep spaces inside quotes.
-		} else {
-			i = strings.IndexAny(line, "=:")
-			if i < 0 {
-				return fmt.Errorf("error parsing line: key-value delimiter not found: %s", line)
-			} else if i == 0 {
-				return fmt.Errorf("error parsing line: key is empty: %s", line)
-			}
-			kname = strings.TrimSpace(line[0:i])
-		}
-
-		isAutoIncr := false
-		// Auto increment.
-		if kname == "-" {
-			isAutoIncr = true
-			kname = "#" + fmt.Sprint(count)
-			count++
-		}
-
-		lineRight := strings.TrimSpace(line[i+1:])
-		lineRightLength := len(lineRight)
-		firstChar := ""
-		if lineRightLength >= 2 {
-			firstChar = lineRight[0:1]
-		}
-		if firstChar == "`" {
-			valQuote = "`"
-		} else if lineRightLength >= 6 && lineRight[0:3] == `"""` {
-			valQuote = `"""`
-		}
-		if len(valQuote) > 0 {
-			qLen := len(valQuote)
-			pos := strings.LastIndex(lineRight[qLen:], valQuote)
-			// For multiple lines value.
-			if pos == -1 {
-				isEnd := false
-				val = lineRight[qLen:] + "\n"
-				for {
-					next, err := buf.ReadString('\n')
-					if err != nil {
-						if err != io.EOF {
-							return err
-						}
-						isEnd = true
-					}
-					pos = strings.LastIndex(next, valQuote)
-					if pos > -1 {
-						val += next[:pos]
-						break
-					}
-					val += next
-					if isEnd {
-						return fmt.Errorf("error parsing line: missing closing key quote from '%s' to '%s'", line, next)
-					}
-				}
-			} else {
-				val = lineRight[qLen : pos+qLen]
-			}
-		} else {
-			val = strings.TrimSpace(cutComment(lineRight[0:]))
-		}
-
-		k, err := section.NewKey(kname, val)
-		if err != nil {
-			return err
-		}
-		k.isAutoIncr = isAutoIncr
-		if len(comments) > 0 {
-			k.Comment = comments
-			comments = ""
-		}
-
-		if isEnd {
-			break
-		}
-	}
-	return nil
+	return f.parse(r)
 }
 }
 
 
 // Reload reloads and parses all data sources.
 // Reload reloads and parses all data sources.
-func (f *File) Reload() error {
+func (f *File) Reload() (err error) {
 	for _, s := range f.dataSources {
 	for _, s := range f.dataSources {
-		r, err := s.Reader()
-		if err != nil {
-			return err
-		}
-		if err = f.parse(r); err != nil {
+		if err = f.reload(s); err != nil {
+			// In loose mode, we create an empty default section for nonexistent files.
+			if os.IsNotExist(err) && f.options.Loose {
+				f.parse(bytes.NewBuffer(nil))
+				continue
+			}
 			return err
 			return err
 		}
 		}
 	}
 	}
@@ -939,8 +353,10 @@ func (f *File) Append(source interface{}, others ...interface{}) error {
 	return f.Reload()
 	return f.Reload()
 }
 }
 
 
-// SaveTo writes content to filesystem.
-func (f *File) SaveTo(filename string) (err error) {
+// WriteToIndent writes content into io.Writer with given indention.
+// If PrettyFormat has been set to be true,
+// it will align "=" sign with spaces under each section.
+func (f *File) WriteToIndent(w io.Writer, indent string) (n int64, err error) {
 	equalSign := "="
 	equalSign := "="
 	if PrettyFormat {
 	if PrettyFormat {
 		equalSign = " = "
 		equalSign = " = "
@@ -955,63 +371,131 @@ func (f *File) SaveTo(filename string) (err error) {
 				sec.Comment = "; " + sec.Comment
 				sec.Comment = "; " + sec.Comment
 			}
 			}
 			if _, err = buf.WriteString(sec.Comment + LineBreak); err != nil {
 			if _, err = buf.WriteString(sec.Comment + LineBreak); err != nil {
-				return err
+				return 0, err
 			}
 			}
 		}
 		}
 
 
-		if i > 0 {
+		if i > 0 || DefaultHeader {
 			if _, err = buf.WriteString("[" + sname + "]" + LineBreak); err != nil {
 			if _, err = buf.WriteString("[" + sname + "]" + LineBreak); err != nil {
-				return err
+				return 0, err
 			}
 			}
 		} else {
 		} else {
-			// Write nothing if default section is empty.
+			// Write nothing if default section is empty
 			if len(sec.keyList) == 0 {
 			if len(sec.keyList) == 0 {
 				continue
 				continue
 			}
 			}
 		}
 		}
 
 
+		// Count and generate alignment length and buffer spaces using the
+		// longest key. Keys may be modifed if they contain certain characters so
+		// we need to take that into account in our calculation.
+		alignLength := 0
+		if PrettyFormat {
+			for _, kname := range sec.keyList {
+				keyLength := len(kname)
+				// First case will surround key by ` and second by """
+				if strings.ContainsAny(kname, "\"=:") {
+					keyLength += 2
+				} else if strings.Contains(kname, "`") {
+					keyLength += 6
+				}
+
+				if keyLength > alignLength {
+					alignLength = keyLength
+				}
+			}
+		}
+		alignSpaces := bytes.Repeat([]byte(" "), alignLength)
+
 		for _, kname := range sec.keyList {
 		for _, kname := range sec.keyList {
 			key := sec.Key(kname)
 			key := sec.Key(kname)
 			if len(key.Comment) > 0 {
 			if len(key.Comment) > 0 {
+				if len(indent) > 0 && sname != DEFAULT_SECTION {
+					buf.WriteString(indent)
+				}
 				if key.Comment[0] != '#' && key.Comment[0] != ';' {
 				if key.Comment[0] != '#' && key.Comment[0] != ';' {
 					key.Comment = "; " + key.Comment
 					key.Comment = "; " + key.Comment
 				}
 				}
 				if _, err = buf.WriteString(key.Comment + LineBreak); err != nil {
 				if _, err = buf.WriteString(key.Comment + LineBreak); err != nil {
-					return err
+					return 0, err
 				}
 				}
 			}
 			}
 
 
+			if len(indent) > 0 && sname != DEFAULT_SECTION {
+				buf.WriteString(indent)
+			}
+
 			switch {
 			switch {
-			case key.isAutoIncr:
+			case key.isAutoIncrement:
 				kname = "-"
 				kname = "-"
-			case strings.Contains(kname, "`") || strings.Contains(kname, `"`):
-				kname = `"""` + kname + `"""`
-			case strings.Contains(kname, `=`) || strings.Contains(kname, `:`):
+			case strings.ContainsAny(kname, "\"=:"):
 				kname = "`" + kname + "`"
 				kname = "`" + kname + "`"
+			case strings.Contains(kname, "`"):
+				kname = `"""` + kname + `"""`
+			}
+			if _, err = buf.WriteString(kname); err != nil {
+				return 0, err
+			}
+
+			if key.isBooleanType {
+				continue
+			}
+
+			// Write out alignment spaces before "=" sign
+			if PrettyFormat {
+				buf.Write(alignSpaces[:alignLength-len(kname)])
 			}
 			}
 
 
 			val := key.value
 			val := key.value
-			// In case key value contains "\n", "`" or "\"".
-			if strings.Contains(val, "\n") || strings.Contains(val, "`") || strings.Contains(val, `"`) {
+			// In case key value contains "\n", "`", "\"", "#" or ";"
+			if strings.ContainsAny(val, "\n`") {
 				val = `"""` + val + `"""`
 				val = `"""` + val + `"""`
+			} else if strings.ContainsAny(val, "#;") {
+				val = "`" + val + "`"
 			}
 			}
-			if _, err = buf.WriteString(kname + equalSign + val + LineBreak); err != nil {
-				return err
+			if _, err = buf.WriteString(equalSign + val + LineBreak); err != nil {
+				return 0, err
 			}
 			}
 		}
 		}
 
 
-		// Put a line between sections.
+		// Put a line between sections
 		if _, err = buf.WriteString(LineBreak); err != nil {
 		if _, err = buf.WriteString(LineBreak); err != nil {
-			return err
+			return 0, err
 		}
 		}
 	}
 	}
 
 
-	fw, err := os.Create(filename)
+	return buf.WriteTo(w)
+}
+
+// WriteTo writes file content into io.Writer.
+func (f *File) WriteTo(w io.Writer) (int64, error) {
+	return f.WriteToIndent(w, "")
+}
+
+// SaveToIndent writes content to file system with given value indention.
+func (f *File) SaveToIndent(filename, indent string) error {
+	// Note: Because we are truncating with os.Create,
+	// 	so it's safer to save to a temporary file location and rename afte done.
+	tmpPath := filename + "." + strconv.Itoa(time.Now().Nanosecond()) + ".tmp"
+	defer os.Remove(tmpPath)
+
+	fw, err := os.Create(tmpPath)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	if _, err = buf.WriteTo(fw); err != nil {
+
+	if _, err = f.WriteToIndent(fw, indent); err != nil {
+		fw.Close()
 		return err
 		return err
 	}
 	}
-	return fw.Close()
+	fw.Close()
+
+	// Remove old file and rename the new one.
+	os.Remove(filename)
+	return os.Rename(tmpPath, filename)
+}
+
+// SaveTo writes content to file system.
+func (f *File) SaveTo(filename string) error {
+	return f.SaveToIndent(filename, "")
 }
 }

+ 0 - 456
Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go

@@ -1,456 +0,0 @@
-// Copyright 2014 Unknwon
-//
-// Licensed under the Apache License, Version 2.0 (the "License"): you may
-// not use this file except in compliance with the License. You may obtain
-// a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations
-// under the License.
-
-package ini
-
-import (
-	"fmt"
-	"strings"
-	"testing"
-	"time"
-
-	. "github.com/smartystreets/goconvey/convey"
-)
-
-func Test_Version(t *testing.T) {
-	Convey("Get version", t, func() {
-		So(Version(), ShouldEqual, _VERSION)
-	})
-}
-
-const _CONF_DATA = `
-; Package name
-NAME = ini
-; Package version
-VERSION = v1
-; Package import path
-IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
-
-# Information about package author
-# Bio can be written in multiple lines.
-[author]
-NAME = Unknwon  # Succeeding comment
-E-MAIL = fake@localhost
-GITHUB = https://github.com/%(NAME)s
-BIO = """Gopher.
-Coding addict.
-Good man.
-"""  # Succeeding comment
-
-[package]
-CLONE_URL = https://%(IMPORT_PATH)s
-
-[package.sub]
-UNUSED_KEY = should be deleted
-
-[features]
--: Support read/write comments of keys and sections
--: Support auto-increment of key names
--: Support load multiple files to overwrite key values
-
-[types]
-STRING = str
-BOOL = true
-BOOL_FALSE = false
-FLOAT64 = 1.25
-INT = 10
-TIME = 2015-01-01T20:17:05Z
-
-[array]
-STRINGS = en, zh, de
-FLOAT64S = 1.1, 2.2, 3.3
-INTS = 1, 2, 3
-TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z
-
-[note]
-
-[advance]
-true = """"2+3=5""""
-"1+1=2" = true
-"""6+1=7""" = true
-"""` + "`" + `5+5` + "`" + `""" = 10
-""""6+6"""" = 12
-` + "`" + `7-2=4` + "`" + ` = false
-ADDRESS = ` + "`" + `404 road,
-NotFound, State, 50000` + "`"
-
-func Test_Load(t *testing.T) {
-	Convey("Load from data sources", t, func() {
-
-		Convey("Load with empty data", func() {
-			So(Empty(), ShouldNotBeNil)
-		})
-
-		Convey("Load with multiple data sources", func() {
-			cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini")
-			So(err, ShouldBeNil)
-			So(cfg, ShouldNotBeNil)
-		})
-	})
-
-	Convey("Bad load process", t, func() {
-
-		Convey("Load from invalid data sources", func() {
-			_, err := Load(_CONF_DATA)
-			So(err, ShouldNotBeNil)
-
-			_, err = Load("testdata/404.ini")
-			So(err, ShouldNotBeNil)
-
-			_, err = Load(1)
-			So(err, ShouldNotBeNil)
-
-			_, err = Load([]byte(""), 1)
-			So(err, ShouldNotBeNil)
-		})
-
-		Convey("Load with empty section name", func() {
-			_, err := Load([]byte("[]"))
-			So(err, ShouldNotBeNil)
-		})
-
-		Convey("Load with bad keys", func() {
-			_, err := Load([]byte(`"""name`))
-			So(err, ShouldNotBeNil)
-
-			_, err = Load([]byte(`"""name"""`))
-			So(err, ShouldNotBeNil)
-
-			_, err = Load([]byte(`""=1`))
-			So(err, ShouldNotBeNil)
-
-			_, err = Load([]byte(`=`))
-			So(err, ShouldNotBeNil)
-
-			_, err = Load([]byte(`name`))
-			So(err, ShouldNotBeNil)
-		})
-
-		Convey("Load with bad values", func() {
-			_, err := Load([]byte(`name="""Unknwon`))
-			So(err, ShouldNotBeNil)
-		})
-	})
-}
-
-func Test_Values(t *testing.T) {
-	Convey("Test getting and setting values", t, func() {
-		cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini")
-		So(err, ShouldBeNil)
-		So(cfg, ShouldNotBeNil)
-
-		Convey("Get values in default section", func() {
-			sec := cfg.Section("")
-			So(sec, ShouldNotBeNil)
-			So(sec.Key("NAME").Value(), ShouldEqual, "ini")
-			So(sec.Key("NAME").String(), ShouldEqual, "ini")
-			So(sec.Key("NAME").Comment, ShouldEqual, "; Package name")
-			So(sec.Key("IMPORT_PATH").String(), ShouldEqual, "gopkg.in/ini.v1")
-		})
-
-		Convey("Get values in non-default section", func() {
-			sec := cfg.Section("author")
-			So(sec, ShouldNotBeNil)
-			So(sec.Key("NAME").String(), ShouldEqual, "Unknwon")
-			So(sec.Key("GITHUB").String(), ShouldEqual, "https://github.com/Unknwon")
-
-			sec = cfg.Section("package")
-			So(sec, ShouldNotBeNil)
-			So(sec.Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1")
-		})
-
-		Convey("Get auto-increment key names", func() {
-			keys := cfg.Section("features").Keys()
-			for i, k := range keys {
-				So(k.Name(), ShouldEqual, fmt.Sprintf("#%d", i+1))
-			}
-		})
-
-		Convey("Get overwrite value", func() {
-			So(cfg.Section("author").Key("E-MAIL").String(), ShouldEqual, "u@gogs.io")
-		})
-
-		Convey("Get sections", func() {
-			sections := cfg.Sections()
-			for i, name := range []string{DEFAULT_SECTION, "author", "package", "package.sub", "features", "types", "array", "note", "advance"} {
-				So(sections[i].Name(), ShouldEqual, name)
-			}
-		})
-
-		Convey("Get parent section value", func() {
-			So(cfg.Section("package.sub").Key("CLONE_URL").String(), ShouldEqual, "https://gopkg.in/ini.v1")
-		})
-
-		Convey("Get multiple line value", func() {
-			So(cfg.Section("author").Key("BIO").String(), ShouldEqual, "Gopher.\nCoding addict.\nGood man.\n")
-		})
-
-		Convey("Get values with type", func() {
-			sec := cfg.Section("types")
-			v1, err := sec.Key("BOOL").Bool()
-			So(err, ShouldBeNil)
-			So(v1, ShouldBeTrue)
-
-			v1, err = sec.Key("BOOL_FALSE").Bool()
-			So(err, ShouldBeNil)
-			So(v1, ShouldBeFalse)
-
-			v2, err := sec.Key("FLOAT64").Float64()
-			So(err, ShouldBeNil)
-			So(v2, ShouldEqual, 1.25)
-
-			v3, err := sec.Key("INT").Int()
-			So(err, ShouldBeNil)
-			So(v3, ShouldEqual, 10)
-
-			v4, err := sec.Key("INT").Int64()
-			So(err, ShouldBeNil)
-			So(v4, ShouldEqual, 10)
-
-			t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
-			So(err, ShouldBeNil)
-			v5, err := sec.Key("TIME").Time()
-			So(err, ShouldBeNil)
-			So(v5.String(), ShouldEqual, t.String())
-
-			Convey("Must get values with type", func() {
-				So(sec.Key("STRING").MustString("404"), ShouldEqual, "str")
-				So(sec.Key("BOOL").MustBool(), ShouldBeTrue)
-				So(sec.Key("FLOAT64").MustFloat64(), ShouldEqual, 1.25)
-				So(sec.Key("INT").MustInt(), ShouldEqual, 10)
-				So(sec.Key("INT").MustInt64(), ShouldEqual, 10)
-				So(sec.Key("TIME").MustTime().String(), ShouldEqual, t.String())
-
-				Convey("Must get values with default value", func() {
-					So(sec.Key("STRING_404").MustString("404"), ShouldEqual, "404")
-					So(sec.Key("BOOL_404").MustBool(true), ShouldBeTrue)
-					So(sec.Key("FLOAT64_404").MustFloat64(2.5), ShouldEqual, 2.5)
-					So(sec.Key("INT_404").MustInt(15), ShouldEqual, 15)
-					So(sec.Key("INT_404").MustInt64(15), ShouldEqual, 15)
-
-					t, err := time.Parse(time.RFC3339, "2014-01-01T20:17:05Z")
-					So(err, ShouldBeNil)
-					So(sec.Key("TIME_404").MustTime(t).String(), ShouldEqual, t.String())
-				})
-			})
-		})
-
-		Convey("Get value with candidates", func() {
-			sec := cfg.Section("types")
-			So(sec.Key("STRING").In("", []string{"str", "arr", "types"}), ShouldEqual, "str")
-			So(sec.Key("FLOAT64").InFloat64(0, []float64{1.25, 2.5, 3.75}), ShouldEqual, 1.25)
-			So(sec.Key("INT").InInt(0, []int{10, 20, 30}), ShouldEqual, 10)
-			So(sec.Key("INT").InInt64(0, []int64{10, 20, 30}), ShouldEqual, 10)
-
-			zt, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z")
-			So(err, ShouldBeNil)
-			t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
-			So(err, ShouldBeNil)
-			So(sec.Key("TIME").InTime(zt, []time.Time{t, time.Now(), time.Now().Add(1 * time.Second)}).String(), ShouldEqual, t.String())
-
-			Convey("Get value with candidates and default value", func() {
-				So(sec.Key("STRING_404").In("str", []string{"str", "arr", "types"}), ShouldEqual, "str")
-				So(sec.Key("FLOAT64_404").InFloat64(1.25, []float64{1.25, 2.5, 3.75}), ShouldEqual, 1.25)
-				So(sec.Key("INT_404").InInt(10, []int{10, 20, 30}), ShouldEqual, 10)
-				So(sec.Key("INT64_404").InInt64(10, []int64{10, 20, 30}), ShouldEqual, 10)
-				So(sec.Key("TIME_404").InTime(t, []time.Time{time.Now(), time.Now(), time.Now().Add(1 * time.Second)}).String(), ShouldEqual, t.String())
-			})
-		})
-
-		Convey("Get values in range", func() {
-			sec := cfg.Section("types")
-			So(sec.Key("FLOAT64").RangeFloat64(0, 1, 2), ShouldEqual, 1.25)
-			So(sec.Key("INT").RangeInt(0, 10, 20), ShouldEqual, 10)
-			So(sec.Key("INT").RangeInt64(0, 10, 20), ShouldEqual, 10)
-
-			minT, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z")
-			So(err, ShouldBeNil)
-			midT, err := time.Parse(time.RFC3339, "2013-01-01T01:00:00Z")
-			So(err, ShouldBeNil)
-			maxT, err := time.Parse(time.RFC3339, "9999-01-01T01:00:00Z")
-			So(err, ShouldBeNil)
-			t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
-			So(err, ShouldBeNil)
-			So(sec.Key("TIME").RangeTime(t, minT, maxT).String(), ShouldEqual, t.String())
-
-			Convey("Get value in range with default value", func() {
-				So(sec.Key("FLOAT64").RangeFloat64(5, 0, 1), ShouldEqual, 5)
-				So(sec.Key("INT").RangeInt(7, 0, 5), ShouldEqual, 7)
-				So(sec.Key("INT").RangeInt64(7, 0, 5), ShouldEqual, 7)
-				So(sec.Key("TIME").RangeTime(t, minT, midT).String(), ShouldEqual, t.String())
-			})
-		})
-
-		Convey("Get values into slice", func() {
-			sec := cfg.Section("array")
-			So(strings.Join(sec.Key("STRINGS").Strings(","), ","), ShouldEqual, "en,zh,de")
-			So(len(sec.Key("STRINGS_404").Strings(",")), ShouldEqual, 0)
-
-			vals1 := sec.Key("FLOAT64S").Float64s(",")
-			for i, v := range []float64{1.1, 2.2, 3.3} {
-				So(vals1[i], ShouldEqual, v)
-			}
-
-			vals2 := sec.Key("INTS").Ints(",")
-			for i, v := range []int{1, 2, 3} {
-				So(vals2[i], ShouldEqual, v)
-			}
-
-			vals3 := sec.Key("INTS").Int64s(",")
-			for i, v := range []int64{1, 2, 3} {
-				So(vals3[i], ShouldEqual, v)
-			}
-
-			t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
-			So(err, ShouldBeNil)
-			vals4 := sec.Key("TIMES").Times(",")
-			for i, v := range []time.Time{t, t, t} {
-				So(vals4[i].String(), ShouldEqual, v.String())
-			}
-		})
-
-		Convey("Get key hash", func() {
-			cfg.Section("").KeysHash()
-		})
-
-		Convey("Set key value", func() {
-			k := cfg.Section("author").Key("NAME")
-			k.SetValue("无闻")
-			So(k.String(), ShouldEqual, "无闻")
-		})
-
-		Convey("Get key strings", func() {
-			So(strings.Join(cfg.Section("types").KeyStrings(), ","), ShouldEqual, "STRING,BOOL,BOOL_FALSE,FLOAT64,INT,TIME")
-		})
-
-		Convey("Delete a key", func() {
-			cfg.Section("package.sub").DeleteKey("UNUSED_KEY")
-			_, err := cfg.Section("package.sub").GetKey("UNUSED_KEY")
-			So(err, ShouldNotBeNil)
-		})
-
-		Convey("Get section strings", func() {
-			So(strings.Join(cfg.SectionStrings(), ","), ShouldEqual, "DEFAULT,author,package,package.sub,features,types,array,note,advance")
-		})
-
-		Convey("Delete a section", func() {
-			cfg.DeleteSection("")
-			So(cfg.SectionStrings()[0], ShouldNotEqual, DEFAULT_SECTION)
-		})
-
-		Convey("Create new sections", func() {
-			cfg.NewSections("test", "test2")
-			_, err := cfg.GetSection("test")
-			So(err, ShouldBeNil)
-			_, err = cfg.GetSection("test2")
-			So(err, ShouldBeNil)
-		})
-	})
-
-	Convey("Test getting and setting bad values", t, func() {
-		cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini")
-		So(err, ShouldBeNil)
-		So(cfg, ShouldNotBeNil)
-
-		Convey("Create new key with empty name", func() {
-			k, err := cfg.Section("").NewKey("", "")
-			So(err, ShouldNotBeNil)
-			So(k, ShouldBeNil)
-		})
-
-		Convey("Create new section with empty name", func() {
-			s, err := cfg.NewSection("")
-			So(err, ShouldNotBeNil)
-			So(s, ShouldBeNil)
-		})
-
-		Convey("Create new sections with empty name", func() {
-			So(cfg.NewSections(""), ShouldNotBeNil)
-		})
-
-		Convey("Get section that not exists", func() {
-			s, err := cfg.GetSection("404")
-			So(err, ShouldNotBeNil)
-			So(s, ShouldBeNil)
-
-			s = cfg.Section("404")
-			So(s, ShouldNotBeNil)
-		})
-	})
-}
-
-func Test_File_Append(t *testing.T) {
-	Convey("Append data sources", t, func() {
-		cfg, err := Load([]byte(""))
-		So(err, ShouldBeNil)
-		So(cfg, ShouldNotBeNil)
-
-		So(cfg.Append([]byte(""), []byte("")), ShouldBeNil)
-
-		Convey("Append bad data sources", func() {
-			So(cfg.Append(1), ShouldNotBeNil)
-			So(cfg.Append([]byte(""), 1), ShouldNotBeNil)
-		})
-	})
-}
-
-func Test_File_SaveTo(t *testing.T) {
-	Convey("Save file", t, func() {
-		cfg, err := Load([]byte(_CONF_DATA), "testdata/conf.ini")
-		So(err, ShouldBeNil)
-		So(cfg, ShouldNotBeNil)
-
-		cfg.Section("").Key("NAME").Comment = "Package name"
-		cfg.Section("author").Comment = `Information about package author
-# Bio can be written in multiple lines.`
-		So(cfg.SaveTo("testdata/conf_out.ini"), ShouldBeNil)
-	})
-}
-
-func Benchmark_Key_Value(b *testing.B) {
-	c, _ := Load([]byte(_CONF_DATA))
-	for i := 0; i < b.N; i++ {
-		c.Section("").Key("NAME").Value()
-	}
-}
-
-func Benchmark_Key_String(b *testing.B) {
-	c, _ := Load([]byte(_CONF_DATA))
-	for i := 0; i < b.N; i++ {
-		c.Section("").Key("NAME").String()
-	}
-}
-
-func Benchmark_Key_Value_NonBlock(b *testing.B) {
-	c, _ := Load([]byte(_CONF_DATA))
-	c.BlockMode = false
-	for i := 0; i < b.N; i++ {
-		c.Section("").Key("NAME").Value()
-	}
-}
-
-func Benchmark_Key_String_NonBlock(b *testing.B) {
-	c, _ := Load([]byte(_CONF_DATA))
-	c.BlockMode = false
-	for i := 0; i < b.N; i++ {
-		c.Section("").Key("NAME").String()
-	}
-}
-
-func Benchmark_Key_SetValue(b *testing.B) {
-	c, _ := Load([]byte(_CONF_DATA))
-	for i := 0; i < b.N; i++ {
-		c.Section("").Key("NAME").SetValue("10")
-	}
-}

+ 633 - 0
Godeps/_workspace/src/gopkg.in/ini.v1/key.go

@@ -0,0 +1,633 @@
+// Copyright 2014 Unknwon
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package ini
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// Key represents a key under a section.
+type Key struct {
+	s               *Section
+	name            string
+	value           string
+	isAutoIncrement bool
+	isBooleanType   bool
+
+	Comment string
+}
+
+// ValueMapper represents a mapping function for values, e.g. os.ExpandEnv
+type ValueMapper func(string) string
+
+// Name returns name of key.
+func (k *Key) Name() string {
+	return k.name
+}
+
+// Value returns raw value of key for performance purpose.
+func (k *Key) Value() string {
+	return k.value
+}
+
+// String returns string representation of value.
+func (k *Key) String() string {
+	val := k.value
+	if k.s.f.ValueMapper != nil {
+		val = k.s.f.ValueMapper(val)
+	}
+	if strings.Index(val, "%") == -1 {
+		return val
+	}
+
+	for i := 0; i < _DEPTH_VALUES; i++ {
+		vr := varPattern.FindString(val)
+		if len(vr) == 0 {
+			break
+		}
+
+		// Take off leading '%(' and trailing ')s'.
+		noption := strings.TrimLeft(vr, "%(")
+		noption = strings.TrimRight(noption, ")s")
+
+		// Search in the same section.
+		nk, err := k.s.GetKey(noption)
+		if err != nil {
+			// Search again in default section.
+			nk, _ = k.s.f.Section("").GetKey(noption)
+		}
+
+		// Substitute by new value and take off leading '%(' and trailing ')s'.
+		val = strings.Replace(val, vr, nk.value, -1)
+	}
+	return val
+}
+
+// Validate accepts a validate function which can
+// return modifed result as key value.
+func (k *Key) Validate(fn func(string) string) string {
+	return fn(k.String())
+}
+
+// parseBool returns the boolean value represented by the string.
+//
+// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On,
+// 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off.
+// Any other value returns an error.
+func parseBool(str string) (value bool, err error) {
+	switch str {
+	case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "y", "ON", "on", "On":
+		return true, nil
+	case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "n", "OFF", "off", "Off":
+		return false, nil
+	}
+	return false, fmt.Errorf("parsing \"%s\": invalid syntax", str)
+}
+
+// Bool returns bool type value.
+func (k *Key) Bool() (bool, error) {
+	return parseBool(k.String())
+}
+
+// Float64 returns float64 type value.
+func (k *Key) Float64() (float64, error) {
+	return strconv.ParseFloat(k.String(), 64)
+}
+
+// Int returns int type value.
+func (k *Key) Int() (int, error) {
+	return strconv.Atoi(k.String())
+}
+
+// Int64 returns int64 type value.
+func (k *Key) Int64() (int64, error) {
+	return strconv.ParseInt(k.String(), 10, 64)
+}
+
+// Uint returns uint type valued.
+func (k *Key) Uint() (uint, error) {
+	u, e := strconv.ParseUint(k.String(), 10, 64)
+	return uint(u), e
+}
+
+// Uint64 returns uint64 type value.
+func (k *Key) Uint64() (uint64, error) {
+	return strconv.ParseUint(k.String(), 10, 64)
+}
+
+// Duration returns time.Duration type value.
+func (k *Key) Duration() (time.Duration, error) {
+	return time.ParseDuration(k.String())
+}
+
+// TimeFormat parses with given format and returns time.Time type value.
+func (k *Key) TimeFormat(format string) (time.Time, error) {
+	return time.Parse(format, k.String())
+}
+
+// Time parses with RFC3339 format and returns time.Time type value.
+func (k *Key) Time() (time.Time, error) {
+	return k.TimeFormat(time.RFC3339)
+}
+
+// MustString returns default value if key value is empty.
+func (k *Key) MustString(defaultVal string) string {
+	val := k.String()
+	if len(val) == 0 {
+		k.value = defaultVal
+		return defaultVal
+	}
+	return val
+}
+
+// MustBool always returns value without error,
+// it returns false if error occurs.
+func (k *Key) MustBool(defaultVal ...bool) bool {
+	val, err := k.Bool()
+	if len(defaultVal) > 0 && err != nil {
+		k.value = strconv.FormatBool(defaultVal[0])
+		return defaultVal[0]
+	}
+	return val
+}
+
+// MustFloat64 always returns value without error,
+// it returns 0.0 if error occurs.
+func (k *Key) MustFloat64(defaultVal ...float64) float64 {
+	val, err := k.Float64()
+	if len(defaultVal) > 0 && err != nil {
+		k.value = strconv.FormatFloat(defaultVal[0], 'f', -1, 64)
+		return defaultVal[0]
+	}
+	return val
+}
+
+// MustInt always returns value without error,
+// it returns 0 if error occurs.
+func (k *Key) MustInt(defaultVal ...int) int {
+	val, err := k.Int()
+	if len(defaultVal) > 0 && err != nil {
+		k.value = strconv.FormatInt(int64(defaultVal[0]), 10)
+		return defaultVal[0]
+	}
+	return val
+}
+
+// MustInt64 always returns value without error,
+// it returns 0 if error occurs.
+func (k *Key) MustInt64(defaultVal ...int64) int64 {
+	val, err := k.Int64()
+	if len(defaultVal) > 0 && err != nil {
+		k.value = strconv.FormatInt(defaultVal[0], 10)
+		return defaultVal[0]
+	}
+	return val
+}
+
+// MustUint always returns value without error,
+// it returns 0 if error occurs.
+func (k *Key) MustUint(defaultVal ...uint) uint {
+	val, err := k.Uint()
+	if len(defaultVal) > 0 && err != nil {
+		k.value = strconv.FormatUint(uint64(defaultVal[0]), 10)
+		return defaultVal[0]
+	}
+	return val
+}
+
+// MustUint64 always returns value without error,
+// it returns 0 if error occurs.
+func (k *Key) MustUint64(defaultVal ...uint64) uint64 {
+	val, err := k.Uint64()
+	if len(defaultVal) > 0 && err != nil {
+		k.value = strconv.FormatUint(defaultVal[0], 10)
+		return defaultVal[0]
+	}
+	return val
+}
+
+// MustDuration always returns value without error,
+// it returns zero value if error occurs.
+func (k *Key) MustDuration(defaultVal ...time.Duration) time.Duration {
+	val, err := k.Duration()
+	if len(defaultVal) > 0 && err != nil {
+		k.value = defaultVal[0].String()
+		return defaultVal[0]
+	}
+	return val
+}
+
+// MustTimeFormat always parses with given format and returns value without error,
+// it returns zero value if error occurs.
+func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time {
+	val, err := k.TimeFormat(format)
+	if len(defaultVal) > 0 && err != nil {
+		k.value = defaultVal[0].Format(format)
+		return defaultVal[0]
+	}
+	return val
+}
+
+// MustTime always parses with RFC3339 format and returns value without error,
+// it returns zero value if error occurs.
+func (k *Key) MustTime(defaultVal ...time.Time) time.Time {
+	return k.MustTimeFormat(time.RFC3339, defaultVal...)
+}
+
+// In always returns value without error,
+// it returns default value if error occurs or doesn't fit into candidates.
+func (k *Key) In(defaultVal string, candidates []string) string {
+	val := k.String()
+	for _, cand := range candidates {
+		if val == cand {
+			return val
+		}
+	}
+	return defaultVal
+}
+
+// InFloat64 always returns value without error,
+// it returns default value if error occurs or doesn't fit into candidates.
+func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 {
+	val := k.MustFloat64()
+	for _, cand := range candidates {
+		if val == cand {
+			return val
+		}
+	}
+	return defaultVal
+}
+
+// InInt always returns value without error,
+// it returns default value if error occurs or doesn't fit into candidates.
+func (k *Key) InInt(defaultVal int, candidates []int) int {
+	val := k.MustInt()
+	for _, cand := range candidates {
+		if val == cand {
+			return val
+		}
+	}
+	return defaultVal
+}
+
+// InInt64 always returns value without error,
+// it returns default value if error occurs or doesn't fit into candidates.
+func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 {
+	val := k.MustInt64()
+	for _, cand := range candidates {
+		if val == cand {
+			return val
+		}
+	}
+	return defaultVal
+}
+
+// InUint always returns value without error,
+// it returns default value if error occurs or doesn't fit into candidates.
+func (k *Key) InUint(defaultVal uint, candidates []uint) uint {
+	val := k.MustUint()
+	for _, cand := range candidates {
+		if val == cand {
+			return val
+		}
+	}
+	return defaultVal
+}
+
+// InUint64 always returns value without error,
+// it returns default value if error occurs or doesn't fit into candidates.
+func (k *Key) InUint64(defaultVal uint64, candidates []uint64) uint64 {
+	val := k.MustUint64()
+	for _, cand := range candidates {
+		if val == cand {
+			return val
+		}
+	}
+	return defaultVal
+}
+
+// InTimeFormat always parses with given format and returns value without error,
+// it returns default value if error occurs or doesn't fit into candidates.
+func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time {
+	val := k.MustTimeFormat(format)
+	for _, cand := range candidates {
+		if val == cand {
+			return val
+		}
+	}
+	return defaultVal
+}
+
+// InTime always parses with RFC3339 format and returns value without error,
+// it returns default value if error occurs or doesn't fit into candidates.
+func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time {
+	return k.InTimeFormat(time.RFC3339, defaultVal, candidates)
+}
+
+// RangeFloat64 checks if value is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 {
+	val := k.MustFloat64()
+	if val < min || val > max {
+		return defaultVal
+	}
+	return val
+}
+
+// RangeInt checks if value is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeInt(defaultVal, min, max int) int {
+	val := k.MustInt()
+	if val < min || val > max {
+		return defaultVal
+	}
+	return val
+}
+
+// RangeInt64 checks if value is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeInt64(defaultVal, min, max int64) int64 {
+	val := k.MustInt64()
+	if val < min || val > max {
+		return defaultVal
+	}
+	return val
+}
+
+// RangeTimeFormat checks if value with given format is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time {
+	val := k.MustTimeFormat(format)
+	if val.Unix() < min.Unix() || val.Unix() > max.Unix() {
+		return defaultVal
+	}
+	return val
+}
+
+// RangeTime checks if value with RFC3339 format is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time {
+	return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max)
+}
+
+// Strings returns list of string divided by given delimiter.
+func (k *Key) Strings(delim string) []string {
+	str := k.String()
+	if len(str) == 0 {
+		return []string{}
+	}
+
+	vals := strings.Split(str, delim)
+	for i := range vals {
+		vals[i] = strings.TrimSpace(vals[i])
+	}
+	return vals
+}
+
+// Float64s returns list of float64 divided by given delimiter. Any invalid input will be treated as zero value.
+func (k *Key) Float64s(delim string) []float64 {
+	vals, _ := k.getFloat64s(delim, true, false)
+	return vals
+}
+
+// Ints returns list of int divided by given delimiter. Any invalid input will be treated as zero value.
+func (k *Key) Ints(delim string) []int {
+	vals, _ := k.getInts(delim, true, false)
+	return vals
+}
+
+// Int64s returns list of int64 divided by given delimiter. Any invalid input will be treated as zero value.
+func (k *Key) Int64s(delim string) []int64 {
+	vals, _ := k.getInt64s(delim, true, false)
+	return vals
+}
+
+// Uints returns list of uint divided by given delimiter. Any invalid input will be treated as zero value.
+func (k *Key) Uints(delim string) []uint {
+	vals, _ := k.getUints(delim, true, false)
+	return vals
+}
+
+// Uint64s returns list of uint64 divided by given delimiter. Any invalid input will be treated as zero value.
+func (k *Key) Uint64s(delim string) []uint64 {
+	vals, _ := k.getUint64s(delim, true, false)
+	return vals
+}
+
+// TimesFormat parses with given format and returns list of time.Time divided by given delimiter.
+// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC).
+func (k *Key) TimesFormat(format, delim string) []time.Time {
+	vals, _ := k.getTimesFormat(format, delim, true, false)
+	return vals
+}
+
+// Times parses with RFC3339 format and returns list of time.Time divided by given delimiter.
+// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC).
+func (k *Key) Times(delim string) []time.Time {
+	return k.TimesFormat(time.RFC3339, delim)
+}
+
+// ValidFloat64s returns list of float64 divided by given delimiter. If some value is not float, then
+// it will not be included to result list.
+func (k *Key) ValidFloat64s(delim string) []float64 {
+	vals, _ := k.getFloat64s(delim, false, false)
+	return vals
+}
+
+// ValidInts returns list of int divided by given delimiter. If some value is not integer, then it will
+// not be included to result list.
+func (k *Key) ValidInts(delim string) []int {
+	vals, _ := k.getInts(delim, false, false)
+	return vals
+}
+
+// ValidInt64s returns list of int64 divided by given delimiter. If some value is not 64-bit integer,
+// then it will not be included to result list.
+func (k *Key) ValidInt64s(delim string) []int64 {
+	vals, _ := k.getInt64s(delim, false, false)
+	return vals
+}
+
+// ValidUints returns list of uint divided by given delimiter. If some value is not unsigned integer,
+// then it will not be included to result list.
+func (k *Key) ValidUints(delim string) []uint {
+	vals, _ := k.getUints(delim, false, false)
+	return vals
+}
+
+// ValidUint64s returns list of uint64 divided by given delimiter. If some value is not 64-bit unsigned
+// integer, then it will not be included to result list.
+func (k *Key) ValidUint64s(delim string) []uint64 {
+	vals, _ := k.getUint64s(delim, false, false)
+	return vals
+}
+
+// ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter.
+func (k *Key) ValidTimesFormat(format, delim string) []time.Time {
+	vals, _ := k.getTimesFormat(format, delim, false, false)
+	return vals
+}
+
+// ValidTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter.
+func (k *Key) ValidTimes(delim string) []time.Time {
+	return k.ValidTimesFormat(time.RFC3339, delim)
+}
+
+// StrictFloat64s returns list of float64 divided by given delimiter or error on first invalid input.
+func (k *Key) StrictFloat64s(delim string) ([]float64, error) {
+	return k.getFloat64s(delim, false, true)
+}
+
+// StrictInts returns list of int divided by given delimiter or error on first invalid input.
+func (k *Key) StrictInts(delim string) ([]int, error) {
+	return k.getInts(delim, false, true)
+}
+
+// StrictInt64s returns list of int64 divided by given delimiter or error on first invalid input.
+func (k *Key) StrictInt64s(delim string) ([]int64, error) {
+	return k.getInt64s(delim, false, true)
+}
+
+// StrictUints returns list of uint divided by given delimiter or error on first invalid input.
+func (k *Key) StrictUints(delim string) ([]uint, error) {
+	return k.getUints(delim, false, true)
+}
+
+// StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input.
+func (k *Key) StrictUint64s(delim string) ([]uint64, error) {
+	return k.getUint64s(delim, false, true)
+}
+
+// StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter
+// or error on first invalid input.
+func (k *Key) StrictTimesFormat(format, delim string) ([]time.Time, error) {
+	return k.getTimesFormat(format, delim, false, true)
+}
+
+// StrictTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter
+// or error on first invalid input.
+func (k *Key) StrictTimes(delim string) ([]time.Time, error) {
+	return k.StrictTimesFormat(time.RFC3339, delim)
+}
+
+// getFloat64s returns list of float64 divided by given delimiter.
+func (k *Key) getFloat64s(delim string, addInvalid, returnOnInvalid bool) ([]float64, error) {
+	strs := k.Strings(delim)
+	vals := make([]float64, 0, len(strs))
+	for _, str := range strs {
+		val, err := strconv.ParseFloat(str, 64)
+		if err != nil && returnOnInvalid {
+			return nil, err
+		}
+		if err == nil || addInvalid {
+			vals = append(vals, val)
+		}
+	}
+	return vals, nil
+}
+
+// getInts returns list of int divided by given delimiter.
+func (k *Key) getInts(delim string, addInvalid, returnOnInvalid bool) ([]int, error) {
+	strs := k.Strings(delim)
+	vals := make([]int, 0, len(strs))
+	for _, str := range strs {
+		val, err := strconv.Atoi(str)
+		if err != nil && returnOnInvalid {
+			return nil, err
+		}
+		if err == nil || addInvalid {
+			vals = append(vals, val)
+		}
+	}
+	return vals, nil
+}
+
+// getInt64s returns list of int64 divided by given delimiter.
+func (k *Key) getInt64s(delim string, addInvalid, returnOnInvalid bool) ([]int64, error) {
+	strs := k.Strings(delim)
+	vals := make([]int64, 0, len(strs))
+	for _, str := range strs {
+		val, err := strconv.ParseInt(str, 10, 64)
+		if err != nil && returnOnInvalid {
+			return nil, err
+		}
+		if err == nil || addInvalid {
+			vals = append(vals, val)
+		}
+	}
+	return vals, nil
+}
+
+// getUints returns list of uint divided by given delimiter.
+func (k *Key) getUints(delim string, addInvalid, returnOnInvalid bool) ([]uint, error) {
+	strs := k.Strings(delim)
+	vals := make([]uint, 0, len(strs))
+	for _, str := range strs {
+		val, err := strconv.ParseUint(str, 10, 0)
+		if err != nil && returnOnInvalid {
+			return nil, err
+		}
+		if err == nil || addInvalid {
+			vals = append(vals, uint(val))
+		}
+	}
+	return vals, nil
+}
+
+// getUint64s returns list of uint64 divided by given delimiter.
+func (k *Key) getUint64s(delim string, addInvalid, returnOnInvalid bool) ([]uint64, error) {
+	strs := k.Strings(delim)
+	vals := make([]uint64, 0, len(strs))
+	for _, str := range strs {
+		val, err := strconv.ParseUint(str, 10, 64)
+		if err != nil && returnOnInvalid {
+			return nil, err
+		}
+		if err == nil || addInvalid {
+			vals = append(vals, val)
+		}
+	}
+	return vals, nil
+}
+
+// getTimesFormat parses with given format and returns list of time.Time divided by given delimiter.
+func (k *Key) getTimesFormat(format, delim string, addInvalid, returnOnInvalid bool) ([]time.Time, error) {
+	strs := k.Strings(delim)
+	vals := make([]time.Time, 0, len(strs))
+	for _, str := range strs {
+		val, err := time.Parse(format, str)
+		if err != nil && returnOnInvalid {
+			return nil, err
+		}
+		if err == nil || addInvalid {
+			vals = append(vals, val)
+		}
+	}
+	return vals, nil
+}
+
+// SetValue changes key value.
+func (k *Key) SetValue(v string) {
+	if k.s.f.BlockMode {
+		k.s.f.lock.Lock()
+		defer k.s.f.lock.Unlock()
+	}
+
+	k.value = v
+	k.s.keysHash[k.name] = v
+}

+ 325 - 0
Godeps/_workspace/src/gopkg.in/ini.v1/parser.go

@@ -0,0 +1,325 @@
+// Copyright 2015 Unknwon
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package ini
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+	"unicode"
+)
+
+type tokenType int
+
+const (
+	_TOKEN_INVALID tokenType = iota
+	_TOKEN_COMMENT
+	_TOKEN_SECTION
+	_TOKEN_KEY
+)
+
+type parser struct {
+	buf     *bufio.Reader
+	isEOF   bool
+	count   int
+	comment *bytes.Buffer
+}
+
+func newParser(r io.Reader) *parser {
+	return &parser{
+		buf:     bufio.NewReader(r),
+		count:   1,
+		comment: &bytes.Buffer{},
+	}
+}
+
+// BOM handles header of BOM-UTF8 format.
+// http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding
+func (p *parser) BOM() error {
+	mask, err := p.buf.Peek(3)
+	if err != nil && err != io.EOF {
+		return err
+	} else if len(mask) < 3 {
+		return nil
+	} else if mask[0] == 239 && mask[1] == 187 && mask[2] == 191 {
+		p.buf.Read(mask)
+	}
+	return nil
+}
+
+func (p *parser) readUntil(delim byte) ([]byte, error) {
+	data, err := p.buf.ReadBytes(delim)
+	if err != nil {
+		if err == io.EOF {
+			p.isEOF = true
+		} else {
+			return nil, err
+		}
+	}
+	return data, nil
+}
+
+func cleanComment(in []byte) ([]byte, bool) {
+	i := bytes.IndexAny(in, "#;")
+	if i == -1 {
+		return nil, false
+	}
+	return in[i:], true
+}
+
+func readKeyName(in []byte) (string, int, error) {
+	line := string(in)
+
+	// Check if key name surrounded by quotes.
+	var keyQuote string
+	if line[0] == '"' {
+		if len(line) > 6 && string(line[0:3]) == `"""` {
+			keyQuote = `"""`
+		} else {
+			keyQuote = `"`
+		}
+	} else if line[0] == '`' {
+		keyQuote = "`"
+	}
+
+	// Get out key name
+	endIdx := -1
+	if len(keyQuote) > 0 {
+		startIdx := len(keyQuote)
+		// FIXME: fail case -> """"""name"""=value
+		pos := strings.Index(line[startIdx:], keyQuote)
+		if pos == -1 {
+			return "", -1, fmt.Errorf("missing closing key quote: %s", line)
+		}
+		pos += startIdx
+
+		// Find key-value delimiter
+		i := strings.IndexAny(line[pos+startIdx:], "=:")
+		if i < 0 {
+			return "", -1, ErrDelimiterNotFound{line}
+		}
+		endIdx = pos + i
+		return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil
+	}
+
+	endIdx = strings.IndexAny(line, "=:")
+	if endIdx < 0 {
+		return "", -1, ErrDelimiterNotFound{line}
+	}
+	return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil
+}
+
+func (p *parser) readMultilines(line, val, valQuote string) (string, error) {
+	for {
+		data, err := p.readUntil('\n')
+		if err != nil {
+			return "", err
+		}
+		next := string(data)
+
+		pos := strings.LastIndex(next, valQuote)
+		if pos > -1 {
+			val += next[:pos]
+
+			comment, has := cleanComment([]byte(next[pos:]))
+			if has {
+				p.comment.Write(bytes.TrimSpace(comment))
+			}
+			break
+		}
+		val += next
+		if p.isEOF {
+			return "", fmt.Errorf("missing closing key quote from '%s' to '%s'", line, next)
+		}
+	}
+	return val, nil
+}
+
+func (p *parser) readContinuationLines(val string) (string, error) {
+	for {
+		data, err := p.readUntil('\n')
+		if err != nil {
+			return "", err
+		}
+		next := strings.TrimSpace(string(data))
+
+		if len(next) == 0 {
+			break
+		}
+		val += next
+		if val[len(val)-1] != '\\' {
+			break
+		}
+		val = val[:len(val)-1]
+	}
+	return val, nil
+}
+
+// hasSurroundedQuote check if and only if the first and last characters
+// are quotes \" or \'.
+// It returns false if any other parts also contain same kind of quotes.
+func hasSurroundedQuote(in string, quote byte) bool {
+	return len(in) > 2 && in[0] == quote && in[len(in)-1] == quote &&
+		strings.IndexByte(in[1:], quote) == len(in)-2
+}
+
+func (p *parser) readValue(in []byte, ignoreContinuation bool) (string, error) {
+	line := strings.TrimLeftFunc(string(in), unicode.IsSpace)
+	if len(line) == 0 {
+		return "", nil
+	}
+
+	var valQuote string
+	if len(line) > 3 && string(line[0:3]) == `"""` {
+		valQuote = `"""`
+	} else if line[0] == '`' {
+		valQuote = "`"
+	}
+
+	if len(valQuote) > 0 {
+		startIdx := len(valQuote)
+		pos := strings.LastIndex(line[startIdx:], valQuote)
+		// Check for multi-line value
+		if pos == -1 {
+			return p.readMultilines(line, line[startIdx:], valQuote)
+		}
+
+		return line[startIdx : pos+startIdx], nil
+	}
+
+	// Won't be able to reach here if value only contains whitespace.
+	line = strings.TrimSpace(line)
+
+	// Check continuation lines when desired.
+	if !ignoreContinuation && line[len(line)-1] == '\\' {
+		return p.readContinuationLines(line[:len(line)-1])
+	}
+
+	i := strings.IndexAny(line, "#;")
+	if i > -1 {
+		p.comment.WriteString(line[i:])
+		line = strings.TrimSpace(line[:i])
+	}
+
+	// Trim single quotes
+	if hasSurroundedQuote(line, '\'') ||
+		hasSurroundedQuote(line, '"') {
+		line = line[1 : len(line)-1]
+	}
+	return line, nil
+}
+
+// parse parses data through an io.Reader.
+func (f *File) parse(reader io.Reader) (err error) {
+	p := newParser(reader)
+	if err = p.BOM(); err != nil {
+		return fmt.Errorf("BOM: %v", err)
+	}
+
+	// Ignore error because default section name is never empty string.
+	section, _ := f.NewSection(DEFAULT_SECTION)
+
+	var line []byte
+	for !p.isEOF {
+		line, err = p.readUntil('\n')
+		if err != nil {
+			return err
+		}
+
+		line = bytes.TrimLeftFunc(line, unicode.IsSpace)
+		if len(line) == 0 {
+			continue
+		}
+
+		// Comments
+		if line[0] == '#' || line[0] == ';' {
+			// Note: we do not care ending line break,
+			// it is needed for adding second line,
+			// so just clean it once at the end when set to value.
+			p.comment.Write(line)
+			continue
+		}
+
+		// Section
+		if line[0] == '[' {
+			// Read to the next ']' (TODO: support quoted strings)
+			// TODO(unknwon): use LastIndexByte when stop supporting Go1.4
+			closeIdx := bytes.LastIndex(line, []byte("]"))
+			if closeIdx == -1 {
+				return fmt.Errorf("unclosed section: %s", line)
+			}
+
+			name := string(line[1:closeIdx])
+			section, err = f.NewSection(name)
+			if err != nil {
+				return err
+			}
+
+			comment, has := cleanComment(line[closeIdx+1:])
+			if has {
+				p.comment.Write(comment)
+			}
+
+			section.Comment = strings.TrimSpace(p.comment.String())
+
+			// Reset aotu-counter and comments
+			p.comment.Reset()
+			p.count = 1
+			continue
+		}
+
+		kname, offset, err := readKeyName(line)
+		if err != nil {
+			// Treat as boolean key when desired, and whole line is key name.
+			if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys {
+				key, err := section.NewKey(string(line), "true")
+				if err != nil {
+					return err
+				}
+				key.isBooleanType = true
+				key.Comment = strings.TrimSpace(p.comment.String())
+				p.comment.Reset()
+				continue
+			}
+			return err
+		}
+
+		// Auto increment.
+		isAutoIncr := false
+		if kname == "-" {
+			isAutoIncr = true
+			kname = "#" + strconv.Itoa(p.count)
+			p.count++
+		}
+
+		key, err := section.NewKey(kname, "")
+		if err != nil {
+			return err
+		}
+		key.isAutoIncrement = isAutoIncr
+
+		value, err := p.readValue(line[offset:], f.options.IgnoreContinuation)
+		if err != nil {
+			return err
+		}
+		key.SetValue(value)
+		key.Comment = strings.TrimSpace(p.comment.String())
+		p.comment.Reset()
+	}
+	return nil
+}

+ 206 - 0
Godeps/_workspace/src/gopkg.in/ini.v1/section.go

@@ -0,0 +1,206 @@
+// Copyright 2014 Unknwon
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package ini
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+)
+
+// Section represents a config section.
+type Section struct {
+	f        *File
+	Comment  string
+	name     string
+	keys     map[string]*Key
+	keyList  []string
+	keysHash map[string]string
+}
+
+func newSection(f *File, name string) *Section {
+	return &Section{f, "", name, make(map[string]*Key), make([]string, 0, 10), make(map[string]string)}
+}
+
+// Name returns name of Section.
+func (s *Section) Name() string {
+	return s.name
+}
+
+// NewKey creates a new key to given section.
+func (s *Section) NewKey(name, val string) (*Key, error) {
+	if len(name) == 0 {
+		return nil, errors.New("error creating new key: empty key name")
+	} else if s.f.options.Insensitive {
+		name = strings.ToLower(name)
+	}
+
+	if s.f.BlockMode {
+		s.f.lock.Lock()
+		defer s.f.lock.Unlock()
+	}
+
+	if inSlice(name, s.keyList) {
+		s.keys[name].value = val
+		return s.keys[name], nil
+	}
+
+	s.keyList = append(s.keyList, name)
+	s.keys[name] = &Key{
+		s:     s,
+		name:  name,
+		value: val,
+	}
+	s.keysHash[name] = val
+	return s.keys[name], nil
+}
+
+// GetKey returns key in section by given name.
+func (s *Section) GetKey(name string) (*Key, error) {
+	// FIXME: change to section level lock?
+	if s.f.BlockMode {
+		s.f.lock.RLock()
+	}
+	if s.f.options.Insensitive {
+		name = strings.ToLower(name)
+	}
+	key := s.keys[name]
+	if s.f.BlockMode {
+		s.f.lock.RUnlock()
+	}
+
+	if key == nil {
+		// Check if it is a child-section.
+		sname := s.name
+		for {
+			if i := strings.LastIndex(sname, "."); i > -1 {
+				sname = sname[:i]
+				sec, err := s.f.GetSection(sname)
+				if err != nil {
+					continue
+				}
+				return sec.GetKey(name)
+			} else {
+				break
+			}
+		}
+		return nil, fmt.Errorf("error when getting key of section '%s': key '%s' not exists", s.name, name)
+	}
+	return key, nil
+}
+
+// HasKey returns true if section contains a key with given name.
+func (s *Section) HasKey(name string) bool {
+	key, _ := s.GetKey(name)
+	return key != nil
+}
+
+// Haskey is a backwards-compatible name for HasKey.
+func (s *Section) Haskey(name string) bool {
+	return s.HasKey(name)
+}
+
+// HasValue returns true if section contains given raw value.
+func (s *Section) HasValue(value string) bool {
+	if s.f.BlockMode {
+		s.f.lock.RLock()
+		defer s.f.lock.RUnlock()
+	}
+
+	for _, k := range s.keys {
+		if value == k.value {
+			return true
+		}
+	}
+	return false
+}
+
+// Key assumes named Key exists in section and returns a zero-value when not.
+func (s *Section) Key(name string) *Key {
+	key, err := s.GetKey(name)
+	if err != nil {
+		// It's OK here because the only possible error is empty key name,
+		// but if it's empty, this piece of code won't be executed.
+		key, _ = s.NewKey(name, "")
+		return key
+	}
+	return key
+}
+
+// Keys returns list of keys of section.
+func (s *Section) Keys() []*Key {
+	keys := make([]*Key, len(s.keyList))
+	for i := range s.keyList {
+		keys[i] = s.Key(s.keyList[i])
+	}
+	return keys
+}
+
+// ParentKeys returns list of keys of parent section.
+func (s *Section) ParentKeys() []*Key {
+	var parentKeys []*Key
+	sname := s.name
+	for {
+		if i := strings.LastIndex(sname, "."); i > -1 {
+			sname = sname[:i]
+			sec, err := s.f.GetSection(sname)
+			if err != nil {
+				continue
+			}
+			parentKeys = append(parentKeys, sec.Keys()...)
+		} else {
+			break
+		}
+
+	}
+	return parentKeys
+}
+
+// KeyStrings returns list of key names of section.
+func (s *Section) KeyStrings() []string {
+	list := make([]string, len(s.keyList))
+	copy(list, s.keyList)
+	return list
+}
+
+// KeysHash returns keys hash consisting of names and values.
+func (s *Section) KeysHash() map[string]string {
+	if s.f.BlockMode {
+		s.f.lock.RLock()
+		defer s.f.lock.RUnlock()
+	}
+
+	hash := map[string]string{}
+	for key, value := range s.keysHash {
+		hash[key] = value
+	}
+	return hash
+}
+
+// DeleteKey deletes a key from section.
+func (s *Section) DeleteKey(name string) {
+	if s.f.BlockMode {
+		s.f.lock.Lock()
+		defer s.f.lock.Unlock()
+	}
+
+	for i, k := range s.keyList {
+		if k == name {
+			s.keyList = append(s.keyList[:i], s.keyList[i+1:]...)
+			delete(s.keys, name)
+			return
+		}
+	}
+}

+ 248 - 37
Godeps/_workspace/src/gopkg.in/ini.v1/struct.go

@@ -15,9 +15,11 @@
 package ini
 package ini
 
 
 import (
 import (
+	"bytes"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"reflect"
 	"reflect"
+	"strings"
 	"time"
 	"time"
 	"unicode"
 	"unicode"
 )
 )
@@ -75,11 +77,64 @@ func parseDelim(actual string) string {
 
 
 var reflectTime = reflect.TypeOf(time.Now()).Kind()
 var reflectTime = reflect.TypeOf(time.Now()).Kind()
 
 
+// setSliceWithProperType sets proper values to slice based on its type.
+func setSliceWithProperType(key *Key, field reflect.Value, delim string) error {
+	strs := key.Strings(delim)
+	numVals := len(strs)
+	if numVals == 0 {
+		return nil
+	}
+
+	var vals interface{}
+
+	sliceOf := field.Type().Elem().Kind()
+	switch sliceOf {
+	case reflect.String:
+		vals = strs
+	case reflect.Int:
+		vals = key.Ints(delim)
+	case reflect.Int64:
+		vals = key.Int64s(delim)
+	case reflect.Uint:
+		vals = key.Uints(delim)
+	case reflect.Uint64:
+		vals = key.Uint64s(delim)
+	case reflect.Float64:
+		vals = key.Float64s(delim)
+	case reflectTime:
+		vals = key.Times(delim)
+	default:
+		return fmt.Errorf("unsupported type '[]%s'", sliceOf)
+	}
+
+	slice := reflect.MakeSlice(field.Type(), numVals, numVals)
+	for i := 0; i < numVals; i++ {
+		switch sliceOf {
+		case reflect.String:
+			slice.Index(i).Set(reflect.ValueOf(vals.([]string)[i]))
+		case reflect.Int:
+			slice.Index(i).Set(reflect.ValueOf(vals.([]int)[i]))
+		case reflect.Int64:
+			slice.Index(i).Set(reflect.ValueOf(vals.([]int64)[i]))
+		case reflect.Uint:
+			slice.Index(i).Set(reflect.ValueOf(vals.([]uint)[i]))
+		case reflect.Uint64:
+			slice.Index(i).Set(reflect.ValueOf(vals.([]uint64)[i]))
+		case reflect.Float64:
+			slice.Index(i).Set(reflect.ValueOf(vals.([]float64)[i]))
+		case reflectTime:
+			slice.Index(i).Set(reflect.ValueOf(vals.([]time.Time)[i]))
+		}
+	}
+	field.Set(slice)
+	return nil
+}
+
 // setWithProperType sets proper value to field based on its type,
 // setWithProperType sets proper value to field based on its type,
 // but it does not return error for failing parsing,
 // but it does not return error for failing parsing,
 // because we want to use default value that is already assigned to strcut.
 // because we want to use default value that is already assigned to strcut.
-func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim string) error {
-	switch kind {
+func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string) error {
+	switch t.Kind() {
 	case reflect.String:
 	case reflect.String:
 		if len(key.String()) == 0 {
 		if len(key.String()) == 0 {
 			return nil
 			return nil
@@ -92,11 +147,33 @@ func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim s
 		}
 		}
 		field.SetBool(boolVal)
 		field.SetBool(boolVal)
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		durationVal, err := key.Duration()
+		// Skip zero value
+		if err == nil && int(durationVal) > 0 {
+			field.Set(reflect.ValueOf(durationVal))
+			return nil
+		}
+
 		intVal, err := key.Int64()
 		intVal, err := key.Int64()
-		if err != nil {
+		if err != nil || intVal == 0 {
 			return nil
 			return nil
 		}
 		}
 		field.SetInt(intVal)
 		field.SetInt(intVal)
+	//	byte is an alias for uint8, so supporting uint8 breaks support for byte
+	case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		durationVal, err := key.Duration()
+		// Skip zero value
+		if err == nil && int(durationVal) > 0 {
+			field.Set(reflect.ValueOf(durationVal))
+			return nil
+		}
+
+		uintVal, err := key.Uint64()
+		if err != nil {
+			return nil
+		}
+		field.SetUint(uintVal)
+
 	case reflect.Float64:
 	case reflect.Float64:
 		floatVal, err := key.Float64()
 		floatVal, err := key.Float64()
 		if err != nil {
 		if err != nil {
@@ -110,31 +187,9 @@ func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim s
 		}
 		}
 		field.Set(reflect.ValueOf(timeVal))
 		field.Set(reflect.ValueOf(timeVal))
 	case reflect.Slice:
 	case reflect.Slice:
-		vals := key.Strings(delim)
-		numVals := len(vals)
-		if numVals == 0 {
-			return nil
-		}
-
-		sliceOf := field.Type().Elem().Kind()
-
-		var times []time.Time
-		if sliceOf == reflectTime {
-			times = key.Times(delim)
-		}
-
-		slice := reflect.MakeSlice(field.Type(), numVals, numVals)
-		for i := 0; i < numVals; i++ {
-			switch sliceOf {
-			case reflectTime:
-				slice.Index(i).Set(reflect.ValueOf(times[i]))
-			default:
-				slice.Index(i).Set(reflect.ValueOf(vals[i]))
-			}
-		}
-		field.Set(slice)
+		return setSliceWithProperType(key, field, delim)
 	default:
 	default:
-		return fmt.Errorf("unsupported type '%s'", kind)
+		return fmt.Errorf("unsupported type '%s'", t)
 	}
 	}
 	return nil
 	return nil
 }
 }
@@ -154,20 +209,19 @@ func (s *Section) mapTo(val reflect.Value) error {
 			continue
 			continue
 		}
 		}
 
 
-		fieldName := s.parseFieldName(tpField.Name, tag)
+		opts := strings.SplitN(tag, ",", 2) // strip off possible omitempty
+		fieldName := s.parseFieldName(tpField.Name, opts[0])
 		if len(fieldName) == 0 || !field.CanSet() {
 		if len(fieldName) == 0 || !field.CanSet() {
 			continue
 			continue
 		}
 		}
 
 
-		if tpField.Type.Kind() == reflect.Struct {
-			if sec, err := s.f.GetSection(fieldName); err == nil {
-				if err = sec.mapTo(field); err != nil {
-					return fmt.Errorf("error mapping field(%s): %v", fieldName, err)
-				}
-				continue
-			}
-		} else if tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous {
+		isAnonymous := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous
+		isStruct := tpField.Type.Kind() == reflect.Struct
+		if isAnonymous {
 			field.Set(reflect.New(tpField.Type.Elem()))
 			field.Set(reflect.New(tpField.Type.Elem()))
+		}
+
+		if isAnonymous || isStruct {
 			if sec, err := s.f.GetSection(fieldName); err == nil {
 			if sec, err := s.f.GetSection(fieldName); err == nil {
 				if err = sec.mapTo(field); err != nil {
 				if err = sec.mapTo(field); err != nil {
 					return fmt.Errorf("error mapping field(%s): %v", fieldName, err)
 					return fmt.Errorf("error mapping field(%s): %v", fieldName, err)
@@ -177,7 +231,7 @@ func (s *Section) mapTo(val reflect.Value) error {
 		}
 		}
 
 
 		if key, err := s.GetKey(fieldName); err == nil {
 		if key, err := s.GetKey(fieldName); err == nil {
-			if err = setWithProperType(tpField.Type.Kind(), key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil {
+			if err = setWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil {
 				return fmt.Errorf("error mapping field(%s): %v", fieldName, err)
 				return fmt.Errorf("error mapping field(%s): %v", fieldName, err)
 			}
 			}
 		}
 		}
@@ -218,3 +272,160 @@ func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, other
 func MapTo(v, source interface{}, others ...interface{}) error {
 func MapTo(v, source interface{}, others ...interface{}) error {
 	return MapToWithMapper(v, nil, source, others...)
 	return MapToWithMapper(v, nil, source, others...)
 }
 }
+
+// reflectSliceWithProperType does the opposite thing as setSliceWithProperType.
+func reflectSliceWithProperType(key *Key, field reflect.Value, delim string) error {
+	slice := field.Slice(0, field.Len())
+	if field.Len() == 0 {
+		return nil
+	}
+
+	var buf bytes.Buffer
+	sliceOf := field.Type().Elem().Kind()
+	for i := 0; i < field.Len(); i++ {
+		switch sliceOf {
+		case reflect.String:
+			buf.WriteString(slice.Index(i).String())
+		case reflect.Int, reflect.Int64:
+			buf.WriteString(fmt.Sprint(slice.Index(i).Int()))
+		case reflect.Uint, reflect.Uint64:
+			buf.WriteString(fmt.Sprint(slice.Index(i).Uint()))
+		case reflect.Float64:
+			buf.WriteString(fmt.Sprint(slice.Index(i).Float()))
+		case reflectTime:
+			buf.WriteString(slice.Index(i).Interface().(time.Time).Format(time.RFC3339))
+		default:
+			return fmt.Errorf("unsupported type '[]%s'", sliceOf)
+		}
+		buf.WriteString(delim)
+	}
+	key.SetValue(buf.String()[:buf.Len()-1])
+	return nil
+}
+
+// reflectWithProperType does the opposite thing as setWithProperType.
+func reflectWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string) error {
+	switch t.Kind() {
+	case reflect.String:
+		key.SetValue(field.String())
+	case reflect.Bool:
+		key.SetValue(fmt.Sprint(field.Bool()))
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		key.SetValue(fmt.Sprint(field.Int()))
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		key.SetValue(fmt.Sprint(field.Uint()))
+	case reflect.Float32, reflect.Float64:
+		key.SetValue(fmt.Sprint(field.Float()))
+	case reflectTime:
+		key.SetValue(fmt.Sprint(field.Interface().(time.Time).Format(time.RFC3339)))
+	case reflect.Slice:
+		return reflectSliceWithProperType(key, field, delim)
+	default:
+		return fmt.Errorf("unsupported type '%s'", t)
+	}
+	return nil
+}
+
+// CR: copied from encoding/json/encode.go with modifications of time.Time support.
+// TODO: add more test coverage.
+func isEmptyValue(v reflect.Value) bool {
+	switch v.Kind() {
+	case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
+		return v.Len() == 0
+	case reflect.Bool:
+		return !v.Bool()
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return v.Int() == 0
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return v.Uint() == 0
+	case reflect.Float32, reflect.Float64:
+		return v.Float() == 0
+	case reflectTime:
+		return v.Interface().(time.Time).IsZero()
+	case reflect.Interface, reflect.Ptr:
+		return v.IsNil()
+	}
+	return false
+}
+
+func (s *Section) reflectFrom(val reflect.Value) error {
+	if val.Kind() == reflect.Ptr {
+		val = val.Elem()
+	}
+	typ := val.Type()
+
+	for i := 0; i < typ.NumField(); i++ {
+		field := val.Field(i)
+		tpField := typ.Field(i)
+
+		tag := tpField.Tag.Get("ini")
+		if tag == "-" {
+			continue
+		}
+
+		opts := strings.SplitN(tag, ",", 2)
+		if len(opts) == 2 && opts[1] == "omitempty" && isEmptyValue(field) {
+			continue
+		}
+
+		fieldName := s.parseFieldName(tpField.Name, opts[0])
+		if len(fieldName) == 0 || !field.CanSet() {
+			continue
+		}
+
+		if (tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous) ||
+			(tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") {
+			// Note: The only error here is section doesn't exist.
+			sec, err := s.f.GetSection(fieldName)
+			if err != nil {
+				// Note: fieldName can never be empty here, ignore error.
+				sec, _ = s.f.NewSection(fieldName)
+			}
+			if err = sec.reflectFrom(field); err != nil {
+				return fmt.Errorf("error reflecting field (%s): %v", fieldName, err)
+			}
+			continue
+		}
+
+		// Note: Same reason as secion.
+		key, err := s.GetKey(fieldName)
+		if err != nil {
+			key, _ = s.NewKey(fieldName, "")
+		}
+		if err = reflectWithProperType(tpField.Type, key, field, parseDelim(tpField.Tag.Get("delim"))); err != nil {
+			return fmt.Errorf("error reflecting field (%s): %v", fieldName, err)
+		}
+
+	}
+	return nil
+}
+
+// ReflectFrom reflects secion from given struct.
+func (s *Section) ReflectFrom(v interface{}) error {
+	typ := reflect.TypeOf(v)
+	val := reflect.ValueOf(v)
+	if typ.Kind() == reflect.Ptr {
+		typ = typ.Elem()
+		val = val.Elem()
+	} else {
+		return errors.New("cannot reflect from non-pointer struct")
+	}
+
+	return s.reflectFrom(val)
+}
+
+// ReflectFrom reflects file from given struct.
+func (f *File) ReflectFrom(v interface{}) error {
+	return f.Section("").ReflectFrom(v)
+}
+
+// ReflectFrom reflects data sources from given struct with name mapper.
+func ReflectFromWithMapper(cfg *File, v interface{}, mapper NameMapper) error {
+	cfg.NameMapper = mapper
+	return cfg.ReflectFrom(v)
+}
+
+// ReflectFrom reflects data sources from given struct.
+func ReflectFrom(cfg *File, v interface{}) error {
+	return ReflectFromWithMapper(cfg, v, nil)
+}

+ 0 - 181
Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go

@@ -1,181 +0,0 @@
-// Copyright 2014 Unknwon
-//
-// Licensed under the Apache License, Version 2.0 (the "License"): you may
-// not use this file except in compliance with the License. You may obtain
-// a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations
-// under the License.
-
-package ini
-
-import (
-	"strings"
-	"testing"
-	"time"
-
-	. "github.com/smartystreets/goconvey/convey"
-)
-
-type testNested struct {
-	Cities []string `delim:"|"`
-	Visits []time.Time
-	Note   string
-	Unused int `ini:"-"`
-}
-
-type testEmbeded struct {
-	GPA float64
-}
-
-type testStruct struct {
-	Name         string `ini:"NAME"`
-	Age          int
-	Male         bool
-	Money        float64
-	Born         time.Time
-	Others       testNested
-	*testEmbeded `ini:"grade"`
-	Unused       int `ini:"-"`
-}
-
-const _CONF_DATA_STRUCT = `
-NAME = Unknwon
-Age = 21
-Male = true
-Money = 1.25
-Born = 1993-10-07T20:17:05Z
-
-[Others]
-Cities = HangZhou|Boston
-Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z
-Note = Hello world!
-
-[grade]
-GPA = 2.8
-`
-
-type unsupport struct {
-	Byte byte
-}
-
-type unsupport2 struct {
-	Others struct {
-		Cities byte
-	}
-}
-
-type unsupport3 struct {
-	Cities byte
-}
-
-type unsupport4 struct {
-	*unsupport3 `ini:"Others"`
-}
-
-type defaultValue struct {
-	Name   string
-	Age    int
-	Male   bool
-	Money  float64
-	Born   time.Time
-	Cities []string
-}
-
-const _INVALID_DATA_CONF_STRUCT = `
-Name = 
-Age = age
-Male = 123
-Money = money
-Born = nil
-Cities = 
-`
-
-func Test_Struct(t *testing.T) {
-	Convey("Map file to struct", t, func() {
-		ts := new(testStruct)
-		So(MapTo(ts, []byte(_CONF_DATA_STRUCT)), ShouldBeNil)
-
-		So(ts.Name, ShouldEqual, "Unknwon")
-		So(ts.Age, ShouldEqual, 21)
-		So(ts.Male, ShouldBeTrue)
-		So(ts.Money, ShouldEqual, 1.25)
-
-		t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
-		So(err, ShouldBeNil)
-		So(ts.Born.String(), ShouldEqual, t.String())
-
-		So(strings.Join(ts.Others.Cities, ","), ShouldEqual, "HangZhou,Boston")
-		So(ts.Others.Visits[0].String(), ShouldEqual, t.String())
-		So(ts.Others.Note, ShouldEqual, "Hello world!")
-		So(ts.testEmbeded.GPA, ShouldEqual, 2.8)
-	})
-
-	Convey("Map to non-pointer struct", t, func() {
-		cfg, err := Load([]byte(_CONF_DATA_STRUCT))
-		So(err, ShouldBeNil)
-		So(cfg, ShouldNotBeNil)
-
-		So(cfg.MapTo(testStruct{}), ShouldNotBeNil)
-	})
-
-	Convey("Map to unsupported type", t, func() {
-		cfg, err := Load([]byte(_CONF_DATA_STRUCT))
-		So(err, ShouldBeNil)
-		So(cfg, ShouldNotBeNil)
-
-		cfg.NameMapper = func(raw string) string {
-			if raw == "Byte" {
-				return "NAME"
-			}
-			return raw
-		}
-		So(cfg.MapTo(&unsupport{}), ShouldNotBeNil)
-		So(cfg.MapTo(&unsupport2{}), ShouldNotBeNil)
-		So(cfg.MapTo(&unsupport4{}), ShouldNotBeNil)
-	})
-
-	Convey("Map from invalid data source", t, func() {
-		So(MapTo(&testStruct{}, "hi"), ShouldNotBeNil)
-	})
-
-	Convey("Map to wrong types and gain default values", t, func() {
-		cfg, err := Load([]byte(_INVALID_DATA_CONF_STRUCT))
-		So(err, ShouldBeNil)
-
-		t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
-		So(err, ShouldBeNil)
-		dv := &defaultValue{"Joe", 10, true, 1.25, t, []string{"HangZhou", "Boston"}}
-		So(cfg.MapTo(dv), ShouldBeNil)
-		So(dv.Name, ShouldEqual, "Joe")
-		So(dv.Age, ShouldEqual, 10)
-		So(dv.Male, ShouldBeTrue)
-		So(dv.Money, ShouldEqual, 1.25)
-		So(dv.Born.String(), ShouldEqual, t.String())
-		So(strings.Join(dv.Cities, ","), ShouldEqual, "HangZhou,Boston")
-	})
-}
-
-type testMapper struct {
-	PackageName string
-}
-
-func Test_NameGetter(t *testing.T) {
-	Convey("Test name mappers", t, func() {
-		So(MapToWithMapper(&testMapper{}, TitleUnderscore, []byte("packag_name=ini")), ShouldBeNil)
-
-		cfg, err := Load([]byte("PACKAGE_NAME=ini"))
-		So(err, ShouldBeNil)
-		So(cfg, ShouldNotBeNil)
-
-		cfg.NameMapper = AllCapsUnderscore
-		tg := new(testMapper)
-		So(cfg.MapTo(tg), ShouldBeNil)
-		So(tg.PackageName, ShouldEqual, "ini")
-	})
-}

+ 0 - 2
Godeps/_workspace/src/gopkg.in/ini.v1/testdata/conf.ini

@@ -1,2 +0,0 @@
-[author]
-E-MAIL = u@gogs.io

+ 1 - 1
README.md

@@ -78,7 +78,7 @@ the latest master builds [here](http://grafana.org/download/builds)
 
 
 ### Dependencies
 ### Dependencies
 
 
-- Go 1.5
+- Go 1.6
 - NodeJS v4+
 - NodeJS v4+
 - [Godep](https://github.com/tools/godep)
 - [Godep](https://github.com/tools/godep)
 
 

+ 1 - 1
build.go

@@ -34,7 +34,7 @@ var (
 	binaries              []string = []string{"grafana-server", "grafana-cli"}
 	binaries              []string = []string{"grafana-server", "grafana-cli"}
 )
 )
 
 
-const minGoVersion = 1.3
+const minGoVersion = 1.6
 
 
 func main() {
 func main() {
 	log.SetOutput(os.Stdout)
 	log.SetOutput(os.Stdout)

+ 1 - 1
circle.yml

@@ -31,4 +31,4 @@ deployment:
     branch: master
     branch: master
     owner: grafana
     owner: grafana
     commands:
     commands:
-      - ./trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}
+      - ./trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}

+ 14 - 1
conf/defaults.ini

@@ -59,7 +59,7 @@ cert_key =
 
 
 #################################### Database ####################################
 #################################### Database ####################################
 [database]
 [database]
-# You can configure the database connection by specifying type, host, name, user and password 
+# You can configure the database connection by specifying type, host, name, user and password
 # as seperate properties or as on string using the url propertie.
 # as seperate properties or as on string using the url propertie.
 
 
 # Either "mysql", "postgres" or "sqlite3", it's your choice
 # Either "mysql", "postgres" or "sqlite3", it's your choice
@@ -223,6 +223,19 @@ token_url = https://accounts.google.com/o/oauth2/token
 api_url = https://www.googleapis.com/oauth2/v1/userinfo
 api_url = https://www.googleapis.com/oauth2/v1/userinfo
 allowed_domains =
 allowed_domains =
 
 
+#################################### Generic OAuth ##########################
+[auth.generic_oauth]
+enabled = false
+allow_sign_up = false
+client_id = some_id
+client_secret = some_secret
+scopes = user:email
+auth_url =
+token_url =
+api_url =
+team_ids =
+allowed_organizations =
+
 #################################### Basic Auth ##########################
 #################################### Basic Auth ##########################
 [auth.basic]
 [auth.basic]
 enabled = true
 enabled = true

+ 15 - 2
conf/sample.ini

@@ -61,7 +61,7 @@
 
 
 #################################### Database ####################################
 #################################### Database ####################################
 [database]
 [database]
-# You can configure the database connection by specifying type, host, name, user and password 
+# You can configure the database connection by specifying type, host, name, user and password
 # as seperate properties or as on string using the url propertie.
 # as seperate properties or as on string using the url propertie.
 
 
 # Either "mysql", "postgres" or "sqlite3", it's your choice
 # Either "mysql", "postgres" or "sqlite3", it's your choice
@@ -205,6 +205,19 @@ check_for_updates = true
 ;api_url = https://www.googleapis.com/oauth2/v1/userinfo
 ;api_url = https://www.googleapis.com/oauth2/v1/userinfo
 ;allowed_domains =
 ;allowed_domains =
 
 
+#################################### Generic OAuth ##########################
+[auth.generic_oauth]
+;enabled = false
+;allow_sign_up = false
+;client_id = some_id
+;client_secret = some_secret
+;scopes = user:email,read:org
+;auth_url = https://foo.bar/login/oauth/authorize
+;token_url = https://foo.bar/login/oauth/access_token
+;api_url = https://foo.bar/user
+;team_ids =
+;allowed_organizations =
+
 #################################### Auth Proxy ##########################
 #################################### Auth Proxy ##########################
 [auth.proxy]
 [auth.proxy]
 ;enabled = false
 ;enabled = false
@@ -318,7 +331,7 @@ check_for_updates = true
 #   \______(_______;;;)__;;;)
 #   \______(_______;;;)__;;;)
 
 
 [alerting]
 [alerting]
-enabled = false
+;enabled = false
 
 
 #################################### Internal Grafana Metrics ##########################
 #################################### Internal Grafana Metrics ##########################
 # Metrics available at HTTP API Url /api/metrics
 # Metrics available at HTTP API Url /api/metrics

+ 1 - 1
docs/sources/datasources/cloudwatch.md

@@ -77,7 +77,7 @@ Example dimension queries which will return list of resources for individual AWS
 
 
 Service | Query
 Service | Query
 ------- | -----
 ------- | -----
-EBS | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)`
+ELB | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)`
 ElastiCache | `dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)`
 ElastiCache | `dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)`
 RedShift | `dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)`
 RedShift | `dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)`
 RDS | `dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)`
 RDS | `dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)`

+ 19 - 0
docs/sources/installation/configuration.md

@@ -88,6 +88,8 @@ Another way is put a webserver like Nginx or Apache in front of Grafana and have
 
 
 `http` or `https`
 `http` or `https`
 
 
+> **Note** Grafana versions earlier than 3.0 are vulnerable to [POODLE](https://en.wikipedia.org/wiki/POODLE). So we strongly recommend to upgrade to 3.x or use a reverse proxy for ssl termination.
+
 ### domain
 ### domain
 
 
 This setting is only used in as a part of the `root_url` setting (see below). Important if you
 This setting is only used in as a part of the `root_url` setting (see below). Important if you
@@ -339,6 +341,23 @@ You may allow users to sign-up via Google authentication by setting the
 user successfully authenticating via Google authentication will be
 user successfully authenticating via Google authentication will be
 automatically signed up.
 automatically signed up.
 
 
+## [auth.generic_oauth]
+
+This option could be used if have your own oauth service.
+
+This callback URL must match the full HTTP address that you use in your
+browser to access Grafana, but with the prefix path of `/login/generic_oauth`.
+
+    [auth.generic_oauth]
+    enabled = true
+    client_id = YOUR_APP_CLIENT_ID
+    client_secret = YOUR_APP_CLIENT_SECRET
+    scopes =
+    auth_url =
+    token_url =
+    allowed_domains = mycompany.com mycompany.org
+    allow_sign_up = false
+
 <hr>
 <hr>
 
 
 ## [auth.basic]
 ## [auth.basic]

+ 1 - 1
packaging/deb/systemd/grafana-server.service

@@ -1,5 +1,5 @@
 [Unit]
 [Unit]
-Description=Starts and stops a single grafana instance on this system
+Description=Grafana instance
 Documentation=http://docs.grafana.org
 Documentation=http://docs.grafana.org
 Wants=network-online.target
 Wants=network-online.target
 After=network-online.target
 After=network-online.target

+ 1 - 1
packaging/rpm/systemd/grafana-server.service

@@ -1,5 +1,5 @@
 [Unit]
 [Unit]
-Description=Starts and stops a single grafana instance on this system
+Description=Grafana instance
 Documentation=http://docs.grafana.org
 Documentation=http://docs.grafana.org
 Wants=network-online.target
 Wants=network-online.target
 After=network-online.target
 After=network-online.target

+ 92 - 7
pkg/api/alerting.go

@@ -8,6 +8,7 @@ import (
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/alerting"
 	"github.com/grafana/grafana/pkg/services/alerting"
+	"github.com/grafana/grafana/pkg/services/annotations"
 )
 )
 
 
 func ValidateOrgAlert(c *middleware.Context) {
 func ValidateOrgAlert(c *middleware.Context) {
@@ -146,7 +147,7 @@ func DelAlert(c *middleware.Context) Response {
 }
 }
 
 
 func GetAlertNotifications(c *middleware.Context) Response {
 func GetAlertNotifications(c *middleware.Context) Response {
-	query := &models.GetAlertNotificationsQuery{OrgId: c.OrgId}
+	query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
 
 
 	if err := bus.Dispatch(query); err != nil {
 	if err := bus.Dispatch(query); err != nil {
 		return ApiError(500, "Failed to get alert notifications", err)
 		return ApiError(500, "Failed to get alert notifications", err)
@@ -156,11 +157,12 @@ func GetAlertNotifications(c *middleware.Context) Response {
 
 
 	for _, notification := range query.Result {
 	for _, notification := range query.Result {
 		result = append(result, dtos.AlertNotification{
 		result = append(result, dtos.AlertNotification{
-			Id:      notification.Id,
-			Name:    notification.Name,
-			Type:    notification.Type,
-			Created: notification.Created,
-			Updated: notification.Updated,
+			Id:        notification.Id,
+			Name:      notification.Name,
+			Type:      notification.Type,
+			IsDefault: notification.IsDefault,
+			Created:   notification.Created,
+			Updated:   notification.Updated,
 		})
 		})
 	}
 	}
 
 
@@ -177,7 +179,7 @@ func GetAlertNotificationById(c *middleware.Context) Response {
 		return ApiError(500, "Failed to get alert notifications", err)
 		return ApiError(500, "Failed to get alert notifications", err)
 	}
 	}
 
 
-	return Json(200, query.Result[0])
+	return Json(200, query.Result)
 }
 }
 
 
 func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotificationCommand) Response {
 func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotificationCommand) Response {
@@ -212,3 +214,86 @@ func DeleteAlertNotification(c *middleware.Context) Response {
 
 
 	return ApiSuccess("Notification deleted")
 	return ApiSuccess("Notification deleted")
 }
 }
+
+//POST /api/alert-notifications/test
+func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) Response {
+	cmd := &alerting.NotificationTestCommand{
+		Name:     dto.Name,
+		Type:     dto.Type,
+		Severity: dto.Severity,
+		Settings: dto.Settings,
+	}
+
+	if err := bus.Dispatch(cmd); err != nil {
+		return ApiError(500, "Failed to send alert notifications", err)
+	}
+
+	return ApiSuccess("Test notification sent")
+}
+
+func GetAlertHistory(c *middleware.Context) Response {
+	alertId, err := getAlertIdForRequest(c)
+	if err != nil {
+		return ApiError(400, "Invalid request", err)
+	}
+
+	query := &annotations.ItemQuery{
+		AlertId: alertId,
+		Type:    annotations.AlertType,
+		OrgId:   c.OrgId,
+		Limit:   c.QueryInt64("limit"),
+	}
+
+	repo := annotations.GetRepository()
+
+	items, err := repo.Find(query)
+	if err != nil {
+		return ApiError(500, "Failed to get history for alert", err)
+	}
+
+	var result []dtos.AlertHistory
+	for _, item := range items {
+		result = append(result, dtos.AlertHistory{
+			AlertId:   item.AlertId,
+			Timestamp: item.Timestamp,
+			Data:      item.Data,
+			NewState:  item.NewState,
+			Text:      item.Text,
+			Metric:    item.Metric,
+			Title:     item.Title,
+		})
+	}
+
+	return Json(200, result)
+}
+
+func getAlertIdForRequest(c *middleware.Context) (int64, error) {
+	alertId := c.QueryInt64("alertId")
+	panelId := c.QueryInt64("panelId")
+	dashboardId := c.QueryInt64("dashboardId")
+
+	if alertId == 0 && dashboardId == 0 && panelId == 0 {
+		return 0, fmt.Errorf("Missing alertId or dashboardId and panelId")
+	}
+
+	if alertId == 0 {
+		//fetch alertId
+		query := models.GetAlertsQuery{
+			OrgId:       c.OrgId,
+			DashboardId: dashboardId,
+			PanelId:     panelId,
+		}
+
+		if err := bus.Dispatch(&query); err != nil {
+			return 0, err
+		}
+
+		if len(query.Result) != 1 {
+			return 0, fmt.Errorf("PanelId is not unique on dashboard")
+		}
+
+		alertId = query.Result[0].Id
+	}
+
+	return alertId, nil
+}

+ 6 - 1
pkg/api/api.go

@@ -19,6 +19,9 @@ func Register(r *macaron.Macaron) {
 	quota := middleware.Quota
 	quota := middleware.Quota
 	bind := binding.Bind
 	bind := binding.Bind
 
 
+	// automatically set HEAD for every GET
+	r.SetAutoHead(true)
+
 	// not logged in views
 	// not logged in views
 	r.Get("/", reqSignedIn, Index)
 	r.Get("/", reqSignedIn, Index)
 	r.Get("/logout", Logout)
 	r.Get("/logout", Logout)
@@ -247,14 +250,16 @@ func Register(r *macaron.Macaron) {
 
 
 		r.Group("/alerts", func() {
 		r.Group("/alerts", func() {
 			r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
 			r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
-			//r.Get("/:alertId/states", wrap(GetAlertStates))
 			r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
 			r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
 			r.Get("/", wrap(GetAlerts))
 			r.Get("/", wrap(GetAlerts))
 		})
 		})
 
 
+		r.Get("/alert-history", wrap(GetAlertHistory))
+
 		r.Get("/alert-notifications", wrap(GetAlertNotifications))
 		r.Get("/alert-notifications", wrap(GetAlertNotifications))
 
 
 		r.Group("/alert-notifications", func() {
 		r.Group("/alert-notifications", func() {
+			r.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))
 			r.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification))
 			r.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification))
 			r.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
 			r.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
 			r.Get("/:notificationId", wrap(GetAlertNotificationById))
 			r.Get("/:notificationId", wrap(GetAlertNotificationById))

+ 1 - 1
pkg/api/datasources.go

@@ -22,7 +22,6 @@ func GetDataSources(c *middleware.Context) {
 
 
 	result := make(dtos.DataSourceList, 0)
 	result := make(dtos.DataSourceList, 0)
 	for _, ds := range query.Result {
 	for _, ds := range query.Result {
-
 		dsItem := dtos.DataSource{
 		dsItem := dtos.DataSource{
 			Id:        ds.Id,
 			Id:        ds.Id,
 			OrgId:     ds.OrgId,
 			OrgId:     ds.OrgId,
@@ -35,6 +34,7 @@ func GetDataSources(c *middleware.Context) {
 			User:      ds.User,
 			User:      ds.User,
 			BasicAuth: ds.BasicAuth,
 			BasicAuth: ds.BasicAuth,
 			IsDefault: ds.IsDefault,
 			IsDefault: ds.IsDefault,
+			JsonData:  ds.JsonData,
 		}
 		}
 
 
 		if plugin, exists := plugins.DataSources[ds.Type]; exists {
 		if plugin, exists := plugins.DataSources[ds.Type]; exists {

+ 24 - 5
pkg/api/dtos/alerting.go

@@ -22,11 +22,12 @@ type AlertRule struct {
 }
 }
 
 
 type AlertNotification struct {
 type AlertNotification struct {
-	Id      int64     `json:"id"`
-	Name    string    `json:"name"`
-	Type    string    `json:"type"`
-	Created time.Time `json:"created"`
-	Updated time.Time `json:"updated"`
+	Id        int64     `json:"id"`
+	Name      string    `json:"name"`
+	Type      string    `json:"type"`
+	IsDefault bool      `json:"isDefault"`
+	Created   time.Time `json:"created"`
+	Updated   time.Time `json:"updated"`
 }
 }
 
 
 type AlertTestCommand struct {
 type AlertTestCommand struct {
@@ -52,3 +53,21 @@ type EvalMatch struct {
 	Metric string            `json:"metric"`
 	Metric string            `json:"metric"`
 	Value  float64           `json:"value"`
 	Value  float64           `json:"value"`
 }
 }
+
+type AlertHistory struct {
+	AlertId   int64     `json:"alertId"`
+	NewState  string    `json:"newState"`
+	Timestamp time.Time `json:"timestamp"`
+	Title     string    `json:"title"`
+	Text      string    `json:"text"`
+	Metric    string    `json:"metric"`
+
+	Data *simplejson.Json `json:"data"`
+}
+
+type NotificationTestCommand struct {
+	Name     string           `json:"name"`
+	Type     string           `json:"type"`
+	Settings *simplejson.Json `json:"settings"`
+	Severity string           `json:"severity"`
+}

+ 2 - 0
pkg/api/login.go

@@ -27,6 +27,8 @@ func LoginView(c *middleware.Context) {
 
 
 	viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
 	viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
 	viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
 	viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
+	viewData.Settings["genericOAuthEnabled"] = setting.OAuthService.Generic
+	viewData.Settings["oauthProviderName"] = setting.OAuthService.OAuthProviderName
 	viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
 	viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
 	viewData.Settings["loginHint"] = setting.LoginHint
 	viewData.Settings["loginHint"] = setting.LoginHint
 	viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin
 	viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin

+ 1 - 1
pkg/cmd/grafana-cli/commands/commands.go

@@ -14,7 +14,7 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C
 		cmd := &contextCommandLine{context}
 		cmd := &contextCommandLine{context}
 		if err := command(cmd); err != nil {
 		if err := command(cmd); err != nil {
 			logger.Errorf("\n%s: ", color.RedString("Error"))
 			logger.Errorf("\n%s: ", color.RedString("Error"))
-			logger.Errorf("%s\n\n", err)
+			logger.Errorf("%s %s\n\n", color.RedString("✗"), err)
 
 
 			cmd.ShowHelp()
 			cmd.ShowHelp()
 			os.Exit(1)
 			os.Exit(1)

+ 10 - 2
pkg/cmd/grafana-cli/commands/upgrade_all_command.go

@@ -53,8 +53,16 @@ func upgradeAllCommand(c CommandLine) error {
 	for _, p := range pluginsToUpgrade {
 	for _, p := range pluginsToUpgrade {
 		logger.Infof("Updating %v \n", p.Id)
 		logger.Infof("Updating %v \n", p.Id)
 
 
-		s.RemoveInstalledPlugin(pluginsDir, p.Id)
-		InstallPlugin(p.Id, "", c)
+		var err error
+		err = s.RemoveInstalledPlugin(pluginsDir, p.Id)
+		if err != nil {
+			return err
+		}
+
+		err = InstallPlugin(p.Id, "", c)
+		if err != nil {
+			return err
+		}
 	}
 	}
 
 
 	return nil
 	return nil

+ 3 - 3
pkg/components/imguploader/imguploader.go

@@ -19,9 +19,9 @@ func NewImageUploader() (ImageUploader, error) {
 			return nil, err
 			return nil, err
 		}
 		}
 
 
-		bucket := s3sec.Key("secret_key").String()
-		accessKey := s3sec.Key("access_key").String()
-		secretKey := s3sec.Key("secret_key").String()
+		bucket := s3sec.Key("bucket_url").MustString("")
+		accessKey := s3sec.Key("access_key").MustString("")
+		secretKey := s3sec.Key("secret_key").MustString("")
 
 
 		if bucket == "" {
 		if bucket == "" {
 			return nil, fmt.Errorf("Could not find bucket setting for image.uploader.s3")
 			return nil, fmt.Errorf("Could not find bucket setting for image.uploader.s3")

+ 12 - 3
pkg/components/imguploader/imguploader_test.go

@@ -1,7 +1,6 @@
 package imguploader
 package imguploader
 
 
 import (
 import (
-	"reflect"
 	"testing"
 	"testing"
 
 
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
@@ -27,7 +26,12 @@ func TestImageUploaderFactory(t *testing.T) {
 			uploader, err := NewImageUploader()
 			uploader, err := NewImageUploader()
 
 
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
-			So(reflect.TypeOf(uploader), ShouldEqual, reflect.TypeOf(&S3Uploader{}))
+			original, ok := uploader.(*S3Uploader)
+
+			So(ok, ShouldBeTrue)
+			So(original.accessKey, ShouldEqual, "access_key")
+			So(original.secretKey, ShouldEqual, "secret_key")
+			So(original.bucket, ShouldEqual, "bucket_url")
 		})
 		})
 
 
 		Convey("Webdav uploader", func() {
 		Convey("Webdav uploader", func() {
@@ -47,7 +51,12 @@ func TestImageUploaderFactory(t *testing.T) {
 			uploader, err := NewImageUploader()
 			uploader, err := NewImageUploader()
 
 
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
-			So(reflect.TypeOf(uploader), ShouldEqual, reflect.TypeOf(&WebdavUploader{}))
+			original, ok := uploader.(*WebdavUploader)
+
+			So(ok, ShouldBeTrue)
+			So(original.url, ShouldEqual, "webdavUrl")
+			So(original.username, ShouldEqual, "username")
+			So(original.password, ShouldEqual, "password")
 		})
 		})
 	})
 	})
 }
 }

+ 21 - 5
pkg/components/imguploader/s3uploader.go

@@ -3,7 +3,10 @@ package imguploader
 import (
 import (
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
+	"net/url"
+	"path"
 
 
+	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/kr/s3/s3util"
 	"github.com/kr/s3/s3util"
 )
 )
@@ -12,6 +15,7 @@ type S3Uploader struct {
 	bucket    string
 	bucket    string
 	secretKey string
 	secretKey string
 	accessKey string
 	accessKey string
+	log       log.Logger
 }
 }
 
 
 func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
 func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
@@ -19,10 +23,11 @@ func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
 		bucket:    bucket,
 		bucket:    bucket,
 		accessKey: accessKey,
 		accessKey: accessKey,
 		secretKey: secretKey,
 		secretKey: secretKey,
+		log:       log.New("s3uploader"),
 	}
 	}
 }
 }
 
 
-func (u *S3Uploader) Upload(path string) (string, error) {
+func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
 
 
 	s3util.DefaultConfig.AccessKey = u.accessKey
 	s3util.DefaultConfig.AccessKey = u.accessKey
 	s3util.DefaultConfig.SecretKey = u.secretKey
 	s3util.DefaultConfig.SecretKey = u.secretKey
@@ -31,15 +36,26 @@ func (u *S3Uploader) Upload(path string) (string, error) {
 	header.Add("x-amz-acl", "public-read")
 	header.Add("x-amz-acl", "public-read")
 	header.Add("Content-Type", "image/png")
 	header.Add("Content-Type", "image/png")
 
 
-	fullUrl := u.bucket + util.GetRandomString(20) + ".png"
-	writer, err := s3util.Create(fullUrl, header, nil)
+	var imageUrl *url.URL
+	var err error
+
+	if imageUrl, err = url.Parse(u.bucket); err != nil {
+		return "", err
+	}
+
+	// add image to url
+	imageUrl.Path = path.Join(imageUrl.Path, util.GetRandomString(20)+".png")
+	imageUrlString := imageUrl.String()
+	log.Debug("Uploading image to s3", "url", imageUrlString)
+
+	writer, err := s3util.Create(imageUrlString, header, nil)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
 
 
 	defer writer.Close()
 	defer writer.Close()
 
 
-	imgData, err := ioutil.ReadFile(path)
+	imgData, err := ioutil.ReadFile(imageDiskPath)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}
@@ -49,5 +65,5 @@ func (u *S3Uploader) Upload(path string) (string, error) {
 		return "", err
 		return "", err
 	}
 	}
 
 
-	return fullUrl, nil
+	return imageUrlString, nil
 }
 }

+ 23 - 0
pkg/components/imguploader/s3uploader_test.go

@@ -0,0 +1,23 @@
+package imguploader
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/setting"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestUploadToS3(t *testing.T) {
+	SkipConvey("[Integration test] for external_image_store.webdav", t, func() {
+		setting.NewConfigContext(&setting.CommandLineArgs{
+			HomePath: "../../../",
+		})
+
+		s3Uploader, _ := NewImageUploader()
+
+		path, err := s3Uploader.Upload("../../../public/img/logo_transparent_400x.png")
+
+		So(err, ShouldBeNil)
+		So(path, ShouldNotEqual, "")
+	})
+}

+ 0 - 2
pkg/components/imguploader/webdavuploader.go

@@ -9,7 +9,6 @@ import (
 	"path"
 	"path"
 	"time"
 	"time"
 
 
-	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
@@ -20,7 +19,6 @@ type WebdavUploader struct {
 }
 }
 
 
 func (u *WebdavUploader) Upload(pa string) (string, error) {
 func (u *WebdavUploader) Upload(pa string) (string, error) {
-	log.Error2("Hej")
 	client := http.Client{Timeout: time.Duration(10 * time.Second)}
 	client := http.Client{Timeout: time.Duration(10 * time.Second)}
 
 
 	url, _ := url.Parse(u.url)
 	url, _ := url.Parse(u.url)

+ 37 - 32
pkg/metrics/metrics.go

@@ -9,34 +9,36 @@ func init() {
 }
 }
 
 
 var (
 var (
-	M_Instance_Start                     Counter
-	M_Page_Status_200                    Counter
-	M_Page_Status_500                    Counter
-	M_Page_Status_404                    Counter
-	M_Api_Status_500                     Counter
-	M_Api_Status_404                     Counter
-	M_Api_User_SignUpStarted             Counter
-	M_Api_User_SignUpCompleted           Counter
-	M_Api_User_SignUpInvite              Counter
-	M_Api_Dashboard_Save                 Timer
-	M_Api_Dashboard_Get                  Timer
-	M_Api_Dashboard_Search               Timer
-	M_Api_Admin_User_Create              Counter
-	M_Api_Login_Post                     Counter
-	M_Api_Login_OAuth                    Counter
-	M_Api_Org_Create                     Counter
-	M_Api_Dashboard_Snapshot_Create      Counter
-	M_Api_Dashboard_Snapshot_External    Counter
-	M_Api_Dashboard_Snapshot_Get         Counter
-	M_Models_Dashboard_Insert            Counter
-	M_Alerting_Result_Critical           Counter
-	M_Alerting_Result_Warning            Counter
-	M_Alerting_Result_Info               Counter
-	M_Alerting_Result_Ok                 Counter
-	M_Alerting_Active_Alerts             Counter
-	M_Alerting_Notification_Sent_Slack   Counter
-	M_Alerting_Notification_Sent_Email   Counter
-	M_Alerting_Notification_Sent_Webhook Counter
+	M_Instance_Start                       Counter
+	M_Page_Status_200                      Counter
+	M_Page_Status_500                      Counter
+	M_Page_Status_404                      Counter
+	M_Api_Status_500                       Counter
+	M_Api_Status_404                       Counter
+	M_Api_User_SignUpStarted               Counter
+	M_Api_User_SignUpCompleted             Counter
+	M_Api_User_SignUpInvite                Counter
+	M_Api_Dashboard_Save                   Timer
+	M_Api_Dashboard_Get                    Timer
+	M_Api_Dashboard_Search                 Timer
+	M_Api_Admin_User_Create                Counter
+	M_Api_Login_Post                       Counter
+	M_Api_Login_OAuth                      Counter
+	M_Api_Org_Create                       Counter
+	M_Api_Dashboard_Snapshot_Create        Counter
+	M_Api_Dashboard_Snapshot_External      Counter
+	M_Api_Dashboard_Snapshot_Get           Counter
+	M_Models_Dashboard_Insert              Counter
+	M_Alerting_Result_State_Critical       Counter
+	M_Alerting_Result_State_Warning        Counter
+	M_Alerting_Result_State_Ok             Counter
+	M_Alerting_Result_State_Paused         Counter
+	M_Alerting_Result_State_Unknown        Counter
+	M_Alerting_Result_State_ExecutionError Counter
+	M_Alerting_Active_Alerts               Counter
+	M_Alerting_Notification_Sent_Slack     Counter
+	M_Alerting_Notification_Sent_Email     Counter
+	M_Alerting_Notification_Sent_Webhook   Counter
 
 
 	// Timers
 	// Timers
 	M_DataSource_ProxyReq_Timer Timer
 	M_DataSource_ProxyReq_Timer Timer
@@ -75,10 +77,13 @@ func initMetricVars(settings *MetricSettings) {
 
 
 	M_Models_Dashboard_Insert = RegCounter("models.dashboard.insert")
 	M_Models_Dashboard_Insert = RegCounter("models.dashboard.insert")
 
 
-	M_Alerting_Result_Critical = RegCounter("alerting.result", "severity", "critical")
-	M_Alerting_Result_Warning = RegCounter("alerting.result", "severity", "warning")
-	M_Alerting_Result_Info = RegCounter("alerting.result", "severity", "info")
-	M_Alerting_Result_Ok = RegCounter("alerting.result", "severity", "ok")
+	M_Alerting_Result_State_Critical = RegCounter("alerting.result", "state", "critical")
+	M_Alerting_Result_State_Warning = RegCounter("alerting.result", "state", "warning")
+	M_Alerting_Result_State_Ok = RegCounter("alerting.result", "state", "ok")
+	M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused")
+	M_Alerting_Result_State_Unknown = RegCounter("alerting.result", "state", "unknown")
+	M_Alerting_Result_State_ExecutionError = RegCounter("alerting.result", "state", "execution_error")
+
 	M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts")
 	M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts")
 	M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")
 	M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")
 	M_Alerting_Notification_Sent_Email = RegCounter("alerting.notifications_sent", "type", "email")
 	M_Alerting_Notification_Sent_Email = RegCounter("alerting.notifications_sent", "type", "email")

+ 2 - 2
pkg/models/alert.go

@@ -10,7 +10,7 @@ type AlertStateType string
 type AlertSeverityType string
 type AlertSeverityType string
 
 
 const (
 const (
-	AlertStatePending        AlertStateType = "pending"
+	AlertStateUnknown        AlertStateType = "unknown"
 	AlertStateExeuctionError AlertStateType = "execution_error"
 	AlertStateExeuctionError AlertStateType = "execution_error"
 	AlertStatePaused         AlertStateType = "paused"
 	AlertStatePaused         AlertStateType = "paused"
 	AlertStateCritical       AlertStateType = "critical"
 	AlertStateCritical       AlertStateType = "critical"
@@ -19,7 +19,7 @@ const (
 )
 )
 
 
 func (s AlertStateType) IsValid() bool {
 func (s AlertStateType) IsValid() bool {
-	return s == AlertStateOK || s == AlertStatePending || s == AlertStateExeuctionError || s == AlertStatePaused || s == AlertStateCritical || s == AlertStateWarning
+	return s == AlertStateOK || s == AlertStateUnknown || s == AlertStateExeuctionError || s == AlertStatePaused || s == AlertStateCritical || s == AlertStateWarning
 }
 }
 
 
 const (
 const (

+ 29 - 14
pkg/models/alert_notifications.go

@@ -7,29 +7,32 @@ import (
 )
 )
 
 
 type AlertNotification struct {
 type AlertNotification struct {
-	Id       int64            `json:"id"`
-	OrgId    int64            `json:"-"`
-	Name     string           `json:"name"`
-	Type     string           `json:"type"`
-	Settings *simplejson.Json `json:"settings"`
-	Created  time.Time        `json:"created"`
-	Updated  time.Time        `json:"updated"`
+	Id        int64            `json:"id"`
+	OrgId     int64            `json:"-"`
+	Name      string           `json:"name"`
+	Type      string           `json:"type"`
+	IsDefault bool             `json:"isDefault"`
+	Settings  *simplejson.Json `json:"settings"`
+	Created   time.Time        `json:"created"`
+	Updated   time.Time        `json:"updated"`
 }
 }
 
 
 type CreateAlertNotificationCommand struct {
 type CreateAlertNotificationCommand struct {
-	Name     string           `json:"name"  binding:"Required"`
-	Type     string           `json:"type"  binding:"Required"`
-	Settings *simplejson.Json `json:"settings"`
+	Name      string           `json:"name"  binding:"Required"`
+	Type      string           `json:"type"  binding:"Required"`
+	IsDefault bool             `json:"isDefault"`
+	Settings  *simplejson.Json `json:"settings"`
 
 
 	OrgId  int64 `json:"-"`
 	OrgId  int64 `json:"-"`
 	Result *AlertNotification
 	Result *AlertNotification
 }
 }
 
 
 type UpdateAlertNotificationCommand struct {
 type UpdateAlertNotificationCommand struct {
-	Id       int64            `json:"id"  binding:"Required"`
-	Name     string           `json:"name"  binding:"Required"`
-	Type     string           `json:"type"  binding:"Required"`
-	Settings *simplejson.Json `json:"settings"  binding:"Required"`
+	Id        int64            `json:"id"  binding:"Required"`
+	Name      string           `json:"name"  binding:"Required"`
+	Type      string           `json:"type"  binding:"Required"`
+	IsDefault bool             `json:"isDefault"`
+	Settings  *simplejson.Json `json:"settings"  binding:"Required"`
 
 
 	OrgId  int64 `json:"-"`
 	OrgId  int64 `json:"-"`
 	Result *AlertNotification
 	Result *AlertNotification
@@ -43,8 +46,20 @@ type DeleteAlertNotificationCommand struct {
 type GetAlertNotificationsQuery struct {
 type GetAlertNotificationsQuery struct {
 	Name  string
 	Name  string
 	Id    int64
 	Id    int64
+	OrgId int64
+
+	Result *AlertNotification
+}
+
+type GetAlertNotificationsToSendQuery struct {
 	Ids   []int64
 	Ids   []int64
 	OrgId int64
 	OrgId int64
 
 
 	Result []*AlertNotification
 	Result []*AlertNotification
 }
 }
+
+type GetAllAlertNotificationsQuery struct {
+	OrgId int64
+
+	Result []*AlertNotification
+}

+ 1 - 0
pkg/models/models.go

@@ -6,4 +6,5 @@ const (
 	GITHUB OAuthType = iota + 1
 	GITHUB OAuthType = iota + 1
 	GOOGLE
 	GOOGLE
 	TWITTER
 	TWITTER
+	GENERIC
 )
 )

+ 22 - 20
pkg/services/alerting/conditions/evaluator.go

@@ -5,25 +5,21 @@ import (
 
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/services/alerting"
 	"github.com/grafana/grafana/pkg/services/alerting"
-	"github.com/grafana/grafana/pkg/tsdb"
 )
 )
 
 
 var (
 var (
-	defaultTypes   []string = []string{"gt", "lt"}
-	rangedTypes    []string = []string{"within_range", "outside_range"}
-	paramlessTypes []string = []string{"no_value"}
+	defaultTypes []string = []string{"gt", "lt"}
+	rangedTypes  []string = []string{"within_range", "outside_range"}
 )
 )
 
 
 type AlertEvaluator interface {
 type AlertEvaluator interface {
-	Eval(timeSeries *tsdb.TimeSeries, reducedValue float64) bool
+	Eval(reducedValue *float64) bool
 }
 }
 
 
-type ParameterlessEvaluator struct {
-	Type string
-}
+type NoDataEvaluator struct{}
 
 
-func (e *ParameterlessEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool {
-	return len(series.Points) == 0
+func (e *NoDataEvaluator) Eval(reducedValue *float64) bool {
+	return reducedValue == nil
 }
 }
 
 
 type ThresholdEvaluator struct {
 type ThresholdEvaluator struct {
@@ -47,14 +43,16 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu
 	return defaultEval, nil
 	return defaultEval, nil
 }
 }
 
 
-func (e *ThresholdEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool {
+func (e *ThresholdEvaluator) Eval(reducedValue *float64) bool {
+	if reducedValue == nil {
+		return false
+	}
+
 	switch e.Type {
 	switch e.Type {
 	case "gt":
 	case "gt":
-		return reducedValue > e.Threshold
+		return *reducedValue > e.Threshold
 	case "lt":
 	case "lt":
-		return reducedValue < e.Threshold
-	case "no_value":
-		return len(series.Points) == 0
+		return *reducedValue < e.Threshold
 	}
 	}
 
 
 	return false
 	return false
@@ -88,12 +86,16 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
 	return rangedEval, nil
 	return rangedEval, nil
 }
 }
 
 
-func (e *RangedEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool {
+func (e *RangedEvaluator) Eval(reducedValue *float64) bool {
+	if reducedValue == nil {
+		return false
+	}
+
 	switch e.Type {
 	switch e.Type {
 	case "within_range":
 	case "within_range":
-		return (e.Lower < reducedValue && e.Upper > reducedValue) || (e.Upper < reducedValue && e.Lower > reducedValue)
+		return (e.Lower < *reducedValue && e.Upper > *reducedValue) || (e.Upper < *reducedValue && e.Lower > *reducedValue)
 	case "outside_range":
 	case "outside_range":
-		return (e.Upper < reducedValue && e.Lower < reducedValue) || (e.Upper > reducedValue && e.Lower > reducedValue)
+		return (e.Upper < *reducedValue && e.Lower < *reducedValue) || (e.Upper > *reducedValue && e.Lower > *reducedValue)
 	}
 	}
 
 
 	return false
 	return false
@@ -113,8 +115,8 @@ func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) {
 		return newRangedEvaluator(typ, model)
 		return newRangedEvaluator(typ, model)
 	}
 	}
 
 
-	if inSlice(typ, paramlessTypes) {
-		return &ParameterlessEvaluator{Type: typ}, nil
+	if typ == "no_data" {
+		return &NoDataEvaluator{}, nil
 	}
 	}
 
 
 	return nil, alerting.ValidationError{Reason: "Evaludator invalid evaluator type"}
 	return nil, alerting.ValidationError{Reason: "Evaludator invalid evaluator type"}

+ 1 - 14
pkg/services/alerting/conditions/evaluator_test.go

@@ -4,7 +4,6 @@ import (
 	"testing"
 	"testing"
 
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
-	"github.com/grafana/grafana/pkg/tsdb"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
@@ -15,19 +14,7 @@ func evalutorScenario(json string, reducedValue float64, datapoints ...float64)
 	evaluator, err := NewAlertEvaluator(jsonModel)
 	evaluator, err := NewAlertEvaluator(jsonModel)
 	So(err, ShouldBeNil)
 	So(err, ShouldBeNil)
 
 
-	var timeserie [][2]float64
-	dummieTimestamp := float64(521452145)
-
-	for _, v := range datapoints {
-		timeserie = append(timeserie, [2]float64{v, dummieTimestamp})
-	}
-
-	tsdb := &tsdb.TimeSeries{
-		Name:   "test time serie",
-		Points: timeserie,
-	}
-
-	return evaluator.Eval(tsdb, reducedValue)
+	return evaluator.Eval(reducedValue)
 }
 }
 
 
 func TestEvalutors(t *testing.T) {
 func TestEvalutors(t *testing.T) {

+ 18 - 7
pkg/services/alerting/conditions/query.go

@@ -40,22 +40,27 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
 
 
 	for _, series := range seriesList {
 	for _, series := range seriesList {
 		reducedValue := c.Reducer.Reduce(series)
 		reducedValue := c.Reducer.Reduce(series)
-		evalMatch := c.Evaluator.Eval(series, reducedValue)
+		evalMatch := c.Evaluator.Eval(reducedValue)
 
 
 		if context.IsTestRun {
 		if context.IsTestRun {
 			context.Logs = append(context.Logs, &alerting.ResultLogEntry{
 			context.Logs = append(context.Logs, &alerting.ResultLogEntry{
-				Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, reducedValue),
+				Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, *reducedValue),
 			})
 			})
 		}
 		}
 
 
 		if evalMatch {
 		if evalMatch {
 			context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
 			context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
 				Metric: series.Name,
 				Metric: series.Name,
-				Value:  reducedValue,
+				Value:  *reducedValue,
 			})
 			})
 		}
 		}
 
 
 		context.Firing = evalMatch
 		context.Firing = evalMatch
+
+		// handle no data scenario
+		if reducedValue == nil {
+			context.NoDataFound = true
+		}
 	}
 	}
 }
 }
 
 
@@ -106,10 +111,16 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource) *tsdb.
 				RefId: "A",
 				RefId: "A",
 				Query: c.Query.Model.Get("target").MustString(),
 				Query: c.Query.Model.Get("target").MustString(),
 				DataSource: &tsdb.DataSourceInfo{
 				DataSource: &tsdb.DataSourceInfo{
-					Id:       datasource.Id,
-					Name:     datasource.Name,
-					PluginId: datasource.Type,
-					Url:      datasource.Url,
+					Id:                datasource.Id,
+					Name:              datasource.Name,
+					PluginId:          datasource.Type,
+					Url:               datasource.Url,
+					User:              datasource.User,
+					Password:          datasource.Password,
+					Database:          datasource.Database,
+					BasicAuth:         datasource.BasicAuth,
+					BasicAuthUser:     datasource.BasicAuthUser,
+					BasicAuthPassword: datasource.BasicAuthPassword,
 				},
 				},
 			},
 			},
 		},
 		},

+ 39 - 19
pkg/services/alerting/conditions/reducer.go

@@ -1,52 +1,72 @@
 package conditions
 package conditions
 
 
-import "github.com/grafana/grafana/pkg/tsdb"
+import (
+	"math"
+
+	"github.com/grafana/grafana/pkg/tsdb"
+)
 
 
 type QueryReducer interface {
 type QueryReducer interface {
-	Reduce(timeSeries *tsdb.TimeSeries) float64
+	Reduce(timeSeries *tsdb.TimeSeries) *float64
 }
 }
 
 
 type SimpleReducer struct {
 type SimpleReducer struct {
 	Type string
 	Type string
 }
 }
 
 
-func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) float64 {
-	var value float64 = 0
+func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
+	if len(series.Points) == 0 {
+		return nil
+	}
+
+	value := float64(0)
+	allNull := true
 
 
 	switch s.Type {
 	switch s.Type {
 	case "avg":
 	case "avg":
 		for _, point := range series.Points {
 		for _, point := range series.Points {
-			value += point[0]
+			if point[0] != nil {
+				value += *point[0]
+				allNull = false
+			}
 		}
 		}
 		value = value / float64(len(series.Points))
 		value = value / float64(len(series.Points))
 	case "sum":
 	case "sum":
 		for _, point := range series.Points {
 		for _, point := range series.Points {
-			value += point[0]
+			if point[0] != nil {
+				value += *point[0]
+				allNull = false
+			}
 		}
 		}
 	case "min":
 	case "min":
-		for i, point := range series.Points {
-			if i == 0 {
-				value = point[0]
-			}
-
-			if value > point[0] {
-				value = point[0]
+		value = math.MaxFloat64
+		for _, point := range series.Points {
+			if point[0] != nil {
+				allNull = false
+				if value > *point[0] {
+					value = *point[0]
+				}
 			}
 			}
 		}
 		}
 	case "max":
 	case "max":
+		value = -math.MaxFloat64
 		for _, point := range series.Points {
 		for _, point := range series.Points {
-			if value < point[0] {
-				value = point[0]
+			if point[0] != nil {
+				allNull = false
+				if value < *point[0] {
+					value = *point[0]
+				}
 			}
 			}
 		}
 		}
-	case "mean":
-		meanPosition := int64(len(series.Points) / 2)
-		value = series.Points[meanPosition][0]
 	case "count":
 	case "count":
 		value = float64(len(series.Points))
 		value = float64(len(series.Points))
 	}
 	}
 
 
-	return value
+	if allNull {
+		return nil
+	}
+
+	return &value
 }
 }
 
 
 func NewSimpleReducer(typ string) *SimpleReducer {
 func NewSimpleReducer(typ string) *SimpleReducer {

+ 39 - 21
pkg/services/alerting/eval_context.go

@@ -26,38 +26,55 @@ type EvalContext struct {
 	dashboardSlug   string
 	dashboardSlug   string
 	ImagePublicUrl  string
 	ImagePublicUrl  string
 	ImageOnDiskPath string
 	ImageOnDiskPath string
+	NoDataFound     bool
+	RetryCount      int
 }
 }
 
 
-func (a *EvalContext) GetDurationMs() float64 {
-	return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
+type StateDescription struct {
+	Color string
+	Text  string
+	Data  string
 }
 }
 
 
-func (c *EvalContext) GetColor() string {
-	if !c.Firing {
-		return "#36a64f"
+func (c *EvalContext) GetStateModel() *StateDescription {
+	switch c.Rule.State {
+	case m.AlertStateOK:
+		return &StateDescription{
+			Color: "#36a64f",
+			Text:  "OK",
+		}
+	case m.AlertStateUnknown:
+		return &StateDescription{
+			Color: "#888888",
+			Text:  "UNKNOWN",
+		}
+	case m.AlertStateExeuctionError:
+		return &StateDescription{
+			Color: "#000",
+			Text:  "EXECUTION_ERROR",
+		}
+	case m.AlertStateWarning:
+		return &StateDescription{
+			Color: "#fd821b",
+			Text:  "WARNING",
+		}
+	case m.AlertStateCritical:
+		return &StateDescription{
+			Color: "#D63232",
+			Text:  "CRITICAL",
+		}
+	default:
+		panic("Unknown rule state " + c.Rule.State)
 	}
 	}
 
 
-	if c.Rule.Severity == m.AlertSeverityWarning {
-		return "#fd821b"
-	} else {
-		return "#D63232"
-	}
 }
 }
 
 
-func (c *EvalContext) GetStateText() string {
-	if !c.Firing {
-		return "OK"
-	}
-
-	if c.Rule.Severity == m.AlertSeverityWarning {
-		return "WARNING"
-	} else {
-		return "CRITICAL"
-	}
+func (a *EvalContext) GetDurationMs() float64 {
+	return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
 }
 }
 
 
 func (c *EvalContext) GetNotificationTitle() string {
 func (c *EvalContext) GetNotificationTitle() string {
-	return "[" + c.GetStateText() + "] " + c.Rule.Name
+	return "[" + c.GetStateModel().Text + "] " + c.Rule.Name
 }
 }
 
 
 func (c *EvalContext) getDashboardSlug() (string, error) {
 func (c *EvalContext) getDashboardSlug() (string, error) {
@@ -101,5 +118,6 @@ func NewEvalContext(rule *Rule) *EvalContext {
 		DoneChan:    make(chan bool, 1),
 		DoneChan:    make(chan bool, 1),
 		CancelChan:  make(chan bool, 1),
 		CancelChan:  make(chan bool, 1),
 		log:         log.New("alerting.evalContext"),
 		log:         log.New("alerting.evalContext"),
+		RetryCount:  0,
 	}
 	}
 }
 }

+ 27 - 1
pkg/services/alerting/eval_handler.go

@@ -8,6 +8,10 @@ import (
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/metrics"
 )
 )
 
 
+var (
+	MaxRetries int = 1
+)
+
 type DefaultEvalHandler struct {
 type DefaultEvalHandler struct {
 	log             log.Logger
 	log             log.Logger
 	alertJobTimeout time.Duration
 	alertJobTimeout time.Duration
@@ -21,7 +25,6 @@ func NewEvalHandler() *DefaultEvalHandler {
 }
 }
 
 
 func (e *DefaultEvalHandler) Eval(context *EvalContext) {
 func (e *DefaultEvalHandler) Eval(context *EvalContext) {
-
 	go e.eval(context)
 	go e.eval(context)
 
 
 	select {
 	select {
@@ -29,13 +32,36 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
 		context.Error = fmt.Errorf("Timeout")
 		context.Error = fmt.Errorf("Timeout")
 		context.EndTime = time.Now()
 		context.EndTime = time.Now()
 		e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id)
 		e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id)
+		e.retry(context)
 	case <-context.DoneChan:
 	case <-context.DoneChan:
 		e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing)
 		e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing)
+
+		if context.Error != nil {
+			e.retry(context)
+		}
 	}
 	}
+}
+
+func (e *DefaultEvalHandler) retry(context *EvalContext) {
+	e.log.Debug("Retrying eval exeuction", "alertId", context.Rule.Id)
 
 
+	context.RetryCount++
+	if context.RetryCount > MaxRetries {
+		context.DoneChan = make(chan bool, 1)
+		context.CancelChan = make(chan bool, 1)
+		e.Eval(context)
+	}
 }
 }
 
 
 func (e *DefaultEvalHandler) eval(context *EvalContext) {
 func (e *DefaultEvalHandler) eval(context *EvalContext) {
+	defer func() {
+		if err := recover(); err != nil {
+			e.log.Error("Alerting rule eval panic", "error", err, "stack", log.Stack(1))
+			if panicErr, ok := err.(error); ok {
+				context.Error = panicErr
+			}
+		}
+	}()
 
 
 	for _, condition := range context.Rule.Conditions {
 	for _, condition := range context.Rule.Conditions {
 		condition.Eval(context)
 		condition.Eval(context)

+ 0 - 1
pkg/services/alerting/eval_handler_test.go

@@ -40,6 +40,5 @@ func TestAlertingExecutor(t *testing.T) {
 			handler.eval(context)
 			handler.eval(context)
 			So(context.Firing, ShouldEqual, false)
 			So(context.Firing, ShouldEqual, false)
 		})
 		})
-
 	})
 	})
 }
 }

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

@@ -1,6 +1,10 @@
 package alerting
 package alerting
 
 
-import "time"
+import (
+	"time"
+
+	"github.com/grafana/grafana/pkg/models"
+)
 
 
 type EvalHandler interface {
 type EvalHandler interface {
 	Eval(context *EvalContext)
 	Eval(context *EvalContext)
@@ -15,6 +19,7 @@ type Notifier interface {
 	Notify(alertResult *EvalContext)
 	Notify(alertResult *EvalContext)
 	GetType() string
 	GetType() string
 	NeedsImage() bool
 	NeedsImage() bool
+	MatchSeverity(result models.AlertSeverityType) bool
 }
 }
 
 
 type Condition interface {
 type Condition interface {

+ 5 - 4
pkg/services/alerting/models.go

@@ -1,10 +1,11 @@
 package alerting
 package alerting
 
 
 type Job struct {
 type Job struct {
-	Offset  int64
-	Delay   bool
-	Running bool
-	Rule    *Rule
+	Offset     int64
+	OffsetWait bool
+	Delay      bool
+	Running    bool
+	Rule       *Rule
 }
 }
 
 
 type ResultLogEntry struct {
 type ResultLogEntry struct {

+ 28 - 11
pkg/services/alerting/notifier.go

@@ -28,10 +28,14 @@ func (n *RootNotifier) NeedsImage() bool {
 	return false
 	return false
 }
 }
 
 
+func (n *RootNotifier) MatchSeverity(result m.AlertSeverityType) bool {
+	return false
+}
+
 func (n *RootNotifier) Notify(context *EvalContext) {
 func (n *RootNotifier) Notify(context *EvalContext) {
 	n.log.Info("Sending notifications for", "ruleId", context.Rule.Id)
 	n.log.Info("Sending notifications for", "ruleId", context.Rule.Id)
 
 
-	notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications)
+	notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
 	if err != nil {
 	if err != nil {
 		n.log.Error("Failed to read notifications", "error", err)
 		n.log.Error("Failed to read notifications", "error", err)
 		return
 		return
@@ -46,15 +50,17 @@ func (n *RootNotifier) Notify(context *EvalContext) {
 		n.log.Error("Failed to upload alert panel image", "error", err)
 		n.log.Error("Failed to upload alert panel image", "error", err)
 	}
 	}
 
 
+	n.sendNotifications(notifiers, context)
+}
+
+func (n *RootNotifier) sendNotifications(notifiers []Notifier, context *EvalContext) {
 	for _, notifier := range notifiers {
 	for _, notifier := range notifiers {
 		n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
 		n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
-
 		go notifier.Notify(context)
 		go notifier.Notify(context)
 	}
 	}
 }
 }
 
 
 func (n *RootNotifier) uploadImage(context *EvalContext) error {
 func (n *RootNotifier) uploadImage(context *EvalContext) error {
-
 	uploader, _ := imguploader.NewImageUploader()
 	uploader, _ := imguploader.NewImageUploader()
 
 
 	imageUrl, err := context.GetImageUrl()
 	imageUrl, err := context.GetImageUrl()
@@ -85,29 +91,28 @@ func (n *RootNotifier) uploadImage(context *EvalContext) error {
 	return nil
 	return nil
 }
 }
 
 
-func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64) ([]Notifier, error) {
-	if len(notificationIds) == 0 {
-		return []Notifier{}, nil
-	}
+func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) ([]Notifier, error) {
+	query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
 
 
-	query := &m.GetAlertNotificationsQuery{OrgId: orgId, Ids: notificationIds}
 	if err := bus.Dispatch(query); err != nil {
 	if err := bus.Dispatch(query); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	var result []Notifier
 	var result []Notifier
 	for _, notification := range query.Result {
 	for _, notification := range query.Result {
-		if not, err := n.getNotifierFor(notification); err != nil {
+		if not, err := n.createNotifierFor(notification); err != nil {
 			return nil, err
 			return nil, err
 		} else {
 		} else {
-			result = append(result, not)
+			if shouldUseNotification(not, context) {
+				result = append(result, not)
+			}
 		}
 		}
 	}
 	}
 
 
 	return result, nil
 	return result, nil
 }
 }
 
 
-func (n *RootNotifier) getNotifierFor(model *m.AlertNotification) (Notifier, error) {
+func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
 	factory, found := notifierFactories[model.Type]
 	factory, found := notifierFactories[model.Type]
 	if !found {
 	if !found {
 		return nil, errors.New("Unsupported notification type")
 		return nil, errors.New("Unsupported notification type")
@@ -116,6 +121,18 @@ func (n *RootNotifier) getNotifierFor(model *m.AlertNotification) (Notifier, err
 	return factory(model)
 	return factory(model)
 }
 }
 
 
+func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
+	if !context.Firing {
+		return true
+	}
+
+	if context.Error != nil {
+		return true
+	}
+
+	return notifier.MatchSeverity(context.Rule.Severity)
+}
+
 type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
 type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
 
 
 var notifierFactories map[string]NotifierFactory = make(map[string]NotifierFactory)
 var notifierFactories map[string]NotifierFactory = make(map[string]NotifierFactory)

+ 80 - 112
pkg/services/alerting/notifier_test.go

@@ -1,114 +1,82 @@
 package alerting
 package alerting
 
 
-// func TestAlertNotificationExtraction(t *testing.T) {
-// 	Convey("Notifier tests", t, func() {
-// 		Convey("rules for sending notifications", func() {
-// 			dummieNotifier := NotifierImpl{}
-//
-// 			result := &AlertResult{
-// 				State: alertstates.Critical,
-// 			}
-//
-// 			notifier := &Notification{
-// 				Name:         "Test Notifier",
-// 				Type:         "TestType",
-// 				SendCritical: true,
-// 				SendWarning:  true,
-// 			}
-//
-// 			Convey("Should send notification", func() {
-// 				So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeTrue)
-// 			})
-//
-// 			Convey("warn:false and state:warn should not send", func() {
-// 				result.State = alertstates.Warn
-// 				notifier.SendWarning = false
-// 				So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeFalse)
-// 			})
-// 		})
-//
-// 		Convey("Parsing alert notification from settings", func() {
-// 			Convey("Parsing email", func() {
-// 				Convey("empty settings should return error", func() {
-// 					json := `{ }`
-//
-// 					settingsJSON, _ := simplejson.NewJson([]byte(json))
-// 					model := &m.AlertNotification{
-// 						Name:     "ops",
-// 						Type:     "email",
-// 						Settings: settingsJSON,
-// 					}
-//
-// 					_, err := NewNotificationFromDBModel(model)
-// 					So(err, ShouldNotBeNil)
-// 				})
-//
-// 				Convey("from settings", func() {
-// 					json := `
-// 				{
-// 					"to": "ops@grafana.org"
-// 				}`
-//
-// 					settingsJSON, _ := simplejson.NewJson([]byte(json))
-// 					model := &m.AlertNotification{
-// 						Name:     "ops",
-// 						Type:     "email",
-// 						Settings: settingsJSON,
-// 					}
-//
-// 					not, err := NewNotificationFromDBModel(model)
-//
-// 					So(err, ShouldBeNil)
-// 					So(not.Name, ShouldEqual, "ops")
-// 					So(not.Type, ShouldEqual, "email")
-// 					So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.EmailNotifier")
-//
-// 					email := not.Notifierr.(*EmailNotifier)
-// 					So(email.To, ShouldEqual, "ops@grafana.org")
-// 				})
-// 			})
-//
-// 			Convey("Parsing webhook", func() {
-// 				Convey("empty settings should return error", func() {
-// 					json := `{ }`
-//
-// 					settingsJSON, _ := simplejson.NewJson([]byte(json))
-// 					model := &m.AlertNotification{
-// 						Name:     "ops",
-// 						Type:     "webhook",
-// 						Settings: settingsJSON,
-// 					}
-//
-// 					_, err := NewNotificationFromDBModel(model)
-// 					So(err, ShouldNotBeNil)
-// 				})
-//
-// 				Convey("from settings", func() {
-// 					json := `
-// 				{
-// 					"url": "http://localhost:3000",
-// 					"username": "username",
-// 					"password": "password"
-// 				}`
-//
-// 					settingsJSON, _ := simplejson.NewJson([]byte(json))
-// 					model := &m.AlertNotification{
-// 						Name:     "slack",
-// 						Type:     "webhook",
-// 						Settings: settingsJSON,
-// 					}
-//
-// 					not, err := NewNotificationFromDBModel(model)
-//
-// 					So(err, ShouldBeNil)
-// 					So(not.Name, ShouldEqual, "slack")
-// 					So(not.Type, ShouldEqual, "webhook")
-// 					So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.WebhookNotifier")
-//
-// 					webhook := not.Notifierr.(*WebhookNotifier)
-// 					So(webhook.Url, ShouldEqual, "http://localhost:3000")
-// 				})
-// 			})
-// 		})
-// 	})
-// }
+import (
+	"testing"
+
+	"fmt"
+
+	"github.com/grafana/grafana/pkg/models"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+type FakeNotifier struct {
+	FakeMatchResult bool
+}
+
+func (fn *FakeNotifier) GetType() string {
+	return "FakeNotifier"
+}
+
+func (fn *FakeNotifier) NeedsImage() bool {
+	return true
+}
+
+func (fn *FakeNotifier) Notify(alertResult *EvalContext) {}
+
+func (fn *FakeNotifier) MatchSeverity(result models.AlertSeverityType) bool {
+	return fn.FakeMatchResult
+}
+
+func TestAlertNotificationExtraction(t *testing.T) {
+
+	Convey("Notifier tests", t, func() {
+		Convey("none firing alerts", func() {
+			ctx := &EvalContext{
+				Firing: false,
+				Rule: &Rule{
+					Severity: models.AlertSeverityCritical,
+				},
+			}
+			notifier := &FakeNotifier{FakeMatchResult: false}
+
+			So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
+		})
+
+		Convey("exeuction error cannot be ignored", func() {
+			ctx := &EvalContext{
+				Firing: true,
+				Error:  fmt.Errorf("I used to be a programmer just like you"),
+				Rule: &Rule{
+					Severity: models.AlertSeverityCritical,
+				},
+			}
+			notifier := &FakeNotifier{FakeMatchResult: false}
+
+			So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
+		})
+
+		Convey("firing alert that match", func() {
+			ctx := &EvalContext{
+				Firing: true,
+				Rule: &Rule{
+					Severity: models.AlertSeverityCritical,
+				},
+			}
+			notifier := &FakeNotifier{FakeMatchResult: true}
+
+			So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
+		})
+
+		Convey("firing alert that dont match", func() {
+			ctx := &EvalContext{
+				Firing: true,
+				Rule: &Rule{
+					Severity: models.AlertSeverityCritical,
+				},
+			}
+			notifier := &FakeNotifier{FakeMatchResult: false}
+
+			So(shouldUseNotification(notifier, ctx), ShouldBeFalse)
+		})
+	})
+}

+ 28 - 2
pkg/services/alerting/notifiers/base.go

@@ -1,8 +1,34 @@
 package notifiers
 package notifiers
 
 
+import (
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/models"
+)
+
 type NotifierBase struct {
 type NotifierBase struct {
-	Name string
-	Type string
+	Name           string
+	Type           string
+	SeverityFilter models.AlertSeverityType
+}
+
+func NewNotifierBase(name, notifierType string, model *simplejson.Json) NotifierBase {
+	base := NotifierBase{Name: name, Type: notifierType}
+
+	severityFilter := models.AlertSeverityType(model.Get("severityFilter").MustString(""))
+
+	if severityFilter == models.AlertSeverityCritical || severityFilter == models.AlertSeverityWarning {
+		base.SeverityFilter = severityFilter
+	}
+
+	return base
+}
+
+func (n *NotifierBase) MatchSeverity(result models.AlertSeverityType) bool {
+	if !n.SeverityFilter.IsValid() {
+		return true
+	}
+
+	return n.SeverityFilter == result
 }
 }
 
 
 func (n *NotifierBase) GetType() string {
 func (n *NotifierBase) GetType() string {

+ 36 - 0
pkg/services/alerting/notifiers/base_test.go

@@ -0,0 +1,36 @@
+package notifiers
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	m "github.com/grafana/grafana/pkg/models"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestBaseNotifier(t *testing.T) {
+	Convey("Parsing base notification severity", t, func() {
+
+		Convey("matches", func() {
+			json := `
+				{
+					"severityFilter": "critical"
+				}`
+
+			settingsJSON, _ := simplejson.NewJson([]byte(json))
+			not := NewNotifierBase("ops", "email", settingsJSON)
+			So(not.MatchSeverity(m.AlertSeverityCritical), ShouldBeTrue)
+		})
+
+		Convey("does not match", func() {
+			json := `
+				{
+					"severityFilter": "critical"
+				}`
+
+			settingsJSON, _ := simplejson.NewJson([]byte(json))
+			not := NewNotifierBase("ops", "email", settingsJSON)
+			So(not.MatchSeverity(m.AlertSeverityWarning), ShouldBeFalse)
+		})
+	})
+}

+ 0 - 1
pkg/services/alerting/notifiers/common.go

@@ -1 +0,0 @@
-package notifiers

+ 4 - 7
pkg/services/alerting/notifiers/email.go

@@ -29,12 +29,9 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
 	}
 	}
 
 
 	return &EmailNotifier{
 	return &EmailNotifier{
-		NotifierBase: NotifierBase{
-			Name: model.Name,
-			Type: model.Type,
-		},
-		Addresses: strings.Split(addressesString, "\n"),
-		log:       log.New("alerting.notifier.email"),
+		NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
+		Addresses:    strings.Split(addressesString, "\n"),
+		log:          log.New("alerting.notifier.email"),
 	}, nil
 	}, nil
 }
 }
 
 
@@ -54,7 +51,7 @@ func (this *EmailNotifier) Notify(context *alerting.EvalContext) {
 			"State":         context.Rule.State,
 			"State":         context.Rule.State,
 			"Name":          context.Rule.Name,
 			"Name":          context.Rule.Name,
 			"Severity":      context.Rule.Severity,
 			"Severity":      context.Rule.Severity,
-			"SeverityColor": context.GetColor(),
+			"SeverityColor": context.GetStateModel().Color,
 			"Message":       context.Rule.Message,
 			"Message":       context.Rule.Message,
 			"RuleUrl":       ruleUrl,
 			"RuleUrl":       ruleUrl,
 			"ImageLink":     context.ImagePublicUrl,
 			"ImageLink":     context.ImagePublicUrl,

+ 18 - 8
pkg/services/alerting/notifiers/slack.go

@@ -23,12 +23,9 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
 	}
 	}
 
 
 	return &SlackNotifier{
 	return &SlackNotifier{
-		NotifierBase: NotifierBase{
-			Name: model.Name,
-			Type: model.Type,
-		},
-		Url: url,
-		log: log.New("alerting.notifier.slack"),
+		NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
+		Url:          url,
+		log:          log.New("alerting.notifier.slack"),
 	}, nil
 	}, nil
 }
 }
 
 
@@ -61,13 +58,26 @@ func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
 		}
 		}
 	}
 	}
 
 
+	if context.Error != nil {
+		fields = append(fields, map[string]interface{}{
+			"title": "Error message",
+			"value": context.Error.Error(),
+			"short": false,
+		})
+	}
+
+	message := ""
+	if context.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
+		message = context.Rule.Message
+	}
+
 	body := map[string]interface{}{
 	body := map[string]interface{}{
 		"attachments": []map[string]interface{}{
 		"attachments": []map[string]interface{}{
 			{
 			{
-				"color":       context.GetColor(),
+				"color":       context.GetStateModel().Color,
 				"title":       context.GetNotificationTitle(),
 				"title":       context.GetNotificationTitle(),
 				"title_link":  ruleUrl,
 				"title_link":  ruleUrl,
-				"text":        context.Rule.Message,
+				"text":        message,
 				"fields":      fields,
 				"fields":      fields,
 				"image_url":   context.ImagePublicUrl,
 				"image_url":   context.ImagePublicUrl,
 				"footer":      "Grafana v" + setting.BuildVersion,
 				"footer":      "Grafana v" + setting.BuildVersion,

+ 5 - 8
pkg/services/alerting/notifiers/webhook.go

@@ -20,14 +20,11 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
 	}
 	}
 
 
 	return &WebhookNotifier{
 	return &WebhookNotifier{
-		NotifierBase: NotifierBase{
-			Name: model.Name,
-			Type: model.Type,
-		},
-		Url:      url,
-		User:     model.Settings.Get("user").MustString(),
-		Password: model.Settings.Get("password").MustString(),
-		log:      log.New("alerting.notifier.webhook"),
+		NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
+		Url:          url,
+		User:         model.Settings.Get("user").MustString(),
+		Password:     model.Settings.Get("password").MustString(),
+		log:          log.New("alerting.notifier.webhook"),
 	}, nil
 	}, nil
 }
 }
 
 

+ 26 - 12
pkg/services/alerting/result_handler.go

@@ -4,6 +4,7 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/metrics"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
@@ -30,17 +31,25 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
 	oldState := ctx.Rule.State
 	oldState := ctx.Rule.State
 
 
 	exeuctionError := ""
 	exeuctionError := ""
+	annotationData := simplejson.New()
 	if ctx.Error != nil {
 	if ctx.Error != nil {
 		handler.log.Error("Alert Rule Result Error", "ruleId", ctx.Rule.Id, "error", ctx.Error)
 		handler.log.Error("Alert Rule Result Error", "ruleId", ctx.Rule.Id, "error", ctx.Error)
 		ctx.Rule.State = m.AlertStateExeuctionError
 		ctx.Rule.State = m.AlertStateExeuctionError
 		exeuctionError = ctx.Error.Error()
 		exeuctionError = ctx.Error.Error()
+		annotationData.Set("errorMessage", exeuctionError)
 	} else if ctx.Firing {
 	} else if ctx.Firing {
 		ctx.Rule.State = m.AlertStateType(ctx.Rule.Severity)
 		ctx.Rule.State = m.AlertStateType(ctx.Rule.Severity)
+		annotationData = simplejson.NewFromAny(ctx.EvalMatches)
 	} else {
 	} else {
-		ctx.Rule.State = m.AlertStateOK
+		// handle no data case
+		if ctx.NoDataFound {
+			ctx.Rule.State = ctx.Rule.NoDataState
+		} else {
+			ctx.Rule.State = m.AlertStateOK
+		}
 	}
 	}
 
 
-	countSeverity(ctx.Rule.Severity)
+	countStateResult(ctx.Rule.State)
 	if ctx.Rule.State != oldState {
 	if ctx.Rule.State != oldState {
 		handler.log.Info("New state change", "alertId", ctx.Rule.Id, "newState", ctx.Rule.State, "oldState", oldState)
 		handler.log.Info("New state change", "alertId", ctx.Rule.Id, "newState", ctx.Rule.State, "oldState", oldState)
 
 
@@ -61,10 +70,11 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
 			Type:      annotations.AlertType,
 			Type:      annotations.AlertType,
 			AlertId:   ctx.Rule.Id,
 			AlertId:   ctx.Rule.Id,
 			Title:     ctx.Rule.Name,
 			Title:     ctx.Rule.Name,
-			Text:      ctx.GetStateText(),
+			Text:      ctx.GetStateModel().Text,
 			NewState:  string(ctx.Rule.State),
 			NewState:  string(ctx.Rule.State),
 			PrevState: string(oldState),
 			PrevState: string(oldState),
 			Timestamp: time.Now(),
 			Timestamp: time.Now(),
+			Data:      annotationData,
 		}
 		}
 
 
 		annotationRepo := annotations.GetRepository()
 		annotationRepo := annotations.GetRepository()
@@ -76,15 +86,19 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
 	}
 	}
 }
 }
 
 
-func countSeverity(state m.AlertSeverityType) {
+func countStateResult(state m.AlertStateType) {
 	switch state {
 	switch state {
-	case m.AlertSeverityOK:
-		metrics.M_Alerting_Result_Ok.Inc(1)
-	case m.AlertSeverityInfo:
-		metrics.M_Alerting_Result_Info.Inc(1)
-	case m.AlertSeverityWarning:
-		metrics.M_Alerting_Result_Warning.Inc(1)
-	case m.AlertSeverityCritical:
-		metrics.M_Alerting_Result_Critical.Inc(1)
+	case m.AlertStateCritical:
+		metrics.M_Alerting_Result_State_Critical.Inc(1)
+	case m.AlertStateWarning:
+		metrics.M_Alerting_Result_State_Warning.Inc(1)
+	case m.AlertStateOK:
+		metrics.M_Alerting_Result_State_Ok.Inc(1)
+	case m.AlertStatePaused:
+		metrics.M_Alerting_Result_State_Paused.Inc(1)
+	case m.AlertStateUnknown:
+		metrics.M_Alerting_Result_State_Unknown.Inc(1)
+	case m.AlertStateExeuctionError:
+		metrics.M_Alerting_Result_State_ExecutionError.Inc(1)
 	}
 	}
 }
 }

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

@@ -18,6 +18,7 @@ type Rule struct {
 	Frequency     int64
 	Frequency     int64
 	Name          string
 	Name          string
 	Message       string
 	Message       string
+	NoDataState   m.AlertStateType
 	State         m.AlertStateType
 	State         m.AlertStateType
 	Severity      m.AlertSeverityType
 	Severity      m.AlertSeverityType
 	Conditions    []Condition
 	Conditions    []Condition
@@ -67,6 +68,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
 	model.Frequency = ruleDef.Frequency
 	model.Frequency = ruleDef.Frequency
 	model.Severity = ruleDef.Severity
 	model.Severity = ruleDef.Severity
 	model.State = ruleDef.State
 	model.State = ruleDef.State
+	model.NoDataState = m.AlertStateType(ruleDef.Settings.Get("noDataState").MustString("unknown"))
 
 
 	for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
 	for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
 		jsonModel := simplejson.NewFromAny(v)
 		jsonModel := simplejson.NewFromAny(v)

+ 7 - 2
pkg/services/alerting/rule_test.go

@@ -4,7 +4,7 @@ import (
 	"testing"
 	"testing"
 
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"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"
 	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
@@ -45,6 +45,7 @@ func TestAlertRuleModel(t *testing.T) {
 				"name": "name2",
 				"name": "name2",
 				"description": "desc2",
 				"description": "desc2",
 				"handler": 0,
 				"handler": 0,
+				"noDataMode": "critical",
 				"enabled": true,
 				"enabled": true,
 				"frequency": "60s",
 				"frequency": "60s",
         "conditions": [
         "conditions": [
@@ -63,7 +64,7 @@ func TestAlertRuleModel(t *testing.T) {
 			alertJSON, jsonErr := simplejson.NewJson([]byte(json))
 			alertJSON, jsonErr := simplejson.NewJson([]byte(json))
 			So(jsonErr, ShouldBeNil)
 			So(jsonErr, ShouldBeNil)
 
 
-			alert := &models.Alert{
+			alert := &m.Alert{
 				Id:          1,
 				Id:          1,
 				OrgId:       1,
 				OrgId:       1,
 				DashboardId: 1,
 				DashboardId: 1,
@@ -80,6 +81,10 @@ func TestAlertRuleModel(t *testing.T) {
 			Convey("Can read notifications", func() {
 			Convey("Can read notifications", func() {
 				So(len(alertRule.Notifications), ShouldEqual, 2)
 				So(len(alertRule.Notifications), ShouldEqual, 2)
 			})
 			})
+
+			Convey("Can read noDataMode", func() {
+				So(len(alertRule.NoDataMode), ShouldEqual, m.AlertStateCritical)
+			})
 		})
 		})
 	})
 	})
 }
 }

+ 24 - 5
pkg/services/alerting/scheduler.go

@@ -1,6 +1,7 @@
 package alerting
 package alerting
 
 
 import (
 import (
+	"math"
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
@@ -34,8 +35,8 @@ func (s *SchedulerImpl) Update(rules []*Rule) {
 		}
 		}
 
 
 		job.Rule = rule
 		job.Rule = rule
-		job.Offset = int64(i)
-
+		job.Offset = ((rule.Frequency * 1000) / int64(len(rules))) * int64(i)
+		job.Offset = int64(math.Floor(float64(job.Offset) / 1000))
 		jobs[rule.Id] = job
 		jobs[rule.Id] = job
 	}
 	}
 
 
@@ -46,9 +47,27 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
 	now := tickTime.Unix()
 	now := tickTime.Unix()
 
 
 	for _, job := range s.jobs {
 	for _, job := range s.jobs {
-		if now%job.Rule.Frequency == 0 && job.Running == false {
-			s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name)
-			execQueue <- job
+		if job.Running {
+			continue
+		}
+
+		if job.OffsetWait && now%job.Offset == 0 {
+			job.OffsetWait = false
+			s.enque(job, execQueue)
+			continue
+		}
+
+		if now%job.Rule.Frequency == 0 {
+			if job.Offset > 0 {
+				job.OffsetWait = true
+			} else {
+				s.enque(job, execQueue)
+			}
 		}
 		}
 	}
 	}
 }
 }
+
+func (s *SchedulerImpl) enque(job *Job, execQueue chan *Job) {
+	s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name, "id", job.Rule.Id)
+	execQueue <- job
+}

+ 93 - 0
pkg/services/alerting/test_notification.go

@@ -0,0 +1,93 @@
+package alerting
+
+import (
+	"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 NotificationTestCommand struct {
+	Severity string
+	Name     string
+	Type     string
+	Settings *simplejson.Json
+}
+
+func init() {
+	bus.AddHandler("alerting", handleNotificationTestCommand)
+
+}
+
+func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
+	notifier := NewRootNotifier()
+
+	model := &models.AlertNotification{
+		Name:     cmd.Name,
+		Type:     cmd.Type,
+		Settings: cmd.Settings,
+	}
+
+	notifiers, err := notifier.createNotifierFor(model)
+
+	if err != nil {
+		log.Error2("Failed to create notifier", "error", err.Error())
+		return err
+	}
+
+	severity := models.AlertSeverityType(cmd.Severity)
+	notifier.sendNotifications([]Notifier{notifiers}, createTestEvalContext(severity))
+
+	return nil
+}
+
+func createTestEvalContext(severity models.AlertSeverityType) *EvalContext {
+	state := models.AlertStateOK
+	firing := false
+	if severity == models.AlertSeverityCritical {
+		state = models.AlertStateCritical
+		firing = true
+	}
+	if severity == models.AlertSeverityWarning {
+		state = models.AlertStateWarning
+		firing = true
+	}
+
+	testRule := &Rule{
+		DashboardId: 1,
+		PanelId:     1,
+		Name:        "Test notification",
+		Message:     "Someone is testing the alert notification within grafana.",
+		State:       state,
+		Severity:    severity,
+	}
+
+	ctx := NewEvalContext(testRule)
+	ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
+
+	ctx.IsTestRun = true
+	ctx.Firing = firing
+	ctx.Error = nil
+	ctx.EvalMatches = evalMatchesBasedOnSeverity(severity)
+
+	return ctx
+}
+
+func evalMatchesBasedOnSeverity(severity models.AlertSeverityType) []*EvalMatch {
+	matches := make([]*EvalMatch, 0)
+	if severity == models.AlertSeverityOK {
+		return matches
+	}
+
+	matches = append(matches, &EvalMatch{
+		Metric: "High value",
+		Value:  100,
+	})
+
+	matches = append(matches, &EvalMatch{
+		Metric: "Higher Value",
+		Value:  200,
+	})
+
+	return matches
+}

+ 9 - 0
pkg/services/annotations/annotations.go

@@ -8,6 +8,15 @@ import (
 
 
 type Repository interface {
 type Repository interface {
 	Save(item *Item) error
 	Save(item *Item) error
+	Find(query *ItemQuery) ([]*Item, error)
+}
+
+type ItemQuery struct {
+	OrgId   int64    `json:"orgId"`
+	Type    ItemType `json:"type"`
+	AlertId int64    `json:"alertId"`
+
+	Limit int64 `json:"alertId"`
 }
 }
 
 
 var repositoryInstance Repository
 var repositoryInstance Repository

+ 1 - 1
pkg/services/sqlstore/alert.go

@@ -159,7 +159,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xor
 		} else {
 		} else {
 			alert.Updated = time.Now()
 			alert.Updated = time.Now()
 			alert.Created = time.Now()
 			alert.Created = time.Now()
-			alert.State = m.AlertStatePending
+			alert.State = m.AlertStateUnknown
 			alert.NewStateDate = time.Now()
 			alert.NewStateDate = time.Now()
 
 
 			_, err := sess.Insert(alert)
 			_, err := sess.Insert(alert)

+ 86 - 32
pkg/services/sqlstore/alert_notification.go

@@ -16,6 +16,8 @@ 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)
 }
 }
 
 
 func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
 func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
@@ -32,41 +34,84 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
 }
 }
 
 
 func GetAlertNotifications(query *m.GetAlertNotificationsQuery) error {
 func GetAlertNotifications(query *m.GetAlertNotificationsQuery) error {
-	return getAlertNotificationsInternal(query, x.NewSession())
+	return getAlertNotificationInternal(query, x.NewSession())
 }
 }
 
 
-func getAlertNotificationsInternal(query *m.GetAlertNotificationsQuery, sess *xorm.Session) error {
+func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error {
+	results := make([]*m.AlertNotification, 0)
+	if err := x.Where("org_id = ?", query.OrgId).Find(&results); err != nil {
+		return err
+	}
+
+	query.Result = results
+	return nil
+}
+
+func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) 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.org_id,
-	   					  alert_notification.name,
-	              alert_notification.type,
-	   					  alert_notification.created,
-	              alert_notification.updated,
-	              alert_notification.settings
-	   					  FROM alert_notification
-	   					  `)
+										alert_notification.id,
+										alert_notification.org_id,
+										alert_notification.name,
+										alert_notification.type,
+										alert_notification.created,
+										alert_notification.updated,
+										alert_notification.settings,
+										alert_notification.is_default
+										FROM alert_notification
+	  							`)
 
 
 	sql.WriteString(` WHERE alert_notification.org_id = ?`)
 	sql.WriteString(` WHERE alert_notification.org_id = ?`)
 	params = append(params, query.OrgId)
 	params = append(params, query.OrgId)
 
 
-	if query.Name != "" {
-		sql.WriteString(` AND alert_notification.name = ?`)
-		params = append(params, query.Name)
+	sql.WriteString(` AND ((alert_notification.is_default = 1)`)
+	if len(query.Ids) > 0 {
+		sql.WriteString(` OR alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")")
+		for _, v := range query.Ids {
+			params = append(params, v)
+		}
 	}
 	}
+	sql.WriteString(`)`)
 
 
-	if query.Id != 0 {
-		sql.WriteString(` AND alert_notification.id = ?`)
-		params = append(params, query.Id)
+	results := make([]*m.AlertNotification, 0)
+	if err := x.Sql(sql.String(), params...).Find(&results); err != nil {
+		return err
 	}
 	}
 
 
-	if len(query.Ids) > 0 {
-		sql.WriteString(` AND alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")")
-		for _, v := range query.Ids {
-			params = append(params, v)
+	query.Result = results
+	return nil
+}
+
+func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *xorm.Session) error {
+	var sql bytes.Buffer
+	params := make([]interface{}, 0)
+
+	sql.WriteString(`SELECT
+										alert_notification.id,
+										alert_notification.org_id,
+										alert_notification.name,
+										alert_notification.type,
+										alert_notification.created,
+										alert_notification.updated,
+										alert_notification.settings,
+										alert_notification.is_default
+										FROM alert_notification
+	  							`)
+
+	sql.WriteString(` WHERE alert_notification.org_id = ?`)
+	params = append(params, query.OrgId)
+
+	if query.Name != "" || query.Id != 0 {
+		if query.Name != "" {
+			sql.WriteString(` AND alert_notification.name = ?`)
+			params = append(params, query.Name)
+		}
+
+		if query.Id != 0 {
+			sql.WriteString(` AND alert_notification.id = ?`)
+			params = append(params, query.Id)
 		}
 		}
 	}
 	}
 
 
@@ -75,30 +120,36 @@ func getAlertNotificationsInternal(query *m.GetAlertNotificationsQuery, sess *xo
 		return err
 		return err
 	}
 	}
 
 
-	query.Result = results
+	if len(results) == 0 {
+		query.Result = nil
+	} else {
+		query.Result = results[0]
+	}
+
 	return nil
 	return nil
 }
 }
 
 
 func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error {
 func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error {
 	return inTransaction(func(sess *xorm.Session) error {
 	return inTransaction(func(sess *xorm.Session) error {
 		existingQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
 		existingQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
-		err := getAlertNotificationsInternal(existingQuery, sess)
+		err := getAlertNotificationInternal(existingQuery, sess)
 
 
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		if len(existingQuery.Result) > 0 {
+		if existingQuery.Result != nil {
 			return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
 			return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
 		}
 		}
 
 
 		alertNotification := &m.AlertNotification{
 		alertNotification := &m.AlertNotification{
-			OrgId:    cmd.OrgId,
-			Name:     cmd.Name,
-			Type:     cmd.Type,
-			Settings: cmd.Settings,
-			Created:  time.Now(),
-			Updated:  time.Now(),
+			OrgId:     cmd.OrgId,
+			Name:      cmd.Name,
+			Type:      cmd.Type,
+			Settings:  cmd.Settings,
+			Created:   time.Now(),
+			Updated:   time.Now(),
+			IsDefault: cmd.IsDefault,
 		}
 		}
 
 
 		if _, err = sess.Insert(alertNotification); err != nil {
 		if _, err = sess.Insert(alertNotification); err != nil {
@@ -120,11 +171,11 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
 
 
 		// check if name exists
 		// check if name exists
 		sameNameQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
 		sameNameQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
-		if err := getAlertNotificationsInternal(sameNameQuery, sess); err != nil {
+		if err := getAlertNotificationInternal(sameNameQuery, sess); err != nil {
 			return err
 			return err
 		}
 		}
 
 
-		if len(sameNameQuery.Result) > 0 && sameNameQuery.Result[0].Id != current.Id {
+		if sameNameQuery.Result != nil && sameNameQuery.Result.Id != current.Id {
 			return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
 			return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
 		}
 		}
 
 
@@ -132,6 +183,9 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
 		current.Settings = cmd.Settings
 		current.Settings = cmd.Settings
 		current.Name = cmd.Name
 		current.Name = cmd.Name
 		current.Type = cmd.Type
 		current.Type = cmd.Type
+		current.IsDefault = cmd.IsDefault
+
+		sess.UseBool("is_default")
 
 
 		if affected, err := sess.Id(cmd.Id).Update(current); err != nil {
 		if affected, err := sess.Id(cmd.Id).Update(current); err != nil {
 			return err
 			return err

+ 19 - 4
pkg/services/sqlstore/alert_notification_test.go

@@ -23,7 +23,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
 			err := GetAlertNotifications(cmd)
 			err := GetAlertNotifications(cmd)
 			fmt.Printf("errror %v", err)
 			fmt.Printf("errror %v", err)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
-			So(len(cmd.Result), ShouldEqual, 0)
+			So(cmd.Result, ShouldBeNil)
 		})
 		})
 
 
 		Convey("Can save Alert Notification", func() {
 		Convey("Can save Alert Notification", func() {
@@ -63,20 +63,35 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
 			cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
 			cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
 			cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
 			cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
 			cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, Settings: simplejson.New()}
 			cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, Settings: simplejson.New()}
+			cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, Settings: simplejson.New()}
+
+			otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, Settings: simplejson.New()}
 
 
 			So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil)
 			So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil)
 			So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil)
 			So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil)
 			So(CreateAlertNotificationCommand(&cmd3), ShouldBeNil)
 			So(CreateAlertNotificationCommand(&cmd3), ShouldBeNil)
+			So(CreateAlertNotificationCommand(&cmd4), ShouldBeNil)
+			So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil)
 
 
 			Convey("search", func() {
 			Convey("search", func() {
-				query := &m.GetAlertNotificationsQuery{
+				query := &m.GetAlertNotificationsToSendQuery{
 					Ids:   []int64{cmd1.Result.Id, cmd2.Result.Id, 112341231},
 					Ids:   []int64{cmd1.Result.Id, cmd2.Result.Id, 112341231},
 					OrgId: 1,
 					OrgId: 1,
 				}
 				}
 
 
-				err := GetAlertNotifications(query)
+				err := GetAlertNotificationsToSend(query)
+				So(err, ShouldBeNil)
+				So(len(query.Result), ShouldEqual, 3)
+			})
+
+			Convey("all", func() {
+				query := &m.GetAllAlertNotificationsQuery{
+					OrgId: 1,
+				}
+
+				err := GetAllAlertNotifications(query)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
-				So(len(query.Result), ShouldEqual, 2)
+				So(len(query.Result), ShouldEqual, 4)
 			})
 			})
 		})
 		})
 	})
 	})

+ 37 - 0
pkg/services/sqlstore/annotation.go

@@ -1,6 +1,9 @@
 package sqlstore
 package sqlstore
 
 
 import (
 import (
+	"bytes"
+	"fmt"
+
 	"github.com/go-xorm/xorm"
 	"github.com/go-xorm/xorm"
 	"github.com/grafana/grafana/pkg/services/annotations"
 	"github.com/grafana/grafana/pkg/services/annotations"
 )
 )
@@ -17,5 +20,39 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
 
 
 		return nil
 		return nil
 	})
 	})
+}
+
+func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.Item, error) {
+	var sql bytes.Buffer
+	params := make([]interface{}, 0)
+
+	sql.WriteString(`SELECT *
+						from annotation
+						`)
+
+	sql.WriteString(`WHERE org_id = ?`)
+	params = append(params, query.OrgId)
+
+	if query.AlertId != 0 {
+		sql.WriteString(` AND alert_id = ?`)
+		params = append(params, query.AlertId)
+	}
+
+	if query.Type != "" {
+		sql.WriteString(` AND type = ?`)
+		params = append(params, string(query.Type))
+	}
+
+	if query.Limit == 0 {
+		query.Limit = 10
+	}
+
+	sql.WriteString(fmt.Sprintf("ORDER BY timestamp DESC LIMIT %v", query.Limit))
+
+	items := make([]*annotations.Item, 0)
+	if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
+		return nil, err
+	}
 
 
+	return items, nil
 }
 }

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

@@ -62,5 +62,9 @@ func addAlertMigrations(mg *Migrator) {
 	}
 	}
 
 
 	mg.AddMigration("create alert_notification table v1", NewAddTableMigration(alert_notification))
 	mg.AddMigration("create alert_notification table v1", NewAddTableMigration(alert_notification))
+	mg.AddMigration("Add column is_default", NewAddColumnMigration(alert_notification, &Column{
+		Name: "is_default", Type: DB_Bool, Nullable: false, Default: "0",
+	}))
 	mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
 	mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
+
 }
 }

+ 1 - 1
pkg/setting/setting.go

@@ -190,7 +190,7 @@ func ToAbsUrl(relativeUrl string) string {
 
 
 func shouldRedactKey(s string) bool {
 func shouldRedactKey(s string) bool {
 	uppercased := strings.ToUpper(s)
 	uppercased := strings.ToUpper(s)
-	return strings.Contains(uppercased, "PASSWORD") || strings.Contains(uppercased, "SECRET")
+	return strings.Contains(uppercased, "PASSWORD") || strings.Contains(uppercased, "SECRET") || strings.Contains(uppercased, "PROVIDER_CONFIG")
 }
 }
 
 
 func shouldRedactURLKey(s string) bool {
 func shouldRedactURLKey(s string) bool {

+ 3 - 2
pkg/setting/setting_oauth.go

@@ -11,8 +11,9 @@ type OAuthInfo struct {
 }
 }
 
 
 type OAuther struct {
 type OAuther struct {
-	GitHub, Google, Twitter bool
-	OAuthInfos              map[string]*OAuthInfo
+	GitHub, Google, Twitter, Generic bool
+	OAuthInfos                       map[string]*OAuthInfo
+	OAuthProviderName                string
 }
 }
 
 
 var OAuthService *OAuther
 var OAuthService *OAuther

+ 20 - 0
pkg/social/common.go

@@ -0,0 +1,20 @@
+package social
+
+import (
+	"fmt"
+	"strings"
+)
+
+func isEmailAllowed(email string, allowedDomains []string) bool {
+	if len(allowedDomains) == 0 {
+		return true
+	}
+
+	valid := false
+	for _, domain := range allowedDomains {
+		emailSuffix := fmt.Sprintf("@%s", domain)
+		valid = valid || strings.HasSuffix(email, emailSuffix)
+	}
+
+	return valid
+}

+ 205 - 0
pkg/social/generic_oauth.go

@@ -0,0 +1,205 @@
+package social
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/grafana/grafana/pkg/models"
+
+	"golang.org/x/oauth2"
+)
+
+type GenericOAuth struct {
+	*oauth2.Config
+	allowedDomains       []string
+	allowedOrganizations []string
+	apiUrl               string
+	allowSignup          bool
+	teamIds              []int
+}
+
+func (s *GenericOAuth) Type() int {
+	return int(models.GENERIC)
+}
+
+func (s *GenericOAuth) IsEmailAllowed(email string) bool {
+	return isEmailAllowed(email, s.allowedDomains)
+}
+
+func (s *GenericOAuth) IsSignupAllowed() bool {
+	return s.allowSignup
+}
+
+func (s *GenericOAuth) IsTeamMember(client *http.Client) bool {
+	if len(s.teamIds) == 0 {
+		return true
+	}
+
+	teamMemberships, err := s.FetchTeamMemberships(client)
+	if err != nil {
+		return false
+	}
+
+	for _, teamId := range s.teamIds {
+		for _, membershipId := range teamMemberships {
+			if teamId == membershipId {
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
+func (s *GenericOAuth) IsOrganizationMember(client *http.Client) bool {
+	if len(s.allowedOrganizations) == 0 {
+		return true
+	}
+
+	organizations, err := s.FetchOrganizations(client)
+	if err != nil {
+		return false
+	}
+
+	for _, allowedOrganization := range s.allowedOrganizations {
+		for _, organization := range organizations {
+			if organization == allowedOrganization {
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
+func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
+	type Record struct {
+		Email    string `json:"email"`
+		Primary  bool   `json:"primary"`
+		Verified bool   `json:"verified"`
+	}
+
+	emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
+	r, err := client.Get(emailsUrl)
+	if err != nil {
+		return "", err
+	}
+
+	defer r.Body.Close()
+
+	var records []Record
+
+	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+		return "", err
+	}
+
+	var email = ""
+	for _, record := range records {
+		if record.Primary {
+			email = record.Email
+		}
+	}
+
+	return email, nil
+}
+
+func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error) {
+	type Record struct {
+		Id int `json:"id"`
+	}
+
+	membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
+	r, err := client.Get(membershipUrl)
+	if err != nil {
+		return nil, err
+	}
+
+	defer r.Body.Close()
+
+	var records []Record
+
+	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+		return nil, err
+	}
+
+	var ids = make([]int, len(records))
+	for i, record := range records {
+		ids[i] = record.Id
+	}
+
+	return ids, nil
+}
+
+func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error) {
+	type Record struct {
+		Login string `json:"login"`
+	}
+
+	url := fmt.Sprintf(s.apiUrl + "/orgs")
+	r, err := client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+
+	defer r.Body.Close()
+
+	var records []Record
+
+	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+		return nil, err
+	}
+
+	var logins = make([]string, len(records))
+	for i, record := range records {
+		logins[i] = record.Login
+	}
+
+	return logins, nil
+}
+
+func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
+	var data struct {
+		Id    int    `json:"id"`
+		Name  string `json:"login"`
+		Email string `json:"email"`
+	}
+
+	var err error
+	client := s.Client(oauth2.NoContext, token)
+	r, err := client.Get(s.apiUrl)
+	if err != nil {
+		return nil, err
+	}
+
+	defer r.Body.Close()
+
+	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+		return nil, err
+	}
+
+	userInfo := &BasicUserInfo{
+		Identity: strconv.Itoa(data.Id),
+		Name:     data.Name,
+		Email:    data.Email,
+	}
+
+	if !s.IsTeamMember(client) {
+		return nil, errors.New("User not a member of one of the required teams")
+	}
+
+	if !s.IsOrganizationMember(client) {
+		return nil, errors.New("User not a member of one of the required organizations")
+	}
+
+	if userInfo.Email == "" {
+		userInfo.Email, err = s.FetchPrivateEmail(client)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return userInfo, nil
+}

+ 213 - 0
pkg/social/github_oauth.go

@@ -0,0 +1,213 @@
+package social
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/grafana/grafana/pkg/models"
+
+	"golang.org/x/oauth2"
+)
+
+type SocialGithub struct {
+	*oauth2.Config
+	allowedDomains       []string
+	allowedOrganizations []string
+	apiUrl               string
+	allowSignup          bool
+	teamIds              []int
+}
+
+var (
+	ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
+)
+
+var (
+	ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations")
+)
+
+func (s *SocialGithub) Type() int {
+	return int(models.GITHUB)
+}
+
+func (s *SocialGithub) IsEmailAllowed(email string) bool {
+	return isEmailAllowed(email, s.allowedDomains)
+}
+
+func (s *SocialGithub) IsSignupAllowed() bool {
+	return s.allowSignup
+}
+
+func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
+	if len(s.teamIds) == 0 {
+		return true
+	}
+
+	teamMemberships, err := s.FetchTeamMemberships(client)
+	if err != nil {
+		return false
+	}
+
+	for _, teamId := range s.teamIds {
+		for _, membershipId := range teamMemberships {
+			if teamId == membershipId {
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
+func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool {
+	if len(s.allowedOrganizations) == 0 {
+		return true
+	}
+
+	organizations, err := s.FetchOrganizations(client)
+	if err != nil {
+		return false
+	}
+
+	for _, allowedOrganization := range s.allowedOrganizations {
+		for _, organization := range organizations {
+			if organization == allowedOrganization {
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
+func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) {
+	type Record struct {
+		Email    string `json:"email"`
+		Primary  bool   `json:"primary"`
+		Verified bool   `json:"verified"`
+	}
+
+	emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
+	r, err := client.Get(emailsUrl)
+	if err != nil {
+		return "", err
+	}
+
+	defer r.Body.Close()
+
+	var records []Record
+
+	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+		return "", err
+	}
+
+	var email = ""
+	for _, record := range records {
+		if record.Primary {
+			email = record.Email
+		}
+	}
+
+	return email, nil
+}
+
+func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) {
+	type Record struct {
+		Id int `json:"id"`
+	}
+
+	membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
+	r, err := client.Get(membershipUrl)
+	if err != nil {
+		return nil, err
+	}
+
+	defer r.Body.Close()
+
+	var records []Record
+
+	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+		return nil, err
+	}
+
+	var ids = make([]int, len(records))
+	for i, record := range records {
+		ids[i] = record.Id
+	}
+
+	return ids, nil
+}
+
+func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) {
+	type Record struct {
+		Login string `json:"login"`
+	}
+
+	url := fmt.Sprintf(s.apiUrl + "/orgs")
+	r, err := client.Get(url)
+	if err != nil {
+		return nil, err
+	}
+
+	defer r.Body.Close()
+
+	var records []Record
+
+	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+		return nil, err
+	}
+
+	var logins = make([]string, len(records))
+	for i, record := range records {
+		logins[i] = record.Login
+	}
+
+	return logins, nil
+}
+
+func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
+	var data struct {
+		Id    int    `json:"id"`
+		Name  string `json:"login"`
+		Email string `json:"email"`
+	}
+
+	var err error
+	client := s.Client(oauth2.NoContext, token)
+	r, err := client.Get(s.apiUrl)
+	if err != nil {
+		return nil, err
+	}
+
+	defer r.Body.Close()
+
+	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+		return nil, err
+	}
+
+	userInfo := &BasicUserInfo{
+		Identity: strconv.Itoa(data.Id),
+		Name:     data.Name,
+		Email:    data.Email,
+	}
+
+	if !s.IsTeamMember(client) {
+		return nil, ErrMissingTeamMembership
+	}
+
+	if !s.IsOrganizationMember(client) {
+		return nil, ErrMissingOrganizationMembership
+	}
+
+	if userInfo.Email == "" {
+		userInfo.Email, err = s.FetchPrivateEmail(client)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return userInfo, nil
+}

+ 52 - 0
pkg/social/google_oauth.go

@@ -0,0 +1,52 @@
+package social
+
+import (
+	"encoding/json"
+
+	"github.com/grafana/grafana/pkg/models"
+
+	"golang.org/x/oauth2"
+)
+
+type SocialGoogle struct {
+	*oauth2.Config
+	allowedDomains []string
+	apiUrl         string
+	allowSignup    bool
+}
+
+func (s *SocialGoogle) Type() int {
+	return int(models.GOOGLE)
+}
+
+func (s *SocialGoogle) IsEmailAllowed(email string) bool {
+	return isEmailAllowed(email, s.allowedDomains)
+}
+
+func (s *SocialGoogle) IsSignupAllowed() bool {
+	return s.allowSignup
+}
+
+func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
+	var data struct {
+		Id    string `json:"id"`
+		Name  string `json:"name"`
+		Email string `json:"email"`
+	}
+	var err error
+
+	client := s.Client(oauth2.NoContext, token)
+	r, err := client.Get(s.apiUrl)
+	if err != nil {
+		return nil, err
+	}
+	defer r.Body.Close()
+	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+		return nil, err
+	}
+	return &BasicUserInfo{
+		Identity: data.Id,
+		Name:     data.Name,
+		Email:    data.Email,
+	}, nil
+}

+ 14 - 268
pkg/social/social.go

@@ -1,14 +1,8 @@
 package social
 package social
 
 
 import (
 import (
-	"encoding/json"
-	"errors"
-	"fmt"
-	"net/http"
-	"strconv"
 	"strings"
 	"strings"
 
 
-	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	"golang.org/x/net/context"
 	"golang.org/x/net/context"
 
 
@@ -42,7 +36,7 @@ func NewOAuthService() {
 	setting.OAuthService = &setting.OAuther{}
 	setting.OAuthService = &setting.OAuther{}
 	setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
 	setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
 
 
-	allOauthes := []string{"github", "google"}
+	allOauthes := []string{"github", "google", "generic_oauth"}
 
 
 	for _, name := range allOauthes {
 	for _, name := range allOauthes {
 		sec := setting.Cfg.Section("auth." + name)
 		sec := setting.Cfg.Section("auth." + name)
@@ -98,269 +92,21 @@ func NewOAuthService() {
 				allowSignup: info.AllowSignup,
 				allowSignup: info.AllowSignup,
 			}
 			}
 		}
 		}
-	}
-}
-
-func isEmailAllowed(email string, allowedDomains []string) bool {
-	if len(allowedDomains) == 0 {
-		return true
-	}
-
-	valid := false
-	for _, domain := range allowedDomains {
-		emailSuffix := fmt.Sprintf("@%s", domain)
-		valid = valid || strings.HasSuffix(email, emailSuffix)
-	}
-
-	return valid
-}
-
-type SocialGithub struct {
-	*oauth2.Config
-	allowedDomains       []string
-	allowedOrganizations []string
-	apiUrl               string
-	allowSignup          bool
-	teamIds              []int
-}
-
-var (
-	ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
-)
-
-var (
-	ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations")
-)
-
-func (s *SocialGithub) Type() int {
-	return int(models.GITHUB)
-}
-
-func (s *SocialGithub) IsEmailAllowed(email string) bool {
-	return isEmailAllowed(email, s.allowedDomains)
-}
-
-func (s *SocialGithub) IsSignupAllowed() bool {
-	return s.allowSignup
-}
-
-func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
-	if len(s.teamIds) == 0 {
-		return true
-	}
-
-	teamMemberships, err := s.FetchTeamMemberships(client)
-	if err != nil {
-		return false
-	}
-
-	for _, teamId := range s.teamIds {
-		for _, membershipId := range teamMemberships {
-			if teamId == membershipId {
-				return true
-			}
-		}
-	}
-
-	return false
-}
-
-func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool {
-	if len(s.allowedOrganizations) == 0 {
-		return true
-	}
-
-	organizations, err := s.FetchOrganizations(client)
-	if err != nil {
-		return false
-	}
 
 
-	for _, allowedOrganization := range s.allowedOrganizations {
-		for _, organization := range organizations {
-			if organization == allowedOrganization {
-				return true
+		// Generic - Uses the same scheme as Github.
+		if name == "generic_oauth" {
+			setting.OAuthService.Generic = true
+			setting.OAuthService.OAuthProviderName = sec.Key("oauth_provider_name").String()
+			teamIds := sec.Key("team_ids").Ints(",")
+			allowedOrganizations := sec.Key("allowed_organizations").Strings(" ")
+			SocialMap["generic_oauth"] = &GenericOAuth{
+				Config:               &config,
+				allowedDomains:       info.AllowedDomains,
+				apiUrl:               info.ApiUrl,
+				allowSignup:          info.AllowSignup,
+				teamIds:              teamIds,
+				allowedOrganizations: allowedOrganizations,
 			}
 			}
 		}
 		}
 	}
 	}
-
-	return false
-}
-
-func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) {
-	type Record struct {
-		Email    string `json:"email"`
-		Primary  bool   `json:"primary"`
-		Verified bool   `json:"verified"`
-	}
-
-	emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
-	r, err := client.Get(emailsUrl)
-	if err != nil {
-		return "", err
-	}
-
-	defer r.Body.Close()
-
-	var records []Record
-
-	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
-		return "", err
-	}
-
-	var email = ""
-	for _, record := range records {
-		if record.Primary {
-			email = record.Email
-		}
-	}
-
-	return email, nil
-}
-
-func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) {
-	type Record struct {
-		Id int `json:"id"`
-	}
-
-	membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
-	r, err := client.Get(membershipUrl)
-	if err != nil {
-		return nil, err
-	}
-
-	defer r.Body.Close()
-
-	var records []Record
-
-	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
-		return nil, err
-	}
-
-	var ids = make([]int, len(records))
-	for i, record := range records {
-		ids[i] = record.Id
-	}
-
-	return ids, nil
-}
-
-func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) {
-	type Record struct {
-		Login string `json:"login"`
-	}
-
-	url := fmt.Sprintf(s.apiUrl + "/orgs")
-	r, err := client.Get(url)
-	if err != nil {
-		return nil, err
-	}
-
-	defer r.Body.Close()
-
-	var records []Record
-
-	if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
-		return nil, err
-	}
-
-	var logins = make([]string, len(records))
-	for i, record := range records {
-		logins[i] = record.Login
-	}
-
-	return logins, nil
-}
-
-func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
-	var data struct {
-		Id    int    `json:"id"`
-		Name  string `json:"login"`
-		Email string `json:"email"`
-	}
-
-	var err error
-	client := s.Client(oauth2.NoContext, token)
-	r, err := client.Get(s.apiUrl)
-	if err != nil {
-		return nil, err
-	}
-
-	defer r.Body.Close()
-
-	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
-		return nil, err
-	}
-
-	userInfo := &BasicUserInfo{
-		Identity: strconv.Itoa(data.Id),
-		Name:     data.Name,
-		Email:    data.Email,
-	}
-
-	if !s.IsTeamMember(client) {
-		return nil, ErrMissingTeamMembership
-	}
-
-	if !s.IsOrganizationMember(client) {
-		return nil, ErrMissingOrganizationMembership
-	}
-
-	if userInfo.Email == "" {
-		userInfo.Email, err = s.FetchPrivateEmail(client)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	return userInfo, nil
-}
-
-//   ________                     .__
-//  /  _____/  ____   ____   ____ |  |   ____
-// /   \  ___ /  _ \ /  _ \ / ___\|  | _/ __ \
-// \    \_\  (  <_> |  <_> ) /_/  >  |_\  ___/
-//  \______  /\____/ \____/\___  /|____/\___  >
-//         \/             /_____/           \/
-
-type SocialGoogle struct {
-	*oauth2.Config
-	allowedDomains []string
-	apiUrl         string
-	allowSignup    bool
-}
-
-func (s *SocialGoogle) Type() int {
-	return int(models.GOOGLE)
-}
-
-func (s *SocialGoogle) IsEmailAllowed(email string) bool {
-	return isEmailAllowed(email, s.allowedDomains)
-}
-
-func (s *SocialGoogle) IsSignupAllowed() bool {
-	return s.allowSignup
-}
-
-func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
-	var data struct {
-		Id    string `json:"id"`
-		Name  string `json:"name"`
-		Email string `json:"email"`
-	}
-	var err error
-
-	client := s.Client(oauth2.NoContext, token)
-	r, err := client.Get(s.apiUrl)
-	if err != nil {
-		return nil, err
-	}
-	defer r.Body.Close()
-	if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
-		return nil, err
-	}
-	return &BasicUserInfo{
-		Identity: data.Id,
-		Name:     data.Name,
-		Email:    data.Email,
-	}, nil
 }
 }

+ 60 - 11
pkg/tsdb/graphite/graphite.go

@@ -2,16 +2,23 @@ package graphite
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
+	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
+	"path"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
 )
 )
 
 
+var (
+	HttpClient = http.Client{Timeout: time.Duration(10 * time.Second)}
+)
+
 type GraphiteExecutor struct {
 type GraphiteExecutor struct {
 	*tsdb.DataSourceInfo
 	*tsdb.DataSourceInfo
 }
 }
@@ -30,7 +37,7 @@ func init() {
 func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
 func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
 	result := &tsdb.BatchResult{}
 	result := &tsdb.BatchResult{}
 
 
-	params := url.Values{
+	formData := url.Values{
 		"from":          []string{"-" + formatTimeRange(context.TimeRange.From)},
 		"from":          []string{"-" + formatTimeRange(context.TimeRange.From)},
 		"until":         []string{formatTimeRange(context.TimeRange.To)},
 		"until":         []string{formatTimeRange(context.TimeRange.To)},
 		"format":        []string{"json"},
 		"format":        []string{"json"},
@@ -38,28 +45,26 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
 	}
 	}
 
 
 	for _, query := range queries {
 	for _, query := range queries {
-		params["target"] = []string{query.Query}
-		glog.Debug("Graphite request", "query", query.Query)
+		formData["target"] = []string{query.Query}
+	}
+
+	if setting.Env == setting.DEV {
+		glog.Debug("Graphite request", "params", formData)
 	}
 	}
 
 
-	client := http.Client{Timeout: time.Duration(10 * time.Second)}
-	res, err := client.PostForm(e.Url+"/render?", params)
+	req, err := e.createRequest(formData)
 	if err != nil {
 	if err != nil {
 		result.Error = err
 		result.Error = err
 		return result
 		return result
 	}
 	}
-	defer res.Body.Close()
-
-	body, err := ioutil.ReadAll(res.Body)
+	res, err := HttpClient.Do(req)
 	if err != nil {
 	if err != nil {
 		result.Error = err
 		result.Error = err
 		return result
 		return result
 	}
 	}
 
 
-	var data []TargetResponseDTO
-	err = json.Unmarshal(body, &data)
+	data, err := e.parseResponse(res)
 	if err != nil {
 	if err != nil {
-		glog.Info("Failed to unmarshal graphite response", "error", err, "body", string(body))
 		result.Error = err
 		result.Error = err
 		return result
 		return result
 	}
 	}
@@ -71,12 +76,56 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
 			Name:   series.Target,
 			Name:   series.Target,
 			Points: series.DataPoints,
 			Points: series.DataPoints,
 		})
 		})
+
+		if setting.Env == setting.DEV {
+			glog.Debug("Graphite response", "target", series.Target, "datapoints", len(series.DataPoints))
+		}
 	}
 	}
 
 
 	result.QueryResults["A"] = queryRes
 	result.QueryResults["A"] = queryRes
 	return result
 	return result
 }
 }
 
 
+func (e *GraphiteExecutor) parseResponse(res *http.Response) ([]TargetResponseDTO, error) {
+	body, err := ioutil.ReadAll(res.Body)
+	defer res.Body.Close()
+	if err != nil {
+		return nil, err
+	}
+
+	if res.StatusCode == http.StatusUnauthorized {
+		glog.Info("Request is Unauthorized", "status", res.Status, "body", string(body))
+		return nil, fmt.Errorf("Request is Unauthorized status: %v body: %s", res.Status, string(body))
+	}
+
+	var data []TargetResponseDTO
+	err = json.Unmarshal(body, &data)
+	if err != nil {
+		glog.Info("Failed to unmarshal graphite response", "error", err, "status", res.Status, "body", string(body))
+		return nil, err
+	}
+
+	return data, nil
+}
+
+func (e *GraphiteExecutor) createRequest(data url.Values) (*http.Request, error) {
+	u, _ := url.Parse(e.Url)
+	u.Path = path.Join(u.Path, "render")
+
+	req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(data.Encode()))
+	if err != nil {
+		glog.Info("Failed to create request", "error", err)
+		return nil, fmt.Errorf("Failed to create request. error: %v", err)
+	}
+
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	if e.BasicAuth {
+		req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword)
+	}
+
+	return req, err
+}
+
 func formatTimeRange(input string) string {
 func formatTimeRange(input string) string {
 	if input == "now" {
 	if input == "now" {
 		return input
 		return input

+ 2 - 2
pkg/tsdb/graphite/types.go

@@ -1,6 +1,6 @@
 package graphite
 package graphite
 
 
 type TargetResponseDTO struct {
 type TargetResponseDTO struct {
-	Target     string       `json:"target"`
-	DataPoints [][2]float64 `json:"datapoints"`
+	Target     string        `json:"target"`
+	DataPoints [][2]*float64 `json:"datapoints"`
 }
 }

+ 3 - 3
pkg/tsdb/models.go

@@ -46,13 +46,13 @@ type QueryResult struct {
 }
 }
 
 
 type TimeSeries struct {
 type TimeSeries struct {
-	Name   string       `json:"name"`
-	Points [][2]float64 `json:"points"`
+	Name   string        `json:"name"`
+	Points [][2]*float64 `json:"points"`
 }
 }
 
 
 type TimeSeriesSlice []*TimeSeries
 type TimeSeriesSlice []*TimeSeries
 
 
-func NewTimeSeries(name string, points [][2]float64) *TimeSeries {
+func NewTimeSeries(name string, points [][2]*float64) *TimeSeries {
 	return &TimeSeries{
 	return &TimeSeries{
 		Name:   name,
 		Name:   name,
 		Points: points,
 		Points: points,

+ 3 - 1
public/app/core/controllers/login_ctrl.js

@@ -17,8 +17,10 @@ function (angular, coreModule, config) {
 
 
     $scope.googleAuthEnabled = config.googleAuthEnabled;
     $scope.googleAuthEnabled = config.googleAuthEnabled;
     $scope.githubAuthEnabled = config.githubAuthEnabled;
     $scope.githubAuthEnabled = config.githubAuthEnabled;
-    $scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled;
+    $scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled || config.genericOAuthEnabled;
     $scope.allowUserPassLogin = config.allowUserPassLogin;
     $scope.allowUserPassLogin = config.allowUserPassLogin;
+    $scope.genericOAuthEnabled = config.genericOAuthEnabled;
+    $scope.oauthProviderName = config.oauthProviderName;
     $scope.disableUserSignUp = config.disableUserSignUp;
     $scope.disableUserSignUp = config.disableUserSignUp;
     $scope.loginHint     = config.loginHint;
     $scope.loginHint     = config.loginHint;
 
 

+ 1 - 1
public/app/core/directives/metric_segment.js

@@ -113,7 +113,7 @@ function (_, $, coreModule) {
           if (str[0] === '/') { str = str.substring(1); }
           if (str[0] === '/') { str = str.substring(1); }
           if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); }
           if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); }
           try {
           try {
-            return item.toLowerCase().match(str);
+            return item.toLowerCase().match(str.toLowerCase());
           } catch(e) {
           } catch(e) {
             return false;
             return false;
           }
           }

+ 8 - 9
public/app/core/services/alert_srv.ts

@@ -80,28 +80,27 @@ export class AlertSrv {
   showConfirmModal(payload) {
   showConfirmModal(payload) {
     var scope = this.$rootScope.$new();
     var scope = this.$rootScope.$new();
 
 
-    scope.title = payload.title;
-    scope.text = payload.text;
-    scope.text2 = payload.text2;
-    scope.confirmTextRequired = payload.confirmText !== undefined && payload.confirmText !== "";
-
     scope.onConfirm = function() {
     scope.onConfirm = function() {
-      if (!scope.confirmTextRequired || (scope.confirmTextRequired && scope.confirmTextValid)) {
-        payload.onConfirm();
-        scope.dismiss();
-      }
+      payload.onConfirm();
+      scope.dismiss();
     };
     };
 
 
     scope.updateConfirmText = function(value) {
     scope.updateConfirmText = function(value) {
       scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase();
       scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase();
     };
     };
 
 
+    scope.title = payload.title;
+    scope.text = payload.text;
+    scope.text2 = payload.text2;
+    scope.confirmText = payload.confirmText;
+
     scope.onConfirm = payload.onConfirm;
     scope.onConfirm = payload.onConfirm;
     scope.onAltAction = payload.onAltAction;
     scope.onAltAction = payload.onAltAction;
     scope.altActionText = payload.altActionText;
     scope.altActionText = payload.altActionText;
     scope.icon = payload.icon || "fa-check";
     scope.icon = payload.icon || "fa-check";
     scope.yesText = payload.yesText || "Yes";
     scope.yesText = payload.yesText || "Yes";
     scope.noText = payload.noText || "Cancel";
     scope.noText = payload.noText || "Cancel";
+    scope.confirmTextValid = scope.confirmText ? false : true;
 
 
     var confirmModal = this.$modal({
     var confirmModal = this.$modal({
       template: 'public/app/partials/confirm_modal.html',
       template: 'public/app/partials/confirm_modal.html',

+ 10 - 2
public/app/features/alerting/alert_def.ts

@@ -36,6 +36,13 @@ var reducerTypes = [
   {text: 'count()', value: 'count'},
   {text: 'count()', value: 'count'},
 ];
 ];
 
 
+var noDataModes = [
+  {text: 'OK', value: 'ok'},
+  {text: 'Critical', value: 'critical'},
+  {text: 'Warning', value: 'warning'},
+  {text: 'Unknown', value: 'unknown'},
+];
+
 function createReducerPart(model) {
 function createReducerPart(model) {
   var def = new QueryPartDef({type: model.type, defaultParams: []});
   var def = new QueryPartDef({type: model.type, defaultParams: []});
   return new QueryPart(model, def);
   return new QueryPart(model, def);
@@ -69,9 +76,9 @@ function getStateDisplayModel(state) {
         stateClass: 'alert-state-warning'
         stateClass: 'alert-state-warning'
       };
       };
     }
     }
-    case 'pending': {
+    case 'unknown': {
       return {
       return {
-        text: 'PENDING',
+        text: 'UNKNOWN',
         iconClass: "fa fa-question",
         iconClass: "fa fa-question",
         stateClass: 'alert-state-warning'
         stateClass: 'alert-state-warning'
       };
       };
@@ -100,6 +107,7 @@ export default {
   conditionTypes: conditionTypes,
   conditionTypes: conditionTypes,
   evalFunctions: evalFunctions,
   evalFunctions: evalFunctions,
   severityLevels: severityLevels,
   severityLevels: severityLevels,
+  noDataModes: noDataModes,
   reducerTypes: reducerTypes,
   reducerTypes: reducerTypes,
   createReducerPart: createReducerPart,
   createReducerPart: createReducerPart,
 };
 };

+ 1 - 1
public/app/features/alerting/alert_list_ctrl.ts

@@ -13,7 +13,7 @@ export class AlertListCtrl {
   stateFilters = [
   stateFilters = [
     {text: 'All', value: null},
     {text: 'All', value: null},
     {text: 'OK', value: 'ok'},
     {text: 'OK', value: 'ok'},
-    {text: 'Pending', value: 'pending'},
+    {text: 'Unknown', value: 'unknown'},
     {text: 'Warning', value: 'warning'},
     {text: 'Warning', value: 'warning'},
     {text: 'Critical', value: 'critical'},
     {text: 'Critical', value: 'critical'},
     {text: 'Execution Error', value: 'execution_error'},
     {text: 'Execution Error', value: 'execution_error'},

+ 41 - 5
public/app/features/alerting/alert_tab_ctrl.ts

@@ -5,6 +5,7 @@ import {ThresholdMapper} from './threshold_mapper';
 import {QueryPart} from 'app/core/components/query_part/query_part';
 import {QueryPart} from 'app/core/components/query_part/query_part';
 import alertDef from './alert_def';
 import alertDef from './alert_def';
 import config from 'app/core/config';
 import config from 'app/core/config';
+import moment from 'moment';
 
 
 export class AlertTabCtrl {
 export class AlertTabCtrl {
   panel: any;
   panel: any;
@@ -17,11 +18,13 @@ export class AlertTabCtrl {
   conditionModels: any;
   conditionModels: any;
   evalFunctions: any;
   evalFunctions: any;
   severityLevels: any;
   severityLevels: any;
+  noDataModes: any;
   addNotificationSegment;
   addNotificationSegment;
   notifications;
   notifications;
   alertNotifications;
   alertNotifications;
   error: string;
   error: string;
   appSubUrl: string;
   appSubUrl: string;
+  alertHistory: any;
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(private $scope,
   constructor(private $scope,
@@ -39,6 +42,7 @@ export class AlertTabCtrl {
     this.evalFunctions = alertDef.evalFunctions;
     this.evalFunctions = alertDef.evalFunctions;
     this.conditionTypes = alertDef.conditionTypes;
     this.conditionTypes = alertDef.conditionTypes;
     this.severityLevels = alertDef.severityLevels;
     this.severityLevels = alertDef.severityLevels;
+    this.noDataModes = alertDef.noDataModes;
     this.appSubUrl = config.appSubUrl;
     this.appSubUrl = config.appSubUrl;
   }
   }
 
 
@@ -60,6 +64,7 @@ export class AlertTabCtrl {
     // build notification model
     // build notification model
     this.notifications = [];
     this.notifications = [];
     this.alertNotifications = [];
     this.alertNotifications = [];
+    this.alertHistory = [];
 
 
     return this.backendSrv.get('/api/alert-notifications').then(res => {
     return this.backendSrv.get('/api/alert-notifications').then(res => {
       this.notifications = res;
       this.notifications = res;
@@ -74,6 +79,21 @@ export class AlertTabCtrl {
     });
     });
   }
   }
 
 
+  getAlertHistory() {
+    this.backendSrv.get(`/api/alert-history?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}`).then(res => {
+      this.alertHistory = _.map(res, ah => {
+        ah.time = moment(ah.timestamp).format('MMM D, YYYY HH:mm:ss');
+        ah.stateModel = alertDef.getStateDisplayModel(ah.newState);
+
+        ah.metrics = _.map(ah.data, ev=> {
+          return ev.Metric + "=" + ev.Value;
+        }).join(', ');
+
+        return ah;
+      });
+    });
+  }
+
   getNotificationIcon(type) {
   getNotificationIcon(type) {
     switch (type) {
     switch (type) {
       case "email": return "fa fa-envelope";
       case "email": return "fa fa-envelope";
@@ -88,6 +108,13 @@ export class AlertTabCtrl {
     }));
     }));
   }
   }
 
 
+  changeTabIndex(newTabIndex) {
+    this.subTabIndex = newTabIndex;
+
+    if (this.subTabIndex === 2) {
+      this.getAlertHistory();
+    }
+  }
 
 
   notificationAdded() {
   notificationAdded() {
     var model = _.findWhere(this.notifications, {name: this.addNotificationSegment.value});
     var model = _.findWhere(this.notifications, {name: this.addNotificationSegment.value});
@@ -109,13 +136,18 @@ export class AlertTabCtrl {
   }
   }
 
 
   initModel() {
   initModel() {
-    var alert = this.alert = this.panel.alert = this.panel.alert || {};
+    var alert = this.alert = this.panel.alert = this.panel.alert || {enabled: false};
+
+    if (!this.alert.enabled) {
+      return;
+    }
 
 
     alert.conditions = alert.conditions || [];
     alert.conditions = alert.conditions || [];
     if (alert.conditions.length === 0) {
     if (alert.conditions.length === 0) {
       alert.conditions.push(this.buildDefaultCondition());
       alert.conditions.push(this.buildDefaultCondition());
     }
     }
 
 
+    alert.noDataState = alert.noDataState || 'unknown';
     alert.severity = alert.severity || 'critical';
     alert.severity = alert.severity || 'critical';
     alert.frequency = alert.frequency || '60s';
     alert.frequency = alert.frequency || '60s';
     alert.handler = alert.handler || 1;
     alert.handler = alert.handler || 1;
@@ -129,11 +161,9 @@ export class AlertTabCtrl {
       return memo;
       return memo;
     }, []);
     }, []);
 
 
-    if (this.alert.enabled) {
-      this.panelCtrl.editingThresholds = true;
-    }
-
     ThresholdMapper.alertToGraphThresholds(this.panel);
     ThresholdMapper.alertToGraphThresholds(this.panel);
+
+    this.panelCtrl.editingThresholds = true;
     this.panelCtrl.render();
     this.panelCtrl.render();
   }
   }
 
 
@@ -157,6 +187,10 @@ export class AlertTabCtrl {
   }
   }
 
 
   validateModel() {
   validateModel() {
+    if (!this.alert.enabled) {
+      return;
+    }
+
     let firstTarget;
     let firstTarget;
     var fixed = false;
     var fixed = false;
     let foundTarget = null;
     let foundTarget = null;
@@ -192,6 +226,8 @@ export class AlertTabCtrl {
           this.error = 'Currently the alerting backend only supports Graphite queries';
           this.error = 'Currently the alerting backend only supports Graphite queries';
         } else if (this.templateSrv.variableExists(foundTarget.target)) {
         } else if (this.templateSrv.variableExists(foundTarget.target)) {
           this.error = 'Template variables are not supported in alert queries';
           this.error = 'Template variables are not supported in alert queries';
+        } else {
+          this.error = '';
         }
         }
       });
       });
     }
     }

+ 25 - 2
public/app/features/alerting/notification_edit_ctrl.ts

@@ -7,6 +7,8 @@ import config from 'app/core/config';
 
 
 export class AlertNotificationEditCtrl {
 export class AlertNotificationEditCtrl {
   model: any;
   model: any;
+  showTest: boolean = false;
+  testSeverity: string = "critical";
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(private $routeParams, private backendSrv, private $scope, private $location) {
   constructor(private $routeParams, private backendSrv, private $scope, private $location) {
@@ -15,7 +17,10 @@ export class AlertNotificationEditCtrl {
     } else {
     } else {
       this.model = {
       this.model = {
         type: 'email',
         type: 'email',
-        settings: {}
+        settings: {
+          severityFilter: 'none'
+        },
+        isDefault: false
       };
       };
     }
     }
   }
   }
@@ -38,8 +43,8 @@ export class AlertNotificationEditCtrl {
       });
       });
     } else {
     } else {
       this.backendSrv.post(`/api/alert-notifications`, this.model).then(res => {
       this.backendSrv.post(`/api/alert-notifications`, this.model).then(res => {
-        this.$location.path('alerting/notification/' + res.id + '/edit');
         this.$scope.appEvent('alert-success', ['Notification created', '']);
         this.$scope.appEvent('alert-success', ['Notification created', '']);
+        this.$location.path('alerting/notifications');
       });
       });
     }
     }
   }
   }
@@ -47,6 +52,24 @@ export class AlertNotificationEditCtrl {
   typeChanged() {
   typeChanged() {
     this.model.settings = {};
     this.model.settings = {};
   }
   }
+
+  toggleTest() {
+    this.showTest = !this.showTest;
+  }
+
+  testNotification() {
+    var payload = {
+      name: this.model.name,
+      type: this.model.type,
+      settings: this.model.settings,
+      severity: this.testSeverity
+    };
+
+    this.backendSrv.post(`/api/alert-notifications/test`, payload)
+      .then(res => {
+        this.$scope.appEvent('alert-succes', ['Test notification sent', '']);
+      });
+  }
 }
 }
 
 
 coreModule.controller('AlertNotificationEditCtrl', AlertNotificationEditCtrl);
 coreModule.controller('AlertNotificationEditCtrl', AlertNotificationEditCtrl);

+ 45 - 8
public/app/features/alerting/partials/alert_tab.html

@@ -2,15 +2,15 @@
 	<aside class="edit-sidemenu-aside">
 	<aside class="edit-sidemenu-aside">
 		<ul class="edit-sidemenu">
 		<ul class="edit-sidemenu">
 			<li ng-class="{active: ctrl.subTabIndex === 0}">
 			<li ng-class="{active: ctrl.subTabIndex === 0}">
-				<a ng-click="ctrl.subTabIndex = 0">Alert Config</a>
+				<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
 			</li>
 			</li>
 			<li ng-class="{active: ctrl.subTabIndex === 1}">
 			<li ng-class="{active: ctrl.subTabIndex === 1}">
-				<a ng-click="ctrl.subTabIndex = 1">
+				<a ng-click="ctrl.changeTabIndex(1)">
 					Notifications <span class="muted">({{ctrl.alert.notifications.length}})</span>
 					Notifications <span class="muted">({{ctrl.alert.notifications.length}})</span>
 				</a>
 				</a>
 			</li>
 			</li>
 			<li ng-class="{active: ctrl.subTabIndex === 2}">
 			<li ng-class="{active: ctrl.subTabIndex === 2}">
-				<a ng-click="ctrl.subTabIndex = 2">Alert History</a>
+				<a ng-click="ctrl.changeTabIndex(2)">Alert History</a>
 			</li>
 			</li>
       <li>
       <li>
 				<a ng-click="ctrl.delete()">Delete</a>
 				<a ng-click="ctrl.delete()">Delete</a>
@@ -52,20 +52,20 @@
 						<span class="gf-form-label query-keyword width-5" ng-if="$index">AND</span>
 						<span class="gf-form-label query-keyword width-5" ng-if="$index">AND</span>
 						<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
 						<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
 					</div>
 					</div>
-					<div class="gf-form">
-						<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
+          <div class="gf-form">
+						<query-part-editor class="gf-form-label query-part" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
 						</query-part-editor>
 						</query-part-editor>
+            <span class="gf-form-label query-keyword">OF</span>
 					</div>
 					</div>
 					<div class="gf-form">
 					<div class="gf-form">
-						<span class="gf-form-label">Reducer</span>
-						<query-part-editor class="gf-form-label query-part" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
+						<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
 						</query-part-editor>
 						</query-part-editor>
 					</div>
 					</div>
 					<div class="gf-form">
 					<div class="gf-form">
 						<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
 						<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
 						<input class="gf-form-input max-width-7" type="number" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
 						<input class="gf-form-input max-width-7" type="number" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
             <label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
             <label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
-						<input class="gf-form-input max-width-7" type="number" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
+            <input class="gf-form-input max-width-7" type="number" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
 					</div>
 					</div>
 					<div class="gf-form">
 					<div class="gf-form">
 						<label class="gf-form-label">
 						<label class="gf-form-label">
@@ -89,6 +89,18 @@
 					</label>
 					</label>
 				</div>
 				</div>
 
 
+			</div>
+
+			<div class="gf-form-group">
+				<div class="gf-form">
+          <span class="gf-form-label">If no data points or all values are null</span>
+          <span class="gf-form-label query-keyword">SET STATE TO</span>
+					<div class="gf-form-select-wrapper">
+						<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
+						</select>
+					</div>
+				</div>
+
 				<div class="gf-form-button-row">
 				<div class="gf-form-button-row">
 					<button class="btn btn-inverse" ng-click="ctrl.test()">
 					<button class="btn btn-inverse" ng-click="ctrl.test()">
 						Test Rule
 						Test Rule
@@ -122,6 +134,31 @@
 				<textarea class="gf-form-input width-20" rows="10" ng-model="ctrl.alert.message"  placeholder="Notification message details..."></textarea>
 				<textarea class="gf-form-input width-20" rows="10" ng-model="ctrl.alert.message"  placeholder="Notification message details..."></textarea>
 			</div>
 			</div>
 		</div>
 		</div>
+
+		<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
+			<h5 class="section-heading">Alert history</h5>
+			<section class="card-section card-list-layout-list">
+				<ol class="card-list" >
+					<li class="card-item-wrapper" ng-repeat="ah in ctrl.alertHistory">
+						<div class="card-item card-item--alert">
+							<div class="card-item-body">
+								<div class="card-item-details">
+									<div class="card-item-sub-name">
+										<span class="alert-list-item-state {{ah.stateModel.stateClass}}">
+											<i class="{{ah.stateModel.iconClass}}"></i>
+											{{ah.stateModel.text}}
+										</span> {{ah.metrics}}
+									</div>
+									<div class="card-item-sub-name">
+										{{ah.time}}
+									</div>
+								</div>
+							</div>
+						</div>
+					</li>
+				</ol>
+			</section>
+		</div>
 	</div>
 	</div>
 </div>
 </div>
 
 

+ 43 - 5
public/app/features/alerting/partials/notification_edit.html

@@ -12,11 +12,11 @@
 
 
 	<div class="gf-form-group">
 	<div class="gf-form-group">
 		<div class="gf-form">
 		<div class="gf-form">
-			<span class="gf-form-label width-8">Name</span>
+			<span class="gf-form-label width-12">Name</span>
 			<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
 			<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
 		</div>
 		</div>
 		<div class="gf-form">
 		<div class="gf-form">
-			<span class="gf-form-label width-8">Type</span>
+			<span class="gf-form-label width-12">Type</span>
 			<div class="gf-form-select-wrapper width-15">
 			<div class="gf-form-select-wrapper width-15">
 				<select class="gf-form-input"
 				<select class="gf-form-input"
 					ng-model="ctrl.model.type"
 					ng-model="ctrl.model.type"
@@ -25,6 +25,24 @@
 				</select>
 				</select>
 			</div>
 			</div>
 		</div>
 		</div>
+		<div class="gf-form">
+			<span class="gf-form-label width-12">Severity filter</span>
+			<div class="gf-form-select-wrapper width-15">
+				<select class="gf-form-input"
+					ng-model="ctrl.model.settings.severityFilter"
+					ng-options="t for t in ['none', 'critical', 'warning']">
+				</select>
+			</div>
+		</div>
+		<div class="gf-form">
+			<gf-form-switch
+				class="gf-form"
+				label="Send on all alerts"
+				label-class="width-12"
+				checked="ctrl.model.isDefault"
+				tooltip="Use this notification for all alerts">
+			</gf-form-switch>
+		</div>
 	</div>
 	</div>
 
 
 	<div class="gf-form-group" ng-show="ctrl.model.type === 'webhook'">
 	<div class="gf-form-group" ng-show="ctrl.model.type === 'webhook'">
@@ -60,7 +78,27 @@
 		</div>
 		</div>
 	</div>
 	</div>
 
 
-  <div class="gf-form-button-row">
-    <button ng-click="ctrl.save()" class="btn btn-success">Save</button>
-  </div>
+  <div class="gf-form-group">
+		<div class="gf-form-inline">
+			<div class="gf-form width-6">
+				<button ng-click="ctrl.save()" class="btn btn-success">Save</button>
+			</div>
+			<div class="gf-form width-8">
+				<button ng-click="ctrl.toggleTest()" class="btn btn-secondary">Test</button>
+			</div>
+
+			<div class="gf-form width-20" ng-show="ctrl.showTest">
+			  <span class="gf-form-label width-13">Severity for test notification</span>
+				<div class="gf-form-select-wrapper width-7">
+					<select class="gf-form-input"
+						ng-model="ctrl.testSeverity"
+						ng-options="t for t in ['critical', 'warning', 'ok']">
+					</select>
+				</div>
+			</div>
+			<div class="gf-form" ng-show="ctrl.showTest">
+				<button ng-click="ctrl.testNotification()" class="btn btn-secondary">Send</button>
+			</div>
+		</div>
+	</div>
 </div>
 </div>

+ 5 - 2
public/app/features/alerting/partials/notifications_list.html

@@ -10,7 +10,7 @@
     </a>
     </a>
   </div>
   </div>
 
 
-	<table class="grafana-options-table" style="/*width: 600px;*/">
+	<table class="grafana-options-table">
 		<thead>
 		<thead>
 			<th style="min-width: 200px"><strong>Name</strong></th>
 			<th style="min-width: 200px"><strong>Name</strong></th>
 			<th style="min-width: 100px">Type</th>
 			<th style="min-width: 100px">Type</th>
@@ -25,7 +25,10 @@
 			<td>
 			<td>
 				{{notification.type}}
 				{{notification.type}}
 			</td>
 			</td>
-			<td>
+			<td class="text-right">
+				<span class="btn btn-secondary btn-small" ng-show="notification.isDefault == true">
+					default
+				</span>
 				<a href="alerting/notification/{{notification.id}}/edit" class="btn btn-inverse btn-small">
 				<a href="alerting/notification/{{notification.id}}/edit" class="btn btn-inverse btn-small">
 					<i class="fa fa-edit"></i>
 					<i class="fa fa-edit"></i>
 					edit
 					edit

+ 5 - 10
public/app/features/dashboard/dashnav/dashnav.ts

@@ -158,18 +158,13 @@ export class DashNavCtrl {
     $scope.deleteDashboard = function() {
     $scope.deleteDashboard = function() {
       var confirmText = "";
       var confirmText = "";
       var text2 = $scope.dashboard.title;
       var text2 = $scope.dashboard.title;
-      var alerts = 0;
-
-      _.each($scope.dashboard.rows, row => {
-        _.each(row.panels, panel => {
-          if (panel.alerting && panel.alerting.queryRef !== '- select query -') {
-            alerts += 1;
-          };
-        });
-      });
+      var alerts = $scope.dashboard.rows.reduce((memo, row) => {
+        memo += row.panels.filter(panel => panel.alert && panel.alert.enabled).length;
+        return memo;
+      }, 0);
 
 
       if (alerts > 0) {
       if (alerts > 0) {
-        confirmText = $scope.dashboard.title;
+        confirmText = 'DELETE';
         text2 = `This dashboad contains ${alerts} alerts. Deleting this dashboad will also delete those alerts`;
         text2 = `This dashboad contains ${alerts} alerts. Deleting this dashboad will also delete those alerts`;
       }
       }
 
 

+ 1 - 1
public/app/features/dashboard/import/dash_import.html

@@ -36,7 +36,7 @@
 
 
 			<div class="gf-form-group">
 			<div class="gf-form-group">
 				<div class="gf-form">
 				<div class="gf-form">
-					<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-ctrl="ctrl.jsonText"></textarea>
+					<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-model="ctrl.jsonText"></textarea>
 				</div>
 				</div>
 				<button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
 				<button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
 					<i class="fa fa-paste"></i>
 					<i class="fa fa-paste"></i>

+ 8 - 0
public/app/features/dashboard/saveDashboardAsCtrl.js

@@ -12,6 +12,14 @@ function (angular) {
       $scope.clone.id = null;
       $scope.clone.id = null;
       $scope.clone.editable = true;
       $scope.clone.editable = true;
       $scope.clone.title = $scope.clone.title + " Copy";
       $scope.clone.title = $scope.clone.title + " Copy";
+
+      // remove alerts
+      $scope.clone.rows.forEach(function(row) {
+        row.panels.forEach(function(panel) {
+          delete panel.alert;
+        });
+      });
+
       // remove auto update
       // remove auto update
       delete $scope.clone.autoUpdate;
       delete $scope.clone.autoUpdate;
     };
     };

+ 3 - 4
public/app/partials/confirm_modal.html

@@ -22,14 +22,13 @@
 		</div>
 		</div>
 
 
 
 
-		<div class="modal-content-confirm-text" ng-if="confirmTextRequired">
-			<span><i class="fa fa-warning"></i> Please type in the name of the dashboard to confirm.</span>
-			<input type="text" class="gf-form-input width-16" style="display: inline-block;" ng-model="confirmInput" ng-change="updateConfirmText(confirmInput)">
+		<div class="modal-content-confirm-text" ng-if="confirmText">
+			<input type="text" class="gf-form-input width-16" style="display: inline-block;" placeholder="Type {{confirmText}} to confirm" ng-model="confirmInput" ng-change="updateConfirmText(confirmInput)">
 		</div>
 		</div>
 
 
 		<div class="confirm-modal-buttons">
 		<div class="confirm-modal-buttons">
 			<button type="button" class="btn btn-inverse" ng-click="dismiss()">{{noText}}</button>
 			<button type="button" class="btn btn-inverse" ng-click="dismiss()">{{noText}}</button>
-			<button type="button" class="btn btn-danger" ng-click="onConfirm();dismiss();">{{yesText}}</button>
+			<button type="button" class="btn btn-danger" ng-click="onConfirm();dismiss();" ng-disabled="!confirmTextValid">{{yesText}}</button>
 			<button ng-show="onAltAction" type="button" class="btn btn-success" ng-click="dismiss();onAltAction();">{{altActionText}}</button>
 			<button ng-show="onAltAction" type="button" class="btn btn-success" ng-click="dismiss();onAltAction();">{{altActionText}}</button>
 		</div>
 		</div>
 	</div>
 	</div>

+ 4 - 0
public/app/partials/login.html

@@ -59,6 +59,10 @@
 						<i class="fa fa-github"></i>
 						<i class="fa fa-github"></i>
 						with Github
 						with Github
 					</a>
 					</a>
+					<a class="btn btn-large btn-generic-oauth" href="login/generic_oauth" target="_self" ng-if="genericOAuthEnabled">
+						<i class="fa fa-gear"></i>
+            with {{oauthProviderName || "OAuth 2"}}
+          </a>
 				</div>
 				</div>
 			</div>
 			</div>
 
 

+ 9 - 0
public/app/plugins/datasource/influxdb/query_part.ts

@@ -254,6 +254,15 @@ register({
   renderer: functionRenderer,
   renderer: functionRenderer,
 });
 });
 
 
+register({
+  type: 'elapsed',
+  addStrategy: addTransformationStrategy,
+  category: categories.Transformations,
+  params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}],
+  defaultParams: ['10s'],
+  renderer: functionRenderer,
+});
+
 // Selectors
 // Selectors
 register({
 register({
   type: 'bottom',
   type: 'bottom',

+ 0 - 1
public/app/plugins/panel/graph/graph.js

@@ -61,7 +61,6 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) {
         ctrl.events.on('render', function(renderData) {
         ctrl.events.on('render', function(renderData) {
           data = renderData || data;
           data = renderData || data;
           if (!data) {
           if (!data) {
-            ctrl.refresh();
             return;
             return;
           }
           }
           annotations = data.annotations || annotations;
           annotations = data.annotations || annotations;

+ 0 - 1
public/sass/_variables.dark.scss

@@ -39,7 +39,6 @@ $brand-primary:         $orange;
 $brand-success:         $green;
 $brand-success:         $green;
 $brand-warning:         $brand-primary;
 $brand-warning:         $brand-primary;
 $brand-danger:          $red;
 $brand-danger:          $red;
-$brand-text-highlight:  #f7941d;
 
 
 // Status colors
 // Status colors
 // -------------------------
 // -------------------------

+ 0 - 1
public/sass/_variables.light.scss

@@ -44,7 +44,6 @@ $brand-primary:         $orange;
 $brand-success:         $green;
 $brand-success:         $green;
 $brand-warning:         $orange;
 $brand-warning:         $orange;
 $brand-danger:          $red;
 $brand-danger:          $red;
-$brand-text-highlight:  #f7941d;
 
 
 // Status colors
 // Status colors
 // -------------------------
 // -------------------------

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels