diff --git a/cmd/snaptel/metric.go b/cmd/snaptel/metric.go index bd0abf19a..d021fe102 100644 --- a/cmd/snaptel/metric.go +++ b/cmd/snaptel/metric.go @@ -29,7 +29,7 @@ import ( "time" "github.com/intelsdi-x/snap/mgmt/rest/client" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" "github.com/urfave/cli" "github.com/intelsdi-x/snap/pkg/stringutils" diff --git a/control/plugin/cpolicy/node.go b/control/plugin/cpolicy/node.go index cf096776a..e5b08066c 100644 --- a/control/plugin/cpolicy/node.go +++ b/control/plugin/cpolicy/node.go @@ -167,16 +167,18 @@ func (p *ConfigPolicyNode) Add(rules ...Rule) { } } +type RuleTableSlice []RuleTable + type RuleTable struct { - Name string - Type string - Default interface{} - Required bool - Minimum interface{} - Maximum interface{} + Name string `json:"name"` + Type string `json:"type"` + Default interface{} `json:"default,omitempty"` + Required bool `json:"required"` + Minimum interface{} `json:"minimum,omitempty"` + Maximum interface{} `json:"maximum,omitempty"` } -func (p *ConfigPolicyNode) RulesAsTable() []RuleTable { +func (p *ConfigPolicyNode) RulesAsTable() RuleTableSlice { p.mutex.Lock() defer p.mutex.Unlock() diff --git a/mgmt/rest/api/api.go b/mgmt/rest/api/api.go new file mode 100644 index 000000000..1b85a902d --- /dev/null +++ b/mgmt/rest/api/api.go @@ -0,0 +1,18 @@ +package api + +import ( + "github.com/julienschmidt/httprouter" +) + +type API interface { + GetRoutes() []Route + BindMetricManager(Metrics) + BindTaskManager(Tasks) + BindTribeManager(Tribe) + BindConfigManager(Config) +} + +type Route struct { + Method, Path string + Handle httprouter.Handle +} diff --git a/mgmt/rest/api/config.go b/mgmt/rest/api/config.go new file mode 100644 index 000000000..f45c05028 --- /dev/null +++ b/mgmt/rest/api/config.go @@ -0,0 +1,15 @@ +package api + +import ( + "github.com/intelsdi-x/snap/core" + "github.com/intelsdi-x/snap/core/cdata" +) + +type Config interface { + GetPluginConfigDataNode(core.PluginType, string, int) cdata.ConfigDataNode + GetPluginConfigDataNodeAll() cdata.ConfigDataNode + MergePluginConfigDataNode(pluginType core.PluginType, name string, ver int, cdn *cdata.ConfigDataNode) cdata.ConfigDataNode + MergePluginConfigDataNodeAll(cdn *cdata.ConfigDataNode) cdata.ConfigDataNode + DeletePluginConfigDataNodeField(pluginType core.PluginType, name string, ver int, fields ...string) cdata.ConfigDataNode + DeletePluginConfigDataNodeFieldAll(fields ...string) cdata.ConfigDataNode +} diff --git a/mgmt/rest/api/metric.go b/mgmt/rest/api/metric.go new file mode 100644 index 000000000..503fb97ee --- /dev/null +++ b/mgmt/rest/api/metric.go @@ -0,0 +1,18 @@ +package api + +import ( + "github.com/intelsdi-x/snap/core" + "github.com/intelsdi-x/snap/core/serror" +) + +type Metrics interface { + MetricCatalog() ([]core.CatalogedMetric, error) + FetchMetrics(core.Namespace, int) ([]core.CatalogedMetric, error) + GetMetricVersions(core.Namespace) ([]core.CatalogedMetric, error) + GetMetric(core.Namespace, int) (core.CatalogedMetric, error) + Load(*core.RequestedPlugin) (core.CatalogedPlugin, serror.SnapError) + Unload(core.Plugin) (core.CatalogedPlugin, serror.SnapError) + PluginCatalog() core.PluginCatalog + AvailablePlugins() []core.AvailablePlugin + GetAutodiscoverPaths() []string +} diff --git a/mgmt/rest/api/task.go b/mgmt/rest/api/task.go new file mode 100644 index 000000000..baab427d0 --- /dev/null +++ b/mgmt/rest/api/task.go @@ -0,0 +1,19 @@ +package api + +import ( + "github.com/intelsdi-x/snap/core" + "github.com/intelsdi-x/snap/core/serror" + "github.com/intelsdi-x/snap/pkg/schedule" + "github.com/intelsdi-x/snap/scheduler/wmap" +) + +type Tasks interface { + CreateTask(schedule.Schedule, *wmap.WorkflowMap, bool, ...core.TaskOption) (core.Task, core.TaskErrors) + GetTasks() map[string]core.Task + GetTask(string) (core.Task, error) + StartTask(string) []serror.SnapError + StopTask(string) []serror.SnapError + RemoveTask(string) error + WatchTask(string, core.TaskWatcherHandler) (core.TaskWatcherCloser, error) + EnableTask(string) (core.Task, error) +} diff --git a/mgmt/rest/api/tribe.go b/mgmt/rest/api/tribe.go new file mode 100644 index 000000000..662f248d4 --- /dev/null +++ b/mgmt/rest/api/tribe.go @@ -0,0 +1,17 @@ +package api + +import ( + "github.com/intelsdi-x/snap/core/serror" + "github.com/intelsdi-x/snap/mgmt/tribe/agreement" +) + +type Tribe interface { + GetAgreement(name string) (*agreement.Agreement, serror.SnapError) + GetAgreements() map[string]*agreement.Agreement + AddAgreement(name string) serror.SnapError + RemoveAgreement(name string) serror.SnapError + JoinAgreement(agreementName, memberName string) serror.SnapError + LeaveAgreement(agreementName, memberName string) serror.SnapError + GetMembers() []string + GetMember(name string) *agreement.Member +} diff --git a/mgmt/rest/client/client.go b/mgmt/rest/client/client.go index 50001a288..b25aef6f0 100644 --- a/mgmt/rest/client/client.go +++ b/mgmt/rest/client/client.go @@ -38,7 +38,7 @@ import ( "github.com/asaskevich/govalidator" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" ) var ( diff --git a/mgmt/rest/client/client_func_test.go b/mgmt/rest/client/client_func_test.go index 2c861e6d0..1b531a7d2 100644 --- a/mgmt/rest/client/client_func_test.go +++ b/mgmt/rest/client/client_func_test.go @@ -36,6 +36,7 @@ import ( "github.com/intelsdi-x/snap/control" "github.com/intelsdi-x/snap/mgmt/rest" + "github.com/intelsdi-x/snap/mgmt/rest/v1" "github.com/intelsdi-x/snap/plugin/helper" "github.com/intelsdi-x/snap/scheduler" "github.com/intelsdi-x/snap/scheduler/wmap" @@ -75,7 +76,7 @@ func getWMFromSample(sample string) *wmap.WorkflowMap { // When we eventually have a REST API Stop command this can be killed. func startAPI() string { // Start a REST API to talk to - rest.StreamingBufferWindow = 0.01 + v1.StreamingBufferWindow = 0.01 log.SetLevel(LOG_LEVEL) r, _ := rest.New(rest.GetDefaultConfig()) c := control.New(control.GetDefaultConfig()) @@ -490,7 +491,7 @@ func TestSnapClient(t *testing.T) { }) Convey("WatchTasks", func() { Convey("invalid task ID", func() { - rest.StreamingBufferWindow = 0.01 + v1.StreamingBufferWindow = 0.01 type ea struct { events []string @@ -521,7 +522,7 @@ func TestSnapClient(t *testing.T) { So(r.Err.Error(), ShouldEqual, "Task not found: ID(1)") }) Convey("event stream", func() { - rest.StreamingBufferWindow = 0.01 + v1.StreamingBufferWindow = 0.01 sch := &Schedule{Type: "simple", Interval: "100ms"} tf := c.CreateTask(sch, wf, "baron", "", false, 0) diff --git a/mgmt/rest/client/client_tribe_func_test.go b/mgmt/rest/client/client_tribe_func_test.go index e1c3fc1eb..ddd89f97a 100644 --- a/mgmt/rest/client/client_tribe_func_test.go +++ b/mgmt/rest/client/client_tribe_func_test.go @@ -37,7 +37,7 @@ import ( "github.com/intelsdi-x/snap/control" "github.com/intelsdi-x/snap/mgmt/rest" "github.com/intelsdi-x/snap/mgmt/rest/client" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" "github.com/intelsdi-x/snap/mgmt/tribe" "github.com/intelsdi-x/snap/scheduler" ) diff --git a/mgmt/rest/client/config.go b/mgmt/rest/client/config.go index a87a3e726..6839b48f6 100644 --- a/mgmt/rest/client/config.go +++ b/mgmt/rest/client/config.go @@ -25,7 +25,7 @@ import ( "net/url" "github.com/intelsdi-x/snap/core/ctypes" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" ) // GetPluginConfig retrieves the merged plugin config given the type of plugin, diff --git a/mgmt/rest/client/metric.go b/mgmt/rest/client/metric.go index 1e9c1fa8a..35a09dbfc 100644 --- a/mgmt/rest/client/metric.go +++ b/mgmt/rest/client/metric.go @@ -23,7 +23,7 @@ import ( "errors" "fmt" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" ) var ( diff --git a/mgmt/rest/client/plugin.go b/mgmt/rest/client/plugin.go index 77ee4b492..04f67c3a8 100644 --- a/mgmt/rest/client/plugin.go +++ b/mgmt/rest/client/plugin.go @@ -27,7 +27,7 @@ import ( "time" "github.com/intelsdi-x/snap/core/serror" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" ) // LoadPlugin loads plugins for the given plugin names. diff --git a/mgmt/rest/client/task.go b/mgmt/rest/client/task.go index 1e3abea2f..9bcd529b9 100644 --- a/mgmt/rest/client/task.go +++ b/mgmt/rest/client/task.go @@ -29,7 +29,7 @@ import ( "time" "github.com/intelsdi-x/snap/core" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" "github.com/intelsdi-x/snap/scheduler/wmap" ) diff --git a/mgmt/rest/client/tribe.go b/mgmt/rest/client/tribe.go index b02d2e496..aa8ed68ad 100644 --- a/mgmt/rest/client/tribe.go +++ b/mgmt/rest/client/tribe.go @@ -23,7 +23,7 @@ import ( "encoding/json" "fmt" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" ) // ListMembers retrieves a list of tribe members through an HTTP GET call. diff --git a/mgmt/rest/config.go b/mgmt/rest/config.go index 9085a5f17..32b5e06ed 100644 --- a/mgmt/rest/config.go +++ b/mgmt/rest/config.go @@ -1,158 +1,94 @@ -/* -http://www.apache.org/licenses/LICENSE-2.0.txt - - -Copyright 2015 Intel Corporation - -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 rest -import ( - "net/http" - "strconv" - - "github.com/intelsdi-x/snap/core" - "github.com/intelsdi-x/snap/core/cdata" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" - "github.com/julienschmidt/httprouter" +// default configuration values +const ( + defaultEnable bool = true + defaultPort int = 8181 + defaultAddress string = "" + defaultHTTPS bool = false + defaultRestCertificate string = "" + defaultRestKey string = "" + defaultAuth bool = false + defaultAuthPassword string = "" + defaultPortSetByConfig bool = false + defaultPprof bool = false ) -func (s *Server) getPluginConfigItem(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - var err error - styp := p.ByName("type") - if styp == "" { - cdn := s.mc.GetPluginConfigDataNodeAll() - item := &rbody.PluginConfigItem{ConfigDataNode: cdn} - respond(200, item, w) - return - } - - typ, err := getPluginType(styp) - if err != nil { - respond(400, rbody.FromError(err), w) - return - } - - name := p.ByName("name") - sver := p.ByName("version") - var iver int - if sver != "" { - if iver, err = strconv.Atoi(sver); err != nil { - respond(400, rbody.FromError(err), w) - return - } - } else { - iver = -2 - } - - cdn := s.mc.GetPluginConfigDataNode(typ, name, iver) - item := &rbody.PluginConfigItem{ConfigDataNode: cdn} - respond(200, item, w) -} - -func (s *Server) deletePluginConfigItem(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - var err error - var typ core.PluginType - styp := p.ByName("type") - if styp != "" { - typ, err = getPluginType(styp) - if err != nil { - respond(400, rbody.FromError(err), w) - return - } - } - - name := p.ByName("name") - sver := p.ByName("version") - var iver int - if sver != "" { - if iver, err = strconv.Atoi(sver); err != nil { - respond(400, rbody.FromError(err), w) - return - } - } else { - iver = -2 - } - - src := []string{} - errCode, err := core.UnmarshalBody(&src, r.Body) - if errCode != 0 && err != nil { - respond(400, rbody.FromError(err), w) - return - } - - var res cdata.ConfigDataNode - if styp == "" { - res = s.mc.DeletePluginConfigDataNodeFieldAll(src...) - } else { - res = s.mc.DeletePluginConfigDataNodeField(typ, name, iver, src...) - } - - item := &rbody.DeletePluginConfigItem{ConfigDataNode: res} - respond(200, item, w) +// holds the configuration passed in through the SNAP config file +// Note: if this struct is modified, then the switch statement in the +// UnmarshalJSON method in this same file needs to be modified to +// match the field mapping that is defined here +type Config struct { + Enable bool `json:"enable"yaml:"enable"` + Port int `json:"port"yaml:"port"` + Address string `json:"addr"yaml:"addr"` + HTTPS bool `json:"https"yaml:"https"` + RestCertificate string `json:"rest_certificate"yaml:"rest_certificate"` + RestKey string `json:"rest_key"yaml:"rest_key"` + RestAuth bool `json:"rest_auth"yaml:"rest_auth"` + RestAuthPassword string `json:"rest_auth_password"yaml:"rest_auth_password"` + portSetByConfig bool `` + Pprof bool `json:"pprof"yaml:"pprof"` } -func (s *Server) setPluginConfigItem(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - var err error - var typ core.PluginType - styp := p.ByName("type") - if styp != "" { - typ, err = getPluginType(styp) - if err != nil { - respond(400, rbody.FromError(err), w) - return - } - } - - name := p.ByName("name") - sver := p.ByName("version") - var iver int - if sver != "" { - if iver, err = strconv.Atoi(sver); err != nil { - respond(400, rbody.FromError(err), w) - return - } - } else { - iver = -2 - } - - src := cdata.NewNode() - errCode, err := core.UnmarshalBody(src, r.Body) - if errCode != 0 && err != nil { - respond(400, rbody.FromError(err), w) - return - } +const ( + CONFIG_CONSTRAINTS = ` + "restapi" : { + "type": ["object", "null"], + "properties" : { + "enable": { + "type": "boolean" + }, + "https" : { + "type": "boolean" + }, + "rest_auth": { + "type": "boolean" + }, + "rest_auth_password": { + "type": "string" + }, + "rest_certificate": { + "type": "string" + }, + "rest_key" : { + "type": "string" + }, + "port" : { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "addr" : { + "type": "string" + }, + "pprof": { + "type": "boolean" + } + }, + "additionalProperties": false + } + ` +) - var res cdata.ConfigDataNode - if styp == "" { - res = s.mc.MergePluginConfigDataNodeAll(src) - } else { - res = s.mc.MergePluginConfigDataNode(typ, name, iver, src) +// GetDefaultConfig gets the default snapteld configuration +func GetDefaultConfig() *Config { + return &Config{ + Enable: defaultEnable, + Port: defaultPort, + Address: defaultAddress, + HTTPS: defaultHTTPS, + RestCertificate: defaultRestCertificate, + RestKey: defaultRestKey, + RestAuth: defaultAuth, + RestAuthPassword: defaultAuthPassword, + portSetByConfig: defaultPortSetByConfig, + Pprof: defaultPprof, } - - item := &rbody.SetPluginConfigItem{ConfigDataNode: res} - respond(200, item, w) } -func getPluginType(t string) (core.PluginType, error) { - if ityp, err := strconv.Atoi(t); err == nil { - return core.PluginType(ityp), nil - } - ityp, err := core.ToPluginType(t) - if err != nil { - return core.PluginType(-1), err - } - return ityp, nil +// define a method that can be used to determine if the port the RESTful +// API is listening on was set in the configuration file +func (c *Config) PortSetByConfigFile() bool { + return c.portSetByConfig } diff --git a/mgmt/rest/pprof.go b/mgmt/rest/pprof.go new file mode 100644 index 000000000..ff637d0bf --- /dev/null +++ b/mgmt/rest/pprof.go @@ -0,0 +1,44 @@ +package rest + +import ( + "net/http" + "net/http/pprof" + + "github.com/julienschmidt/httprouter" +) + +func (s *Server) addPprofRoutes() { + if s.pprof { + s.r.GET("/debug/pprof/", s.index) + s.r.GET("/debug/pprof/block", s.index) + s.r.GET("/debug/pprof/goroutine", s.index) + s.r.GET("/debug/pprof/heap", s.index) + s.r.GET("/debug/pprof/threadcreate", s.index) + s.r.GET("/debug/pprof/cmdline", s.cmdline) + s.r.GET("/debug/pprof/profile", s.profile) + s.r.GET("/debug/pprof/symbol", s.symbol) + s.r.GET("/debug/pprof/trace", s.trace) + } +} + +// profiling tools handlers + +func (s *Server) index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + pprof.Index(w, r) +} + +func (s *Server) cmdline(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + pprof.Cmdline(w, r) +} + +func (s *Server) profile(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + pprof.Profile(w, r) +} + +func (s *Server) symbol(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + pprof.Symbol(w, r) +} + +func (s *Server) trace(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + pprof.Trace(w, r) +} diff --git a/mgmt/rest/rest_func_test.go b/mgmt/rest/rest_func_test.go deleted file mode 100644 index 06dc8a04f..000000000 --- a/mgmt/rest/rest_func_test.go +++ /dev/null @@ -1,637 +0,0 @@ -// +build legacy - -/* -http://www.apache.org/licenses/LICENSE-2.0.txt - - -Copyright 2015 Intel Corporation - -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 rest - -// This test runs through basic REST API calls and validates them. - -import ( - "bufio" - "bytes" - "compress/gzip" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "mime/multipart" - "net/http" - "os" - "path/filepath" - "testing" - "time" - - log "github.com/Sirupsen/logrus" - - "github.com/intelsdi-x/snap/control" - "github.com/intelsdi-x/snap/core" - "github.com/intelsdi-x/snap/core/cdata" - "github.com/intelsdi-x/snap/core/ctypes" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" - "github.com/intelsdi-x/snap/pkg/cfgfile" - "github.com/intelsdi-x/snap/plugin/helper" - "github.com/intelsdi-x/snap/scheduler" - "github.com/intelsdi-x/snap/scheduler/wmap" - . "github.com/smartystreets/goconvey/convey" -) - -var ( - // Switching this turns on logging for all the REST API calls - LOG_LEVEL = log.WarnLevel - - SNAP_PATH = helper.BuildPath - SNAP_AUTODISCOVER_PATH = os.Getenv("SNAP_AUTODISCOVER_PATH") - MOCK_PLUGIN_PATH1 = helper.PluginFilePath("snap-plugin-collector-mock1") - MOCK_PLUGIN_PATH2 = helper.PluginFilePath("snap-plugin-collector-mock2") - FILE_PLUGIN_PATH = helper.PluginFilePath("snap-plugin-publisher-mock-file") - - CompressedUpload = true - TotalUploadSize = 0 - UploadCount = 0 -) - -const ( - MOCK_CONSTRAINTS = `{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "snapteld global config schema", - "type": ["object", "null"], - "properties": { - "control": { "$ref": "#/definitions/control" }, - "scheduler": { "$ref": "#/definitions/scheduler"}, - "restapi" : { "$ref": "#/definitions/restapi"}, - "tribe": { "$ref": "#/definitions/tribe"} - }, - "additionalProperties": true, - "definitions": { ` + - `"control": {}, "scheduler": {}, ` + CONFIG_CONSTRAINTS + `, "tribe":{}` + - `}` + - `}` -) - -type restAPIInstance struct { - port int - server *Server -} - -func command() string { - return "curl" -} - -func readBody(r *http.Response) []byte { - b, err := ioutil.ReadAll(r.Body) - if err != nil { - log.Fatal(err) - } - r.Body.Close() - return b -} - -func getAPIResponse(resp *http.Response) *rbody.APIResponse { - r := new(rbody.APIResponse) - rb := readBody(resp) - err := json.Unmarshal(rb, r) - if err != nil { - log.Fatal(err) - } - r.JSONResponse = string(rb) - return r -} - -func getStreamingAPIResponse(resp *http.Response) *rbody.APIResponse { - r := new(rbody.APIResponse) - rb := readBody(resp) - err := json.Unmarshal(rb, r) - if err != nil { - log.Fatal(err) - } - r.JSONResponse = string(rb) - return r -} - -type watchTaskResult struct { - eventChan chan string - doneChan chan struct{} - killChan chan struct{} -} - -func (w *watchTaskResult) close() { - close(w.doneChan) -} - -func watchTask(id string, port int) *watchTaskResult { - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/v1/tasks/%s/watch", port, id)) - if err != nil { - log.Fatal(err) - } - - r := &watchTaskResult{ - eventChan: make(chan string), - doneChan: make(chan struct{}), - killChan: make(chan struct{}), - } - go func() { - reader := bufio.NewReader(resp.Body) - for { - select { - case <-r.doneChan: - resp.Body.Close() - return - default: - line, _ := reader.ReadBytes('\n') - ste := &rbody.StreamedTaskEvent{} - err := json.Unmarshal(line, ste) - if err != nil { - log.Fatal(err) - r.close() - return - } - switch ste.EventType { - case rbody.TaskWatchTaskDisabled: - r.eventChan <- ste.EventType - r.close() - return - case rbody.TaskWatchTaskStopped, rbody.TaskWatchTaskStarted, rbody.TaskWatchMetricEvent: - log.Info(ste.EventType) - r.eventChan <- ste.EventType - } - } - } - }() - return r -} - -func getTasks(port int) *rbody.APIResponse { - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/v1/tasks", port)) - if err != nil { - log.Fatal(err) - } - return getAPIResponse(resp) -} - -func getTask(id string, port int) *rbody.APIResponse { - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/v1/tasks/%s", port, id)) - if err != nil { - log.Fatal(err) - } - return getAPIResponse(resp) -} - -func startTask(id string, port int) *rbody.APIResponse { - uri := fmt.Sprintf("http://localhost:%d/v1/tasks/%s/start", port, id) - client := &http.Client{} - b := bytes.NewReader([]byte{}) - req, err := http.NewRequest("PUT", uri, b) - if err != nil { - log.Fatal(err) - } - req.Header.Add("Content-Type", "application/json") - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - return getAPIResponse(resp) -} - -func stopTask(id string, port int) *rbody.APIResponse { - uri := fmt.Sprintf("http://localhost:%d/v1/tasks/%s/stop", port, id) - client := &http.Client{} - b := bytes.NewReader([]byte{}) - req, err := http.NewRequest("PUT", uri, b) - if err != nil { - log.Fatal(err) - } - req.Header.Add("Content-Type", "application/json") - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - return getAPIResponse(resp) -} - -func removeTask(id string, port int) *rbody.APIResponse { - uri := fmt.Sprintf("http://localhost:%d/v1/tasks/%s", port, id) - client := &http.Client{} - req, err := http.NewRequest("DELETE", uri, nil) - if err != nil { - log.Fatal(err) - } - req.Header.Add("Content-Type", "application/json") - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - return getAPIResponse(resp) -} - -func createTask(sample, name, interval string, noStart bool, port int) *rbody.APIResponse { - jsonP, err := ioutil.ReadFile("./wmap_sample/" + sample) - if err != nil { - log.Fatal(err) - } - wf, err := wmap.FromJson(jsonP) - if err != nil { - log.Fatal(err) - } - - uri := fmt.Sprintf("http://localhost:%d/v1/tasks", port) - - t := core.TaskCreationRequest{ - Schedule: &core.Schedule{Type: "simple", Interval: interval}, - Workflow: wf, - Name: name, - Start: !noStart, - } - // Marshal to JSON for request body - j, err := json.Marshal(t) - if err != nil { - log.Fatal(err) - } - - client := &http.Client{} - b := bytes.NewReader(j) - req, err := http.NewRequest("POST", uri, b) - if err != nil { - log.Fatal(err) - } - req.Header.Add("Content-Type", "application/json") - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - return getAPIResponse(resp) -} - -func enableTask(id string, port int) *rbody.APIResponse { - uri := fmt.Sprintf("http://localhost:%d/v1/tasks/%s/enable", port, id) - client := &http.Client{} - b := bytes.NewReader([]byte{}) - req, err := http.NewRequest("PUT", uri, b) - if err != nil { - log.Fatal(err) - } - req.Header.Add("Content-Type", "application/json") - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - return getAPIResponse(resp) -} - -func uploadPlugin(pluginPath string, port int) *rbody.APIResponse { - uri := fmt.Sprintf("http://localhost:%d/v1/plugins", port) - - client := &http.Client{} - file, err := os.Open(pluginPath) - if err != nil { - log.Fatal(err) - } - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - var part io.Writer - part, err = writer.CreateFormFile("snap-plugins", filepath.Base(pluginPath)) - if err != nil { - log.Fatal(err) - } - if CompressedUpload { - cpart := gzip.NewWriter(part) - _, err = io.Copy(cpart, file) - if err != nil { - log.Fatal(err) - } - err = cpart.Close() - } else { - _, err = io.Copy(part, file) - } - if err != nil { - log.Fatal(err) - } - err = writer.Close() - if err != nil { - log.Fatal(err) - } - TotalUploadSize += body.Len() - UploadCount += 1 - req, err := http.NewRequest("POST", uri, body) - if err != nil { - log.Fatal(err) - } - req.Header.Add("Content-Type", writer.FormDataContentType()) - if CompressedUpload { - req.Header.Add("Plugin-Compression", "gzip") - } - file.Close() - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - return getAPIResponse(resp) -} - -func unloadPlugin(port int, pluginType string, name string, version int) *rbody.APIResponse { - uri := fmt.Sprintf("http://localhost:%d/v1/plugins/%s/%s/%d", port, pluginType, name, version) - client := &http.Client{} - req, err := http.NewRequest("DELETE", uri, nil) - if err != nil { - log.Fatal(err) - } - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - - return getAPIResponse(resp) -} - -func getPluginList(port int) *rbody.APIResponse { - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/v1/plugins", port)) - if err != nil { - log.Fatal(err) - } - return getAPIResponse(resp) -} - -func getMetricCatalog(port int) *rbody.APIResponse { - return fetchMetrics(port, "") -} - -func fetchMetrics(port int, ns string) *rbody.APIResponse { - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/v1/metrics%s", port, ns)) - if err != nil { - log.Fatal(err) - } - - return getAPIResponse(resp) -} - -func fetchMetricsWithVersion(port int, ns string, ver int) *rbody.APIResponse { - resp, err := http.Get(fmt.Sprintf("http://localhost:%d/v1/metrics%s?ver=%d", port, ns, ver)) - if err != nil { - log.Fatal(err) - } - - return getAPIResponse(resp) -} - -func getPluginConfigItem(port int, typ *core.PluginType, name, ver string) *rbody.APIResponse { - var uri string - if typ != nil { - uri = fmt.Sprintf("http://localhost:%d/v1/plugins/%d/%s/%s/config", port, *typ, name, ver) - } else { - uri = fmt.Sprintf("http://localhost:%d/v1/plugins/%s/%s/%s/config", port, "", name, ver) - } - resp, err := http.Get(uri) - if err != nil { - log.Fatal(err) - } - - return getAPIResponse(resp) -} - -func setPluginConfigItem(port int, typ string, name, ver string, cdn *cdata.ConfigDataNode) *rbody.APIResponse { - uri := fmt.Sprintf("http://localhost:%d/v1/plugins/%s/%s/%s/config", port, typ, name, ver) - - client := &http.Client{} - b, err := json.Marshal(cdn) - if err != nil { - log.Fatal(err) - } - req, err := http.NewRequest("PUT", uri, bytes.NewReader(b)) - if err != nil { - log.Fatal(err) - } - req.Header.Add("Content-Type", "application/json") - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - - return getAPIResponse(resp) -} - -func deletePluginConfigItem(port int, typ string, name, ver string, fields []string) *rbody.APIResponse { - uri := fmt.Sprintf("http://localhost:%d/v1/plugins/%s/%s/%s/config", port, typ, name, ver) - - client := &http.Client{} - b, err := json.Marshal(fields) - if err != nil { - log.Fatal(err) - } - req, err := http.NewRequest("DELETE", uri, bytes.NewReader(b)) - if err != nil { - log.Fatal(err) - } - req.Header.Add("Content-Type", "application/json") - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - return getAPIResponse(resp) -} - -// REST API instances that are started are killed when the tests end. -// When we eventually have a REST API Stop command this can be killed. -func startAPI(cfg *mockConfig) *restAPIInstance { - // Start a REST API to talk to - log.SetLevel(LOG_LEVEL) - r, _ := New(cfg.RestAPI) - c := control.New(cfg.Control) - c.Start() - s := scheduler.New(cfg.Scheduler) - s.SetMetricManager(c) - s.Start() - r.BindMetricManager(c) - r.BindTaskManager(s) - r.BindConfigManager(c.Config) - go func(ch <-chan error) { - // Block on the error channel. Will return exit status 1 for an error or just return if the channel closes. - err, ok := <-ch - if !ok { - return - } - log.Fatal(err) - }(r.Err()) - r.SetAddress("127.0.0.1:0") - r.Start() - time.Sleep(time.Millisecond * 100) - return &restAPIInstance{ - port: r.Port(), - server: r, - } -} - -func TestPluginRestCalls(t *testing.T) { - CompressedUpload = false - Convey("REST API functional V1", t, func() { - Convey("Load Plugin - POST - /v1/plugins", func() { - Convey("a single plugin loads", func() { - // This test alone tests gzip. Saves on test time. - CompressedUpload = true - r := startAPI(getDefaultMockConfig()) - port := r.port - col := core.CollectorPluginType - pub := core.PublisherPluginType - Convey("A global plugin config is added for all plugins", func() { - cdn := cdata.NewNode() - cdn.AddItem("password", ctypes.ConfigValueStr{Value: "p@ssw0rd"}) - r := setPluginConfigItem(port, "", "", "", cdn) - So(r.Body, ShouldHaveSameTypeAs, &rbody.SetPluginConfigItem{}) - r1 := r.Body.(*rbody.SetPluginConfigItem) - So(r1.Table()["password"], ShouldResemble, ctypes.ConfigValueStr{Value: "p@ssw0rd"}) - - r2 := getPluginConfigItem(port, &col, "", "") - So(r2.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) - r3 := r2.Body.(*rbody.PluginConfigItem) - So(len(r3.Table()), ShouldEqual, 1) - So(r3.Table()["password"], ShouldResemble, ctypes.ConfigValueStr{Value: "p@ssw0rd"}) - - Convey("A plugin config is added for all publishers", func() { - cdn := cdata.NewNode() - cdn.AddItem("user", ctypes.ConfigValueStr{Value: "john"}) - r := setPluginConfigItem(port, core.PublisherPluginType.String(), "", "", cdn) - So(r.Body, ShouldHaveSameTypeAs, &rbody.SetPluginConfigItem{}) - r1 := r.Body.(*rbody.SetPluginConfigItem) - So(r1.Table()["user"], ShouldResemble, ctypes.ConfigValueStr{Value: "john"}) - So(len(r1.Table()), ShouldEqual, 2) - - Convey("A plugin config is added for all versions of a publisher", func() { - cdn := cdata.NewNode() - cdn.AddItem("path", ctypes.ConfigValueStr{Value: "/usr/local/influxdb/bin"}) - r := setPluginConfigItem(port, "2", "influxdb", "", cdn) - So(r.Body, ShouldHaveSameTypeAs, &rbody.SetPluginConfigItem{}) - r1 := r.Body.(*rbody.SetPluginConfigItem) - So(r1.Table()["path"], ShouldResemble, ctypes.ConfigValueStr{Value: "/usr/local/influxdb/bin"}) - So(len(r1.Table()), ShouldEqual, 3) - - Convey("A plugin config is added for a specific version of a publisher", func() { - cdn := cdata.NewNode() - cdn.AddItem("rate", ctypes.ConfigValueFloat{Value: .8}) - r := setPluginConfigItem(port, core.PublisherPluginType.String(), "influxdb", "1", cdn) - So(r.Body, ShouldHaveSameTypeAs, &rbody.SetPluginConfigItem{}) - r1 := r.Body.(*rbody.SetPluginConfigItem) - So(r1.Table()["rate"], ShouldResemble, ctypes.ConfigValueFloat{Value: .8}) - So(len(r1.Table()), ShouldEqual, 4) - - r2 := getPluginConfigItem(port, &pub, "", "") - So(r2.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) - r3 := r2.Body.(*rbody.PluginConfigItem) - So(len(r3.Table()), ShouldEqual, 2) - - r4 := getPluginConfigItem(port, &pub, "influxdb", "1") - So(r4.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) - r5 := r4.Body.(*rbody.PluginConfigItem) - So(len(r5.Table()), ShouldEqual, 4) - - Convey("A global plugin config field is deleted", func() { - r := deletePluginConfigItem(port, "", "", "", []string{"password"}) - So(r.Body, ShouldHaveSameTypeAs, &rbody.DeletePluginConfigItem{}) - r1 := r.Body.(*rbody.DeletePluginConfigItem) - So(len(r1.Table()), ShouldEqual, 0) - - r2 := setPluginConfigItem(port, core.PublisherPluginType.String(), "influxdb", "", cdn) - So(r2.Body, ShouldHaveSameTypeAs, &rbody.SetPluginConfigItem{}) - r3 := r2.Body.(*rbody.SetPluginConfigItem) - So(len(r3.Table()), ShouldEqual, 3) - }) - }) - }) - }) - }) - - }) - Convey("Plugin config is set at startup", func() { - cfg := getDefaultMockConfig() - err := cfgfile.Read("../../examples/configs/snap-config-sample.json", &cfg, MOCK_CONSTRAINTS) - So(err, ShouldBeNil) - if len(SNAP_AUTODISCOVER_PATH) == 0 { - if len(SNAP_PATH) != 0 { - - SNAP_AUTODISCOVER_PATH = helper.PluginPath() - log.Warning(fmt.Sprintf("SNAP_AUTODISCOVER_PATH has been set to plugin build path (%s). This might cause test failures", SNAP_AUTODISCOVER_PATH)) - } - } else { - log.Warning(fmt.Sprintf("SNAP_AUTODISCOVER_PATH is set to %s. This might cause test failures", SNAP_AUTODISCOVER_PATH)) - } - cfg.Control.AutoDiscoverPath = SNAP_AUTODISCOVER_PATH - r := startAPI(cfg) - port := r.port - col := core.CollectorPluginType - - Convey("Gets the collector config by name and version", func() { - r := getPluginConfigItem(port, &col, "pcm", "1") - So(r.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) - r1 := r.Body.(*rbody.PluginConfigItem) - So(r1.Table()["path"], ShouldResemble, ctypes.ConfigValueStr{Value: "/usr/local/pcm/bin"}) - So(r1.Table()["user"], ShouldResemble, ctypes.ConfigValueStr{Value: "john"}) - So(len(r1.Table()), ShouldEqual, 6) - }) - Convey("Gets the config for a collector by name", func() { - r := getPluginConfigItem(port, &col, "pcm", "") - So(r.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) - r1 := r.Body.(*rbody.PluginConfigItem) - So(r1.Table()["path"], ShouldResemble, ctypes.ConfigValueStr{Value: "/usr/local/pcm/bin"}) - So(r1.Table()["user"], ShouldResemble, ctypes.ConfigValueStr{Value: "jane"}) - So(len(r1.Table()), ShouldEqual, 3) - }) - Convey("Gets the config for all collectors", func() { - r := getPluginConfigItem(port, &col, "", "") - So(r.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) - r1 := r.Body.(*rbody.PluginConfigItem) - So(r1.Table()["user"], ShouldResemble, ctypes.ConfigValueStr{Value: "jane"}) - So(r1.Table()["password"], ShouldResemble, ctypes.ConfigValueStr{Value: "p@ssw0rd"}) - So(len(r1.Table()), ShouldEqual, 2) - }) - Convey("Gets the config for all plugins", func() { - r := getPluginConfigItem(port, nil, "", "") - So(r.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) - r1 := r.Body.(*rbody.PluginConfigItem) - So(r1.Table()["password"], ShouldResemble, ctypes.ConfigValueStr{Value: "p@ssw0rd"}) - So(len(r1.Table()), ShouldEqual, 1) - }) - }) - }) - - Convey("Enable task - put - /v1/tasks/:id/enable", func() { - Convey("Enable a running task", func(c C) { - r := startAPI(getDefaultMockConfig()) - port := r.port - - uploadPlugin(MOCK_PLUGIN_PATH2, port) - uploadPlugin(FILE_PLUGIN_PATH, port) - - r1 := createTask("1.json", "yeti", "1s", true, port) - So(r1.Body, ShouldHaveSameTypeAs, new(rbody.AddScheduledTask)) - plr1 := r1.Body.(*rbody.AddScheduledTask) - - id := plr1.ID - - r2 := startTask(id, port) - So(r2.Body, ShouldHaveSameTypeAs, new(rbody.ScheduledTaskStarted)) - plr2 := r2.Body.(*rbody.ScheduledTaskStarted) - So(plr2.ID, ShouldEqual, id) - - r4 := enableTask(id, port) - So(r4.Body, ShouldHaveSameTypeAs, new(rbody.Error)) - plr4 := r4.Body.(*rbody.Error) - So(plr4.ErrorMessage, ShouldEqual, "Task must be disabled") - }) - }) - }) -} diff --git a/mgmt/rest/config_test.go b/mgmt/rest/rest_test.go similarity index 57% rename from mgmt/rest/config_test.go rename to mgmt/rest/rest_test.go index 8e40d6ab2..fef07d928 100644 --- a/mgmt/rest/config_test.go +++ b/mgmt/rest/rest_test.go @@ -1,9 +1,10 @@ -// +build legacy small medium large +// +build medium /* http://www.apache.org/licenses/LICENSE-2.0.txt -Copyright 2016 Intel Corporation + +Copyright 2015 Intel Corporation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,10 +22,33 @@ limitations under the License. package rest import ( + "io/ioutil" + "net/http" + "os" + + log "github.com/Sirupsen/logrus" "github.com/intelsdi-x/snap/control" + "github.com/intelsdi-x/snap/plugin/helper" "github.com/intelsdi-x/snap/scheduler" ) +// common ressources used for medium tests + +var ( + // Switching this turns on logging for all the REST API calls + LOG_LEVEL = log.WarnLevel + + SNAP_PATH = helper.BuildPath + SNAP_AUTODISCOVER_PATH = os.Getenv("SNAP_AUTODISCOVER_PATH") + MOCK_PLUGIN_PATH1 = helper.PluginFilePath("snap-plugin-collector-mock1") + MOCK_PLUGIN_PATH2 = helper.PluginFilePath("snap-plugin-collector-mock2") + FILE_PLUGIN_PATH = helper.PluginFilePath("snap-plugin-publisher-mock-file") + + CompressedUpload = true + TotalUploadSize = 0 + UploadCount = 0 +) + // Since we do not have a global snap package that could be imported // we create a mock config struct to mock what is in snapteld.go @@ -47,3 +71,21 @@ func getDefaultMockConfig() *mockConfig { RestAPI: GetDefaultConfig(), } } + +type restAPIInstance struct { + port int + server *Server +} + +func command() string { + return "curl" +} + +func readBody(r *http.Response) []byte { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Fatal(err) + } + r.Body.Close() + return b +} diff --git a/mgmt/rest/rest_v1_test.go b/mgmt/rest/rest_v1_test.go index 30a9e9fc8..6d01b55d2 100644 --- a/mgmt/rest/rest_v1_test.go +++ b/mgmt/rest/rest_v1_test.go @@ -21,9 +21,12 @@ limitations under the License. package rest +// This test runs through basic REST API calls and validates them. + import ( "bufio" "bytes" + "compress/gzip" "encoding/json" "fmt" "io" @@ -32,26 +35,557 @@ import ( "net/http" "net/url" "os" + "path/filepath" "strings" "testing" - - . "github.com/smartystreets/goconvey/convey" + "time" log "github.com/Sirupsen/logrus" + + "github.com/intelsdi-x/snap/control" + "github.com/intelsdi-x/snap/core" "github.com/intelsdi-x/snap/core/cdata" "github.com/intelsdi-x/snap/core/ctypes" - "github.com/intelsdi-x/snap/mgmt/rest/fixtures" + "github.com/intelsdi-x/snap/mgmt/rest/v1/fixtures" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" + "github.com/intelsdi-x/snap/pkg/cfgfile" "github.com/intelsdi-x/snap/plugin/helper" + "github.com/intelsdi-x/snap/scheduler" + "github.com/intelsdi-x/snap/scheduler/wmap" + . "github.com/smartystreets/goconvey/convey" ) -var ( - LOG_LEVEL = log.WarnLevel - MOCK_PLUGIN_PATH1 = helper.PluginFilePath("snap-plugin-collector-mock1") -) +func getAPIResponse(resp *http.Response) *rbody.APIResponse { + r := new(rbody.APIResponse) + rb := readBody(resp) + err := json.Unmarshal(rb, r) + if err != nil { + log.Fatal(err) + } + r.JSONResponse = string(rb) + return r +} + +func getStreamingAPIResponse(resp *http.Response) *rbody.APIResponse { + r := new(rbody.APIResponse) + rb := readBody(resp) + err := json.Unmarshal(rb, r) + if err != nil { + log.Fatal(err) + } + r.JSONResponse = string(rb) + return r +} + +type watchTaskResult struct { + eventChan chan string + doneChan chan struct{} + killChan chan struct{} +} + +func (w *watchTaskResult) close() { + close(w.doneChan) +} + +func watchTask(id string, port int) *watchTaskResult { + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/v1/tasks/%s/watch", port, id)) + if err != nil { + log.Fatal(err) + } + + r := &watchTaskResult{ + eventChan: make(chan string), + doneChan: make(chan struct{}), + killChan: make(chan struct{}), + } + go func() { + reader := bufio.NewReader(resp.Body) + for { + select { + case <-r.doneChan: + resp.Body.Close() + return + default: + line, _ := reader.ReadBytes('\n') + ste := &rbody.StreamedTaskEvent{} + err := json.Unmarshal(line, ste) + if err != nil { + log.Fatal(err) + r.close() + return + } + switch ste.EventType { + case rbody.TaskWatchTaskDisabled: + r.eventChan <- ste.EventType + r.close() + return + case rbody.TaskWatchTaskStopped, rbody.TaskWatchTaskStarted, rbody.TaskWatchMetricEvent: + log.Info(ste.EventType) + r.eventChan <- ste.EventType + } + } + } + }() + return r +} + +func getTasks(port int) *rbody.APIResponse { + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/v1/tasks", port)) + if err != nil { + log.Fatal(err) + } + return getAPIResponse(resp) +} + +func getTask(id string, port int) *rbody.APIResponse { + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/v1/tasks/%s", port, id)) + if err != nil { + log.Fatal(err) + } + return getAPIResponse(resp) +} + +func startTask(id string, port int) *rbody.APIResponse { + uri := fmt.Sprintf("http://localhost:%d/v1/tasks/%s/start", port, id) + client := &http.Client{} + b := bytes.NewReader([]byte{}) + req, err := http.NewRequest("PUT", uri, b) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + return getAPIResponse(resp) +} + +func stopTask(id string, port int) *rbody.APIResponse { + uri := fmt.Sprintf("http://localhost:%d/v1/tasks/%s/stop", port, id) + client := &http.Client{} + b := bytes.NewReader([]byte{}) + req, err := http.NewRequest("PUT", uri, b) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + return getAPIResponse(resp) +} + +func removeTask(id string, port int) *rbody.APIResponse { + uri := fmt.Sprintf("http://localhost:%d/v1/tasks/%s", port, id) + client := &http.Client{} + req, err := http.NewRequest("DELETE", uri, nil) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + return getAPIResponse(resp) +} + +func createTask(sample, name, interval string, noStart bool, port int) *rbody.APIResponse { + jsonP, err := ioutil.ReadFile("./wmap_sample/" + sample) + if err != nil { + log.Fatal(err) + } + wf, err := wmap.FromJson(jsonP) + if err != nil { + log.Fatal(err) + } + + uri := fmt.Sprintf("http://localhost:%d/v1/tasks", port) + + t := core.TaskCreationRequest{ + Schedule: &core.Schedule{Type: "simple", Interval: interval}, + Workflow: wf, + Name: name, + Start: !noStart, + } + // Marshal to JSON for request body + j, err := json.Marshal(t) + if err != nil { + log.Fatal(err) + } + + client := &http.Client{} + b := bytes.NewReader(j) + req, err := http.NewRequest("POST", uri, b) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + return getAPIResponse(resp) +} + +func enableTask(id string, port int) *rbody.APIResponse { + uri := fmt.Sprintf("http://localhost:%d/v1/tasks/%s/enable", port, id) + client := &http.Client{} + b := bytes.NewReader([]byte{}) + req, err := http.NewRequest("PUT", uri, b) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + return getAPIResponse(resp) +} + +func uploadPlugin(pluginPath string, port int) *rbody.APIResponse { + uri := fmt.Sprintf("http://localhost:%d/v1/plugins", port) + + client := &http.Client{} + file, err := os.Open(pluginPath) + if err != nil { + log.Fatal(err) + } + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + var part io.Writer + part, err = writer.CreateFormFile("snap-plugins", filepath.Base(pluginPath)) + if err != nil { + log.Fatal(err) + } + if CompressedUpload { + cpart := gzip.NewWriter(part) + _, err = io.Copy(cpart, file) + if err != nil { + log.Fatal(err) + } + err = cpart.Close() + } else { + _, err = io.Copy(part, file) + } + if err != nil { + log.Fatal(err) + } + err = writer.Close() + if err != nil { + log.Fatal(err) + } + TotalUploadSize += body.Len() + UploadCount += 1 + req, err := http.NewRequest("POST", uri, body) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", writer.FormDataContentType()) + if CompressedUpload { + req.Header.Add("Plugin-Compression", "gzip") + } + file.Close() + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + return getAPIResponse(resp) +} + +func unloadPlugin(port int, pluginType string, name string, version int) *rbody.APIResponse { + uri := fmt.Sprintf("http://localhost:%d/v1/plugins/%s/%s/%d", port, pluginType, name, version) + client := &http.Client{} + req, err := http.NewRequest("DELETE", uri, nil) + if err != nil { + log.Fatal(err) + } + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + + return getAPIResponse(resp) +} -type restAPIInstance struct { - port int - server *Server +func getPluginList(port int) *rbody.APIResponse { + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/v1/plugins", port)) + if err != nil { + log.Fatal(err) + } + return getAPIResponse(resp) +} + +func getMetricCatalog(port int) *rbody.APIResponse { + return fetchMetrics(port, "") +} + +func fetchMetrics(port int, ns string) *rbody.APIResponse { + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/v1/metrics%s", port, ns)) + if err != nil { + log.Fatal(err) + } + + return getAPIResponse(resp) +} + +func fetchMetricsWithVersion(port int, ns string, ver int) *rbody.APIResponse { + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/v1/metrics%s?ver=%d", port, ns, ver)) + if err != nil { + log.Fatal(err) + } + + return getAPIResponse(resp) +} + +func getPluginConfigItem(port int, typ *core.PluginType, name, ver string) *rbody.APIResponse { + var uri string + if typ != nil { + uri = fmt.Sprintf("http://localhost:%d/v1/plugins/%d/%s/%s/config", port, *typ, name, ver) + } else { + uri = fmt.Sprintf("http://localhost:%d/v1/plugins/%s/%s/%s/config", port, "", name, ver) + } + resp, err := http.Get(uri) + if err != nil { + log.Fatal(err) + } + + return getAPIResponse(resp) +} + +func setPluginConfigItem(port int, typ string, name, ver string, cdn *cdata.ConfigDataNode) *rbody.APIResponse { + uri := fmt.Sprintf("http://localhost:%d/v1/plugins/%s/%s/%s/config", port, typ, name, ver) + + client := &http.Client{} + b, err := json.Marshal(cdn) + if err != nil { + log.Fatal(err) + } + req, err := http.NewRequest("PUT", uri, bytes.NewReader(b)) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + + return getAPIResponse(resp) +} + +func deletePluginConfigItem(port int, typ string, name, ver string, fields []string) *rbody.APIResponse { + uri := fmt.Sprintf("http://localhost:%d/v1/plugins/%s/%s/%s/config", port, typ, name, ver) + + client := &http.Client{} + b, err := json.Marshal(fields) + if err != nil { + log.Fatal(err) + } + req, err := http.NewRequest("DELETE", uri, bytes.NewReader(b)) + if err != nil { + log.Fatal(err) + } + req.Header.Add("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + return getAPIResponse(resp) +} + +// REST API instances that are started are killed when the tests end. +// When we eventually have a REST API Stop command this can be killed. +func startAPI(cfg *mockConfig) *restAPIInstance { + // Start a REST API to talk to + log.SetLevel(LOG_LEVEL) + r, _ := New(cfg.RestAPI) + c := control.New(cfg.Control) + c.Start() + s := scheduler.New(cfg.Scheduler) + s.SetMetricManager(c) + s.Start() + r.BindMetricManager(c) + r.BindTaskManager(s) + r.BindConfigManager(c.Config) + go func(ch <-chan error) { + // Block on the error channel. Will return exit status 1 for an error or just return if the channel closes. + err, ok := <-ch + if !ok { + return + } + log.Fatal(err) + }(r.Err()) + r.SetAddress("127.0.0.1:0") + r.Start() + time.Sleep(time.Millisecond * 100) + return &restAPIInstance{ + port: r.Port(), + server: r, + } +} + +func TestPluginRestCalls(t *testing.T) { + CompressedUpload = false + Convey("REST API functional V1", t, func() { + Convey("Load Plugin - POST - /v1/plugins", func() { + Convey("a single plugin loads", func() { + // This test alone tests gzip. Saves on test time. + CompressedUpload = true + r := startAPI(getDefaultMockConfig()) + port := r.port + col := core.CollectorPluginType + pub := core.PublisherPluginType + Convey("A global plugin config is added for all plugins", func() { + cdn := cdata.NewNode() + cdn.AddItem("password", ctypes.ConfigValueStr{Value: "p@ssw0rd"}) + r := setPluginConfigItem(port, "", "", "", cdn) + So(r.Body, ShouldHaveSameTypeAs, &rbody.SetPluginConfigItem{}) + r1 := r.Body.(*rbody.SetPluginConfigItem) + So(r1.Table()["password"], ShouldResemble, ctypes.ConfigValueStr{Value: "p@ssw0rd"}) + + r2 := getPluginConfigItem(port, &col, "", "") + So(r2.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) + r3 := r2.Body.(*rbody.PluginConfigItem) + So(len(r3.Table()), ShouldEqual, 1) + So(r3.Table()["password"], ShouldResemble, ctypes.ConfigValueStr{Value: "p@ssw0rd"}) + + Convey("A plugin config is added for all publishers", func() { + cdn := cdata.NewNode() + cdn.AddItem("user", ctypes.ConfigValueStr{Value: "john"}) + r := setPluginConfigItem(port, core.PublisherPluginType.String(), "", "", cdn) + So(r.Body, ShouldHaveSameTypeAs, &rbody.SetPluginConfigItem{}) + r1 := r.Body.(*rbody.SetPluginConfigItem) + So(r1.Table()["user"], ShouldResemble, ctypes.ConfigValueStr{Value: "john"}) + So(len(r1.Table()), ShouldEqual, 2) + + Convey("A plugin config is added for all versions of a publisher", func() { + cdn := cdata.NewNode() + cdn.AddItem("path", ctypes.ConfigValueStr{Value: "/usr/local/influxdb/bin"}) + r := setPluginConfigItem(port, "2", "influxdb", "", cdn) + So(r.Body, ShouldHaveSameTypeAs, &rbody.SetPluginConfigItem{}) + r1 := r.Body.(*rbody.SetPluginConfigItem) + So(r1.Table()["path"], ShouldResemble, ctypes.ConfigValueStr{Value: "/usr/local/influxdb/bin"}) + So(len(r1.Table()), ShouldEqual, 3) + + Convey("A plugin config is added for a specific version of a publisher", func() { + cdn := cdata.NewNode() + cdn.AddItem("rate", ctypes.ConfigValueFloat{Value: .8}) + r := setPluginConfigItem(port, core.PublisherPluginType.String(), "influxdb", "1", cdn) + So(r.Body, ShouldHaveSameTypeAs, &rbody.SetPluginConfigItem{}) + r1 := r.Body.(*rbody.SetPluginConfigItem) + So(r1.Table()["rate"], ShouldResemble, ctypes.ConfigValueFloat{Value: .8}) + So(len(r1.Table()), ShouldEqual, 4) + + r2 := getPluginConfigItem(port, &pub, "", "") + So(r2.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) + r3 := r2.Body.(*rbody.PluginConfigItem) + So(len(r3.Table()), ShouldEqual, 2) + + r4 := getPluginConfigItem(port, &pub, "influxdb", "1") + So(r4.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) + r5 := r4.Body.(*rbody.PluginConfigItem) + So(len(r5.Table()), ShouldEqual, 4) + + Convey("A global plugin config field is deleted", func() { + r := deletePluginConfigItem(port, "", "", "", []string{"password"}) + So(r.Body, ShouldHaveSameTypeAs, &rbody.DeletePluginConfigItem{}) + r1 := r.Body.(*rbody.DeletePluginConfigItem) + So(len(r1.Table()), ShouldEqual, 0) + + r2 := setPluginConfigItem(port, core.PublisherPluginType.String(), "influxdb", "", cdn) + So(r2.Body, ShouldHaveSameTypeAs, &rbody.SetPluginConfigItem{}) + r3 := r2.Body.(*rbody.SetPluginConfigItem) + So(len(r3.Table()), ShouldEqual, 3) + }) + }) + }) + }) + }) + + }) + Convey("Plugin config is set at startup", func() { + cfg := getDefaultMockConfig() + err := cfgfile.Read("../../examples/configs/snap-config-sample.json", &cfg, MOCK_CONSTRAINTS) + So(err, ShouldBeNil) + if len(SNAP_AUTODISCOVER_PATH) == 0 { + if len(SNAP_PATH) != 0 { + + SNAP_AUTODISCOVER_PATH = helper.PluginPath() + log.Warning(fmt.Sprintf("SNAP_AUTODISCOVER_PATH has been set to plugin build path (%s). This might cause test failures", SNAP_AUTODISCOVER_PATH)) + } + } else { + log.Warning(fmt.Sprintf("SNAP_AUTODISCOVER_PATH is set to %s. This might cause test failures", SNAP_AUTODISCOVER_PATH)) + } + cfg.Control.AutoDiscoverPath = SNAP_AUTODISCOVER_PATH + r := startAPI(cfg) + port := r.port + col := core.CollectorPluginType + + Convey("Gets the collector config by name and version", func() { + r := getPluginConfigItem(port, &col, "pcm", "1") + So(r.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) + r1 := r.Body.(*rbody.PluginConfigItem) + So(r1.Table()["path"], ShouldResemble, ctypes.ConfigValueStr{Value: "/usr/local/pcm/bin"}) + So(r1.Table()["user"], ShouldResemble, ctypes.ConfigValueStr{Value: "john"}) + So(len(r1.Table()), ShouldEqual, 6) + }) + Convey("Gets the config for a collector by name", func() { + r := getPluginConfigItem(port, &col, "pcm", "") + So(r.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) + r1 := r.Body.(*rbody.PluginConfigItem) + So(r1.Table()["path"], ShouldResemble, ctypes.ConfigValueStr{Value: "/usr/local/pcm/bin"}) + So(r1.Table()["user"], ShouldResemble, ctypes.ConfigValueStr{Value: "jane"}) + So(len(r1.Table()), ShouldEqual, 3) + }) + Convey("Gets the config for all collectors", func() { + r := getPluginConfigItem(port, &col, "", "") + So(r.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) + r1 := r.Body.(*rbody.PluginConfigItem) + So(r1.Table()["user"], ShouldResemble, ctypes.ConfigValueStr{Value: "jane"}) + So(r1.Table()["password"], ShouldResemble, ctypes.ConfigValueStr{Value: "p@ssw0rd"}) + So(len(r1.Table()), ShouldEqual, 2) + }) + Convey("Gets the config for all plugins", func() { + r := getPluginConfigItem(port, nil, "", "") + So(r.Body, ShouldHaveSameTypeAs, &rbody.PluginConfigItem{}) + r1 := r.Body.(*rbody.PluginConfigItem) + So(r1.Table()["password"], ShouldResemble, ctypes.ConfigValueStr{Value: "p@ssw0rd"}) + So(len(r1.Table()), ShouldEqual, 1) + }) + }) + }) + + Convey("Enable task - put - /v1/tasks/:id/enable", func() { + Convey("Enable a running task", func(c C) { + r := startAPI(getDefaultMockConfig()) + port := r.port + + uploadPlugin(MOCK_PLUGIN_PATH2, port) + uploadPlugin(FILE_PLUGIN_PATH, port) + + r1 := createTask("1.json", "yeti", "1s", true, port) + So(r1.Body, ShouldHaveSameTypeAs, new(rbody.AddScheduledTask)) + plr1 := r1.Body.(*rbody.AddScheduledTask) + + id := plr1.ID + + r2 := startTask(id, port) + So(r2.Body, ShouldHaveSameTypeAs, new(rbody.ScheduledTaskStarted)) + plr2 := r2.Body.(*rbody.ScheduledTaskStarted) + So(plr2.ID, ShouldEqual, id) + + r4 := enableTask(id, port) + So(r4.Body, ShouldHaveSameTypeAs, new(rbody.Error)) + plr4 := r4.Body.(*rbody.Error) + So(plr4.ErrorMessage, ShouldEqual, "Task must be disabled") + }) + }) + }) } func startV1API(cfg *mockConfig, testType string) *restAPIInstance { @@ -73,7 +607,6 @@ func startV1API(cfg *mockConfig, testType string) *restAPIInstance { mockTaskManager := &fixtures.MockTaskManager{} r.BindTaskManager(mockTaskManager) } - go func(ch <-chan error) { // Block on the error channel. Will return exit status 1 for an error or // just return if the channel closes. diff --git a/mgmt/rest/rest_v2_test.go b/mgmt/rest/rest_v2_test.go new file mode 100644 index 000000000..b3b3d935e --- /dev/null +++ b/mgmt/rest/rest_v2_test.go @@ -0,0 +1,463 @@ +// +build medium + +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2015 Intel Corporation + +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 rest + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/url" + "os" + "strings" + "testing" + + log "github.com/Sirupsen/logrus" + "github.com/intelsdi-x/snap/core/cdata" + "github.com/intelsdi-x/snap/core/ctypes" + "github.com/intelsdi-x/snap/mgmt/rest/v2/mock" + . "github.com/smartystreets/goconvey/convey" +) + +func startV2API(cfg *mockConfig, testType string) *restAPIInstance { + log.SetLevel(LOG_LEVEL) + r, _ := New(cfg.RestAPI) + switch testType { + case "plugin": + mockMetricManager := &mock.MockManagesMetrics{} + mockConfigManager := &mock.MockConfigManager{} + r.BindMetricManager(mockMetricManager) + r.BindConfigManager(mockConfigManager) + case "metric": + mockMetricManager := &mock.MockManagesMetrics{} + r.BindMetricManager(mockMetricManager) + case "task": + mockTaskManager := &mock.MockTaskManager{} + r.BindTaskManager(mockTaskManager) + } + go func(ch <-chan error) { + // Block on the error channel. Will return exit status 1 for an error or + // just return if the channel closes. + err, ok := <-ch + if !ok { + return + } + log.Fatal(err) + }(r.Err()) + r.SetAddress("127.0.0.1:0") + r.Start() + return &restAPIInstance{ + port: r.Port(), + server: r, + } +} + +func TestV2Plugin(t *testing.T) { + r := startV2API(getDefaultMockConfig(), "plugin") + Convey("Test Plugin REST API V2", t, func() { + + Convey("Post plugins - v2/plugins/:type:name", func(c C) { + f, err := os.Open(MOCK_PLUGIN_PATH1) + defer f.Close() + So(err, ShouldBeNil) + + // We create a pipe so that we can write the file in multipart + // format and read it in to the body of the http request + reader, writer := io.Pipe() + mwriter := multipart.NewWriter(writer) + bufin := bufio.NewReader(f) + + // A go routine is needed since we must write the multipart file + // to the pipe so we can read from it in the http call + go func() { + part, err := mwriter.CreateFormFile("snap-plugins", "mock") + c.So(err, ShouldBeNil) + bufin.WriteTo(part) + mwriter.Close() + writer.Close() + }() + + resp1, err1 := http.Post( + fmt.Sprintf("http://localhost:%d/v2/plugins", r.port), + mwriter.FormDataContentType(), reader) + So(err1, ShouldBeNil) + So(resp1.StatusCode, ShouldEqual, 201) + }) + + Convey("Get plugins - v2/plugins", func() { + resp, err := http.Get( + fmt.Sprintf("http://localhost:%d/v2/plugins", r.port)) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, 200) + body, err := ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + string(body), + ShouldResemble, + fmt.Sprintf(mock.GET_PLUGINS_RESPONSE, r.port, r.port, + r.port, r.port, r.port, r.port)) + }) + Convey("Get plugins - v2/plugins/:type", func() { + c := &http.Client{} + req, err := http.NewRequest("GET", + fmt.Sprintf("http://localhost:%d/v2/plugins", r.port), + bytes.NewReader([]byte{})) + So(err, ShouldBeNil) + q := req.URL.Query() + q.Add("type", "collector") + req.URL.RawQuery = q.Encode() + resp, err := c.Do(req) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, 200) + body, err := ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + string(body), + ShouldResemble, + fmt.Sprintf(mock.GET_PLUGINS_RESPONSE_TYPE, r.port, r.port)) + }) + Convey("Get plugins - v2/plugins/:type:name", func() { + c := &http.Client{} + req, err := http.NewRequest("GET", + fmt.Sprintf("http://localhost:%d/v2/plugins", r.port), + bytes.NewReader([]byte{})) + So(err, ShouldBeNil) + q := req.URL.Query() + q.Add("type", "publisher") + q.Add("name", "bar") + req.URL.RawQuery = q.Encode() + resp, err := c.Do(req) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, 200) + body, err := ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + string(body), + ShouldResemble, + fmt.Sprintf(mock.GET_PLUGINS_RESPONSE_TYPE_NAME, r.port)) + }) + Convey("Get plugin - v2/plugins/:type:name:version", func() { + resp, err := http.Get( + fmt.Sprintf("http://localhost:%d/v2/plugins/publisher/bar/3", r.port)) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, 200) + body, err := ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + string(body), + ShouldResemble, + fmt.Sprintf(mock.GET_PLUGINS_RESPONSE_TYPE_NAME_VERSION, r.port)) + }) + + Convey("Delete plugins - v2/plugins/:type:name:version", func() { + c := &http.Client{} + pluginName := "foo" + pluginType := "collector" + pluginVersion := 2 + req, err := http.NewRequest( + "DELETE", + fmt.Sprintf("http://localhost:%d/v2/plugins/%s/%s/%d", + r.port, + pluginType, + pluginName, + pluginVersion), + bytes.NewReader([]byte{})) + So(err, ShouldBeNil) + resp, err := c.Do(req) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusNoContent) + body, err := ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + string(body), + ShouldResemble, + fmt.Sprintf(mock.UNLOAD_PLUGIN_RESPONSE)) + }) + + Convey("Get plugin config items - v2/plugins/:type/:name/:version/config", func() { + resp, err := http.Get( + fmt.Sprintf("http://localhost:%d/v2/plugins/publisher/bar/3/config", r.port)) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, 200) + body, err := ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + string(body), + ShouldResemble, + fmt.Sprintf(mock.GET_PLUGIN_CONFIG_ITEM)) + }) + + Convey("Set plugin config item- v2/plugins/:type/:name/:version/config", func() { + c := &http.Client{} + pluginName := "foo" + pluginType := "collector" + pluginVersion := 2 + cd := cdata.NewNode() + cd.AddItem("user", ctypes.ConfigValueStr{Value: "Jane"}) + body, err := cd.MarshalJSON() + So(err, ShouldBeNil) + + req, err := http.NewRequest( + "PUT", + fmt.Sprintf("http://localhost:%d/v2/plugins/%s/%s/%d/config", + r.port, + pluginType, + pluginName, + pluginVersion), + bytes.NewReader(body)) + So(err, ShouldBeNil) + resp, err := c.Do(req) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + body, err = ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + string(body), + ShouldResemble, + fmt.Sprintf(mock.SET_PLUGIN_CONFIG_ITEM)) + + }) + + Convey("Delete plugin config item - /v2/plugins/:type/:name/:version/config", func() { + c := &http.Client{} + pluginName := "foo" + pluginType := "collector" + pluginVersion := 2 + cd := []string{"foo"} + body, err := json.Marshal(cd) + So(err, ShouldBeNil) + req, err := http.NewRequest( + "DELETE", + fmt.Sprintf("http://localhost:%d/v2/plugins/%s/%s/%d/config", + r.port, + pluginType, + pluginName, + pluginVersion), + bytes.NewReader(body)) + + So(err, ShouldBeNil) + resp, err := c.Do(req) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + body, err = ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + string(body), + ShouldResemble, + fmt.Sprintf(mock.DELETE_PLUGIN_CONFIG_ITEM)) + }) + }) +} + +func TestV2Task(t *testing.T) { + r := startV2API(getDefaultMockConfig(), "task") + Convey("Test Task REST API V2", t, func() { + + Convey("Add tasks - v2/tasks", func() { + reader := strings.NewReader(mock.TASK) + resp, err := http.Post( + fmt.Sprintf("http://localhost:%d/v2/tasks", r.port), + http.DetectContentType([]byte(mock.TASK)), + reader) + So(err, ShouldBeNil) + So(resp, ShouldNotBeEmpty) + So(resp.StatusCode, ShouldEqual, 201) + body, err := ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + fmt.Sprintf(mock.ADD_TASK_RESPONSE, r.port), + ShouldResemble, + string(body)) + }) + + Convey("Get tasks - v2/tasks", func() { + resp, err := http.Get( + fmt.Sprintf("http://localhost:%d/v2/tasks", r.port)) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, 200) + body, err := ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + responses := []string{ + fmt.Sprintf(mock.GET_TASKS_RESPONSE, r.port, r.port), + fmt.Sprintf(mock.GET_TASKS_RESPONSE2, r.port, r.port), + } + // GetTasks returns an unordered map, + // thus there is more than one possible response + So( + responses, + ShouldContain, + string(body)) + }) + + Convey("Get task - v2/tasks/:id", func() { + taskID := "1234" + resp, err := http.Get( + fmt.Sprintf("http://localhost:%d/v2/tasks/:%s", r.port, taskID)) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, 200) + body, err := ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + fmt.Sprintf(mock.GET_TASK_RESPONSE, r.port), + ShouldResemble, + string(body)) + }) + + Convey("Watch tasks - v2/tasks/:id/watch", func() { + taskID := "1234" + resp, err := http.Get( + fmt.Sprintf("http://localhost:%d/v2/tasks/:%s/watch", r.port, taskID)) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("Start tasks - v2/tasks/:id", func() { + c := &http.Client{} + taskID := "MockTask1234" + cd := cdata.NewNode() + cd.AddItem("user", ctypes.ConfigValueStr{Value: "Kelly"}) + body, err := cd.MarshalJSON() + So(err, ShouldBeNil) + + req, err := http.NewRequest( + "PUT", + fmt.Sprintf("http://localhost:%d/v2/tasks/%s", r.port, taskID), + bytes.NewReader(body)) + So(err, ShouldBeNil) + q := req.URL.Query() + q.Add("action", "start") + req.URL.RawQuery = q.Encode() + resp, err := c.Do(req) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusNoContent) + body, err = ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + string(body), + ShouldResemble, + fmt.Sprintf(mock.START_TASK_RESPONSE_ID_START)) + }) + + Convey("Stop tasks - v2/tasks/:id", func() { + c := &http.Client{} + taskID := "MockTask1234" + cd := cdata.NewNode() + cd.AddItem("user", ctypes.ConfigValueStr{Value: "Kelly"}) + body, err := cd.MarshalJSON() + So(err, ShouldBeNil) + + req, err := http.NewRequest( + "PUT", + fmt.Sprintf("http://localhost:%d/v2/tasks/%s", r.port, taskID), + bytes.NewReader(body)) + So(err, ShouldBeNil) + q := req.URL.Query() + q.Add("action", "stop") + req.URL.RawQuery = q.Encode() + resp, err := c.Do(req) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusNoContent) + body, err = ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + string(body), + ShouldResemble, + fmt.Sprintf(mock.STOP_TASK_RESPONSE_ID_STOP)) + }) + + Convey("Enable tasks - v2/tasks/:id", func() { + c := &http.Client{} + taskID := "MockTask1234" + cd := cdata.NewNode() + cd.AddItem("user", ctypes.ConfigValueStr{Value: "Kelly"}) + body, err := cd.MarshalJSON() + So(err, ShouldBeNil) + + req, err := http.NewRequest( + "PUT", + fmt.Sprintf("http://localhost:%d/v2/tasks/%s", r.port, taskID), + bytes.NewReader(body)) + So(err, ShouldBeNil) + q := req.URL.Query() + q.Add("action", "enable") + req.URL.RawQuery = q.Encode() + resp, err := c.Do(req) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusNoContent) + body, err = ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + string(body), + ShouldResemble, + fmt.Sprintf(mock.ENABLE_TASK_RESPONSE_ID_ENABLE)) + }) + + Convey("Remove tasks - v2/tasks/:id", func() { + c := &http.Client{} + taskID := "MockTask1234" + cd := []string{"foo"} + body, err := json.Marshal(cd) + So(err, ShouldBeNil) + req, err := http.NewRequest( + "DELETE", + fmt.Sprintf("http://localhost:%d/v2/tasks/%s", + r.port, + taskID), + bytes.NewReader([]byte{})) + So(err, ShouldBeNil) + resp, err := c.Do(req) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, http.StatusNoContent) + body, err = ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + So( + string(body), + ShouldResemble, + fmt.Sprintf(mock.REMOVE_TASK_RESPONSE_ID)) + }) + }) +} + +func TestV2Metric(t *testing.T) { + r := startV2API(getDefaultMockConfig(), "metric") + Convey("Test Metric REST API V2", t, func() { + + Convey("Get metrics - v2/metrics", func() { + resp, err := http.Get( + fmt.Sprintf("http://localhost:%d/v2/metrics", r.port)) + So(err, ShouldBeNil) + So(resp.StatusCode, ShouldEqual, 200) + body, err := ioutil.ReadAll(resp.Body) + So(err, ShouldBeNil) + resp1, err := url.QueryUnescape(string(body)) + So(err, ShouldBeNil) + So( + resp1, + ShouldResemble, + fmt.Sprintf(mock.GET_METRICS_RESPONSE, r.port)) + }) + }) +} diff --git a/mgmt/rest/server.go b/mgmt/rest/server.go index e46a3eb32..326238a78 100644 --- a/mgmt/rest/server.go +++ b/mgmt/rest/server.go @@ -20,15 +20,11 @@ limitations under the License. package rest import ( - "bytes" "crypto/tls" - "encoding/json" "errors" "fmt" "net" "net/http" - "net/http/pprof" - "strings" "sync" "time" @@ -36,32 +32,9 @@ import ( "github.com/julienschmidt/httprouter" "github.com/urfave/negroni" - "github.com/intelsdi-x/snap/core" - "github.com/intelsdi-x/snap/core/cdata" - "github.com/intelsdi-x/snap/core/serror" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" - "github.com/intelsdi-x/snap/mgmt/tribe/agreement" - cschedule "github.com/intelsdi-x/snap/pkg/schedule" - "github.com/intelsdi-x/snap/pkg/stringutils" - "github.com/intelsdi-x/snap/scheduler/wmap" -) - -const ( - APIVersion = 1 -) - -// default configuration values -const ( - defaultEnable bool = true - defaultPort int = 8181 - defaultAddress string = "" - defaultHTTPS bool = false - defaultRestCertificate string = "" - defaultRestKey string = "" - defaultAuth bool = false - defaultAuthPassword string = "" - defaultPortSetByConfig bool = false - defaultPprof bool = false + "github.com/intelsdi-x/snap/mgmt/rest/api" + "github.com/intelsdi-x/snap/mgmt/rest/v1" + "github.com/intelsdi-x/snap/mgmt/rest/v2" ) var ( @@ -71,111 +44,8 @@ var ( protocolPrefix = "http" ) -// holds the configuration passed in through the SNAP config file -// Note: if this struct is modified, then the switch statement in the -// UnmarshalJSON method in this same file needs to be modified to -// match the field mapping that is defined here -type Config struct { - Enable bool `json:"enable"yaml:"enable"` - Port int `json:"port"yaml:"port"` - Address string `json:"addr"yaml:"addr"` - HTTPS bool `json:"https"yaml:"https"` - RestCertificate string `json:"rest_certificate"yaml:"rest_certificate"` - RestKey string `json:"rest_key"yaml:"rest_key"` - RestAuth bool `json:"rest_auth"yaml:"rest_auth"` - RestAuthPassword string `json:"rest_auth_password"yaml:"rest_auth_password"` - portSetByConfig bool `` - Pprof bool `json:"pprof"yaml:"pprof"` -} - -const ( - CONFIG_CONSTRAINTS = ` - "restapi" : { - "type": ["object", "null"], - "properties" : { - "enable": { - "type": "boolean" - }, - "https" : { - "type": "boolean" - }, - "rest_auth": { - "type": "boolean" - }, - "rest_auth_password": { - "type": "string" - }, - "rest_certificate": { - "type": "string" - }, - "rest_key" : { - "type": "string" - }, - "port" : { - "type": "integer", - "minimum": 1, - "maximum": 65535 - }, - "addr" : { - "type": "string" - }, - "pprof": { - "type": "boolean" - } - }, - "additionalProperties": false - } - ` -) - -type managesMetrics interface { - MetricCatalog() ([]core.CatalogedMetric, error) - FetchMetrics(core.Namespace, int) ([]core.CatalogedMetric, error) - GetMetricVersions(core.Namespace) ([]core.CatalogedMetric, error) - GetMetric(core.Namespace, int) (core.CatalogedMetric, error) - Load(*core.RequestedPlugin) (core.CatalogedPlugin, serror.SnapError) - Unload(core.Plugin) (core.CatalogedPlugin, serror.SnapError) - PluginCatalog() core.PluginCatalog - AvailablePlugins() []core.AvailablePlugin - GetAutodiscoverPaths() []string -} - -type managesTasks interface { - CreateTask(cschedule.Schedule, *wmap.WorkflowMap, bool, ...core.TaskOption) (core.Task, core.TaskErrors) - GetTasks() map[string]core.Task - GetTask(string) (core.Task, error) - StartTask(string) []serror.SnapError - StopTask(string) []serror.SnapError - RemoveTask(string) error - WatchTask(string, core.TaskWatcherHandler) (core.TaskWatcherCloser, error) - EnableTask(string) (core.Task, error) -} - -type managesTribe interface { - GetAgreement(name string) (*agreement.Agreement, serror.SnapError) - GetAgreements() map[string]*agreement.Agreement - AddAgreement(name string) serror.SnapError - RemoveAgreement(name string) serror.SnapError - JoinAgreement(agreementName, memberName string) serror.SnapError - LeaveAgreement(agreementName, memberName string) serror.SnapError - GetMembers() []string - GetMember(name string) *agreement.Member -} - -type managesConfig interface { - GetPluginConfigDataNode(core.PluginType, string, int) cdata.ConfigDataNode - GetPluginConfigDataNodeAll() cdata.ConfigDataNode - MergePluginConfigDataNode(pluginType core.PluginType, name string, ver int, cdn *cdata.ConfigDataNode) cdata.ConfigDataNode - MergePluginConfigDataNodeAll(cdn *cdata.ConfigDataNode) cdata.ConfigDataNode - DeletePluginConfigDataNodeField(pluginType core.PluginType, name string, ver int, fields ...string) cdata.ConfigDataNode - DeletePluginConfigDataNodeFieldAll(fields ...string) cdata.ConfigDataNode -} - type Server struct { - mm managesMetrics - mt managesTasks - tr managesTribe - mc managesConfig + apis []api.API n *negroni.Negroni r *httprouter.Router snapTLS *snapTLS @@ -195,26 +65,27 @@ type Server struct { // New creates a REST API server with a given config func New(cfg *Config) (*Server, error) { // pull a few parameters from the configuration passed in by snapteld - https := cfg.HTTPS - cpath := cfg.RestCertificate - kpath := cfg.RestKey - pprof := cfg.Pprof s := &Server{ err: make(chan error), killChan: make(chan struct{}), addrString: cfg.Address, - pprof: pprof, + pprof: cfg.Pprof, } - if https { + if cfg.HTTPS { var err error - s.snapTLS, err = newtls(cpath, kpath) + s.snapTLS, err = newtls(cfg.RestCertificate, cfg.RestKey) if err != nil { return nil, err } protocolPrefix = "https" } + restLogger.Info(fmt.Sprintf("Configuring REST API with HTTPS set to: %v", cfg.HTTPS)) + + s.apis = []api.API{ + v1.New(&s.wg, s.killChan, protocolPrefix), + v2.New(&s.wg, s.killChan, protocolPrefix), + } - restLogger.Info(fmt.Sprintf("Configuring REST API with HTTPS set to: %v", https)) s.n = negroni.New( NewLogger(), negroni.NewRecovery(), @@ -226,84 +97,28 @@ func New(cfg *Config) (*Server, error) { return s, nil } -// GetDefaultConfig gets the default snapteld configuration -func GetDefaultConfig() *Config { - return &Config{ - Enable: defaultEnable, - Port: defaultPort, - Address: defaultAddress, - HTTPS: defaultHTTPS, - RestCertificate: defaultRestCertificate, - RestKey: defaultRestKey, - RestAuth: defaultAuth, - RestAuthPassword: defaultAuthPassword, - portSetByConfig: defaultPortSetByConfig, - Pprof: defaultPprof, +func (s *Server) BindMetricManager(m api.Metrics) { + for _, apiInstance := range s.apis { + apiInstance.BindMetricManager(m) } } -// define a method that can be used to determine if the port the RESTful -// API is listening on was set in the configuration file -func (c *Config) PortSetByConfigFile() bool { - return c.portSetByConfig +func (s *Server) BindTaskManager(t api.Tasks) { + for _, apiInstance := range s.apis { + apiInstance.BindTaskManager(t) + } } -// UnmarshalJSON unmarshals valid json into a Config. An example Config can be found -// at github.com/intelsdi-x/snap/blob/master/examples/configs/snap-config-sample.json -func (c *Config) UnmarshalJSON(data []byte) error { - // construct a map of strings to json.RawMessages (to defer the parsing of individual - // fields from the unmarshalled interface until later) and unmarshal the input - // byte array into that map - t := make(map[string]json.RawMessage) - if err := json.Unmarshal(data, &t); err != nil { - return err +func (s *Server) BindTribeManager(t api.Tribe) { + for _, apiInstance := range s.apis { + apiInstance.BindTribeManager(t) } - // loop through the individual map elements, parse each in turn, and set - // the appropriate field in this configuration - for k, v := range t { - switch k { - case "enable": - if err := json.Unmarshal(v, &(c.Enable)); err != nil { - return fmt.Errorf("%v (while parsing 'restapi::enable')", err) - } - case "port": - if err := json.Unmarshal(v, &(c.Port)); err != nil { - return fmt.Errorf("%v (while parsing 'restapi::port')", err) - } - c.portSetByConfig = true - case "addr": - if err := json.Unmarshal(v, &(c.Address)); err != nil { - return fmt.Errorf("%v (while parsing 'restapi::addr')", err) - } - case "https": - if err := json.Unmarshal(v, &(c.HTTPS)); err != nil { - return fmt.Errorf("%v (while parsing 'restapi::https')", err) - } - case "rest_certificate": - if err := json.Unmarshal(v, &(c.RestCertificate)); err != nil { - return fmt.Errorf("%v (while parsing 'restapi::rest_certificate')", err) - } - case "rest_key": - if err := json.Unmarshal(v, &(c.RestKey)); err != nil { - return fmt.Errorf("%v (while parsing 'restapi::rest_key')", err) - } - case "rest_auth": - if err := json.Unmarshal(v, &(c.RestAuth)); err != nil { - return fmt.Errorf("%v (while parsing 'restapi::rest_auth')", err) - } - case "rest_auth_password": - if err := json.Unmarshal(v, &(c.RestAuthPassword)); err != nil { - return fmt.Errorf("%v (while parsing 'restapi::rest_auth_password')", err) - } - case "pprof": - if err := json.Unmarshal(v, &(c.Pprof)); err != nil { - return fmt.Errorf("%v (while parsing 'restapi::pprof')", err) - } - default: - return fmt.Errorf("Unrecognized key '%v' in global config file while parsing 'restapi'", k) - } +} + +func (s *Server) BindConfigManager(c api.Config) { + for _, apiInstance := range s.apis { + apiInstance.BindConfigManager(c) } - return nil } // SetAPIAuth sets API authentication to enabled or disabled @@ -411,8 +226,8 @@ func (s *Server) serveTLS(ln net.Listener) { if err != nil { select { case <-s.closingChan: - // If we called Stop() then there will be a value in s.closingChan, so - // we'll get here and we can exit without showing the error. + // If we called Stop() then there will be a value in s.closingChan, so + // we'll get here and we can exit without showing the error. default: restLogger.Error(err) s.err <- err @@ -426,8 +241,8 @@ func (s *Server) serve(ln net.Listener) { if err != nil { select { case <-s.closingChan: - // If we called Stop() then there will be a value in s.closingChan, so - // we'll get here and we can exit without showing the error. + // If we called Stop() then there will be a value in s.closingChan, so + // we'll get here and we can exit without showing the error. default: restLogger.Error(err) s.err <- err @@ -435,6 +250,15 @@ func (s *Server) serve(ln net.Listener) { } } +func (s *Server) addRoutes() { + for _, apiInstance := range s.apis { + for _, route := range apiInstance.GetRoutes() { + s.r.Handle(route.Method, route.Path, route.Handle) + } + } + s.addPprofRoutes() +} + // Monkey patch ListenAndServe and TCP alive code from https://golang.org/src/net/http/server.go // The built in ListenAndServe and ListenAndServeTLS include TCP keepalive // At this point the Go team is not wanting to provide separate listen and serve methods @@ -452,122 +276,3 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { tc.SetKeepAlivePeriod(3 * time.Minute) return tc, nil } - -func (s *Server) BindMetricManager(m managesMetrics) { - s.mm = m -} - -func (s *Server) BindTaskManager(t managesTasks) { - s.mt = t -} - -func (s *Server) BindTribeManager(t managesTribe) { - s.tr = t -} - -func (s *Server) BindConfigManager(c managesConfig) { - s.mc = c -} - -func (s *Server) addRoutes() { - - // plugin routes - s.r.GET("/v1/plugins", s.getPlugins) - s.r.GET("/v1/plugins/:type", s.getPlugins) - s.r.GET("/v1/plugins/:type/:name", s.getPlugins) - s.r.GET("/v1/plugins/:type/:name/:version", s.getPlugin) - s.r.POST("/v1/plugins", s.loadPlugin) - s.r.DELETE("/v1/plugins/:type/:name/:version", s.unloadPlugin) - s.r.GET("/v1/plugins/:type/:name/:version/config", s.getPluginConfigItem) - s.r.PUT("/v1/plugins/:type/:name/:version/config", s.setPluginConfigItem) - s.r.DELETE("/v1/plugins/:type/:name/:version/config", s.deletePluginConfigItem) - - // metric routes - s.r.GET("/v1/metrics", s.getMetrics) - s.r.GET("/v1/metrics/*namespace", s.getMetricsFromTree) - - // task routes - s.r.GET("/v1/tasks", s.getTasks) - s.r.GET("/v1/tasks/:id", s.getTask) - s.r.GET("/v1/tasks/:id/watch", s.watchTask) - s.r.POST("/v1/tasks", s.addTask) - s.r.PUT("/v1/tasks/:id/start", s.startTask) - s.r.PUT("/v1/tasks/:id/stop", s.stopTask) - s.r.DELETE("/v1/tasks/:id", s.removeTask) - s.r.PUT("/v1/tasks/:id/enable", s.enableTask) - - // tribe routes - if s.tr != nil { - s.r.GET("/v1/tribe/agreements", s.getAgreements) - s.r.POST("/v1/tribe/agreements", s.addAgreement) - s.r.GET("/v1/tribe/agreements/:name", s.getAgreement) - s.r.DELETE("/v1/tribe/agreements/:name", s.deleteAgreement) - s.r.PUT("/v1/tribe/agreements/:name/join", s.joinAgreement) - s.r.DELETE("/v1/tribe/agreements/:name/leave", s.leaveAgreement) - s.r.GET("/v1/tribe/members", s.getMembers) - s.r.GET("/v1/tribe/member/:name", s.getMember) - } - - // profiling tools routes - if s.pprof { - s.r.GET("/debug/pprof/", s.index) - s.r.GET("/debug/pprof/block", s.index) - s.r.GET("/debug/pprof/goroutine", s.index) - s.r.GET("/debug/pprof/heap", s.index) - s.r.GET("/debug/pprof/threadcreate", s.index) - s.r.GET("/debug/pprof/cmdline", s.cmdline) - s.r.GET("/debug/pprof/profile", s.profile) - s.r.GET("/debug/pprof/symbol", s.symbol) - s.r.GET("/debug/pprof/trace", s.trace) - } -} - -// profiling tools handlers - -func (s *Server) index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - pprof.Index(w, r) -} - -func (s *Server) cmdline(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - pprof.Cmdline(w, r) -} - -func (s *Server) profile(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - pprof.Profile(w, r) -} - -func (s *Server) symbol(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - pprof.Symbol(w, r) -} - -func (s *Server) trace(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - pprof.Trace(w, r) -} - -func respond(code int, b rbody.Body, w http.ResponseWriter) { - resp := &rbody.APIResponse{ - Meta: &rbody.APIResponseMeta{ - Code: code, - Message: b.ResponseBodyMessage(), - Type: b.ResponseBodyType(), - Version: APIVersion, - }, - Body: b, - } - if !w.(negroni.ResponseWriter).Written() { - w.WriteHeader(code) - } - - j, err := json.MarshalIndent(resp, "", " ") - if err != nil { - panic(err) - } - j = bytes.Replace(j, []byte("\\u0026"), []byte("&"), -1) - fmt.Fprint(w, string(j)) -} - -func parseNamespace(ns string) []string { - fc := stringutils.GetFirstChar(ns) - ns = strings.Trim(ns, fc) - return strings.Split(ns, fc) -} diff --git a/mgmt/rest/server_test.go b/mgmt/rest/server_test.go index 7a2fbe8c8..f57a87ccf 100644 --- a/mgmt/rest/server_test.go +++ b/mgmt/rest/server_test.go @@ -1,4 +1,4 @@ -// +build legacy +// +build medium /* http://www.apache.org/licenses/LICENSE-2.0.txt @@ -28,6 +28,24 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const ( + MOCK_CONSTRAINTS = `{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "snapteld global config schema", + "type": ["object", "null"], + "properties": { + "control": { "$ref": "#/definitions/control" }, + "scheduler": { "$ref": "#/definitions/scheduler"}, + "restapi" : { "$ref": "#/definitions/restapi"}, + "tribe": { "$ref": "#/definitions/tribe"} + }, + "additionalProperties": true, + "definitions": { ` + + `"control": {}, "scheduler": {}, ` + CONFIG_CONSTRAINTS + `, "tribe":{}` + + `}` + + `}` +) + type mockRestAPIConfig struct { RestAPI *Config } @@ -145,44 +163,3 @@ func TestRestAPIDefaultConfig(t *testing.T) { }) }) } - -func TestParseNamespace(t *testing.T) { - tcs := getNsTestCases() - - Convey("Test parseNamespace", t, func() { - for _, c := range tcs { - Convey("Test parseNamespace "+c.input, func() { - So(c.output, ShouldResemble, parseNamespace(c.input)) - }) - } - }) -} - -type nsTestCase struct { - input string - output []string -} - -func getNsTestCases() []nsTestCase { - tcs := []nsTestCase{ - { - input: "小a小b小c", - output: []string{"a", "b", "c"}}, - { - input: "%a%b%c", - output: []string{"a", "b", "c"}}, - { - input: "-aヒ-b/-c|", - output: []string{"aヒ", "b/", "c|"}}, - { - input: ">a>b=>c=", - output: []string{"a", "b=", "c="}}, - { - input: ">a>b<>c<", - output: []string{"a", "b<", "c<"}}, - { - input: "㊽a㊽b%㊽c/|", - output: []string{"a", "b%", "c/|"}}, - } - return tcs -} diff --git a/mgmt/rest/tribe_test.go b/mgmt/rest/tribe_v1_test.go similarity index 99% rename from mgmt/rest/tribe_test.go rename to mgmt/rest/tribe_v1_test.go index c11c74ac5..a11cae485 100644 --- a/mgmt/rest/tribe_test.go +++ b/mgmt/rest/tribe_v1_test.go @@ -1,4 +1,4 @@ -// +build legacy +// +build medium /* http://www.apache.org/licenses/LICENSE-2.0.txt @@ -39,11 +39,17 @@ import ( "github.com/intelsdi-x/snap/control" "github.com/intelsdi-x/snap/core" "github.com/intelsdi-x/snap/core/tribe_event" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" "github.com/intelsdi-x/snap/mgmt/tribe" "github.com/intelsdi-x/snap/scheduler" ) +var ( + tribeLogger = restLogger.WithFields(log.Fields{ + "_module": "rest-tribe", + }) +) + func getMembers(port int) *rbody.APIResponse { resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/v1/tribe/members", port)) if err != nil { diff --git a/mgmt/rest/v1/api.go b/mgmt/rest/v1/api.go new file mode 100644 index 000000000..852156209 --- /dev/null +++ b/mgmt/rest/v1/api.go @@ -0,0 +1,92 @@ +package v1 + +import ( + "sync" + + log "github.com/Sirupsen/logrus" + "github.com/intelsdi-x/snap/mgmt/rest/api" +) + +const ( + version = "v1" + prefix = "/" + version +) + +var ( + restLogger = log.WithField("_module", "_mgmt-rest-v1") + protocolPrefix = "http" +) + +type apiV1 struct { + metricManager api.Metrics + taskManager api.Tasks + tribeManager api.Tribe + configManager api.Config + + wg *sync.WaitGroup + killChan chan struct{} +} + +func New(wg *sync.WaitGroup, killChan chan struct{}, protocol string) *apiV1 { + protocolPrefix = protocol + return &apiV1{wg: wg, killChan: killChan} +} + +func (s *apiV1) GetRoutes() []api.Route { + routes := []api.Route{ + // plugin routes + api.Route{Method: "GET", Path: prefix + "/plugins", Handle: s.getPlugins}, + api.Route{Method: "GET", Path: prefix + "/plugins/:type", Handle: s.getPlugins}, + api.Route{Method: "GET", Path: prefix + "/plugins/:type/:name", Handle: s.getPlugins}, + api.Route{Method: "GET", Path: prefix + "/plugins/:type/:name/:version", Handle: s.getPlugin}, + api.Route{Method: "POST", Path: prefix + "/plugins", Handle: s.loadPlugin}, + api.Route{Method: "DELETE", Path: prefix + "/plugins/:type/:name/:version", Handle: s.unloadPlugin}, + api.Route{Method: "GET", Path: prefix + "/plugins/:type/:name/:version/config", Handle: s.getPluginConfigItem}, + api.Route{Method: "PUT", Path: prefix + "/plugins/:type/:name/:version/config", Handle: s.setPluginConfigItem}, + api.Route{Method: "DELETE", Path: prefix + "/plugins/:type/:name/:version/config", Handle: s.deletePluginConfigItem}, + + // metric routes + api.Route{Method: "GET", Path: prefix + "/metrics", Handle: s.getMetrics}, + api.Route{Method: "GET", Path: prefix + "/metrics/*namespace", Handle: s.getMetricsFromTree}, + + // task routes + api.Route{Method: "GET", Path: prefix + "/tasks", Handle: s.getTasks}, + api.Route{Method: "GET", Path: prefix + "/tasks/:id", Handle: s.getTask}, + api.Route{Method: "GET", Path: prefix + "/tasks/:id/watch", Handle: s.watchTask}, + api.Route{Method: "POST", Path: prefix + "/tasks", Handle: s.addTask}, + api.Route{Method: "PUT", Path: prefix + "/tasks/:id/start", Handle: s.startTask}, + api.Route{Method: "PUT", Path: prefix + "/tasks/:id/stop", Handle: s.stopTask}, + api.Route{Method: "DELETE", Path: prefix + "/tasks/:id", Handle: s.removeTask}, + api.Route{Method: "PUT", Path: prefix + "/tasks/:id/enable", Handle: s.enableTask}, + } + // tribe routes + if s.tribeManager != nil { + routes = append(routes, []api.Route{ + api.Route{Method: "GET", Path: prefix + "/tribe/agreements", Handle: s.getAgreements}, + api.Route{Method: "POST", Path: prefix + "/tribe/agreements", Handle: s.addAgreement}, + api.Route{Method: "GET", Path: prefix + "/tribe/agreements/:name", Handle: s.getAgreement}, + api.Route{Method: "DELETE", Path: prefix + "/tribe/agreements/:name", Handle: s.deleteAgreement}, + api.Route{Method: "PUT", Path: prefix + "/tribe/agreements/:name/join", Handle: s.joinAgreement}, + api.Route{Method: "DELETE", Path: prefix + "/tribe/agreements/:name/leave", Handle: s.leaveAgreement}, + api.Route{Method: "GET", Path: prefix + "/tribe/members", Handle: s.getMembers}, + api.Route{Method: "GET", Path: prefix + "/tribe/member/:name", Handle: s.getMember}, + }...) + } + return routes +} + +func (s *apiV1) BindMetricManager(metricManager api.Metrics) { + s.metricManager = metricManager +} + +func (s *apiV1) BindTaskManager(taskManager api.Tasks) { + s.taskManager = taskManager +} + +func (s *apiV1) BindTribeManager(tribeManager api.Tribe) { + s.tribeManager = tribeManager +} + +func (s *apiV1) BindConfigManager(configManager api.Config) { + s.configManager = configManager +} diff --git a/mgmt/rest/v1/config.go b/mgmt/rest/v1/config.go new file mode 100644 index 000000000..23215b658 --- /dev/null +++ b/mgmt/rest/v1/config.go @@ -0,0 +1,158 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2015 Intel Corporation + +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 v1 + +import ( + "net/http" + "strconv" + + "github.com/intelsdi-x/snap/core" + "github.com/intelsdi-x/snap/core/cdata" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" + "github.com/julienschmidt/httprouter" +) + +func (s *apiV1) getPluginConfigItem(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + var err error + styp := p.ByName("type") + if styp == "" { + cdn := s.configManager.GetPluginConfigDataNodeAll() + item := &rbody.PluginConfigItem{ConfigDataNode: cdn} + rbody.Write(200, item, w) + return + } + + typ, err := getPluginType(styp) + if err != nil { + rbody.Write(400, rbody.FromError(err), w) + return + } + + name := p.ByName("name") + sver := p.ByName("version") + var iver int + if sver != "" { + if iver, err = strconv.Atoi(sver); err != nil { + rbody.Write(400, rbody.FromError(err), w) + return + } + } else { + iver = -2 + } + + cdn := s.configManager.GetPluginConfigDataNode(typ, name, iver) + item := &rbody.PluginConfigItem{ConfigDataNode: cdn} + rbody.Write(200, item, w) +} + +func (s *apiV1) deletePluginConfigItem(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + var err error + var typ core.PluginType + styp := p.ByName("type") + if styp != "" { + typ, err = getPluginType(styp) + if err != nil { + rbody.Write(400, rbody.FromError(err), w) + return + } + } + + name := p.ByName("name") + sver := p.ByName("version") + var iver int + if sver != "" { + if iver, err = strconv.Atoi(sver); err != nil { + rbody.Write(400, rbody.FromError(err), w) + return + } + } else { + iver = -2 + } + + src := []string{} + errCode, err := core.UnmarshalBody(&src, r.Body) + if errCode != 0 && err != nil { + rbody.Write(400, rbody.FromError(err), w) + return + } + + var res cdata.ConfigDataNode + if styp == "" { + res = s.configManager.DeletePluginConfigDataNodeFieldAll(src...) + } else { + res = s.configManager.DeletePluginConfigDataNodeField(typ, name, iver, src...) + } + + item := &rbody.DeletePluginConfigItem{ConfigDataNode: res} + rbody.Write(200, item, w) +} + +func (s *apiV1) setPluginConfigItem(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + var err error + var typ core.PluginType + styp := p.ByName("type") + if styp != "" { + typ, err = getPluginType(styp) + if err != nil { + rbody.Write(400, rbody.FromError(err), w) + return + } + } + + name := p.ByName("name") + sver := p.ByName("version") + var iver int + if sver != "" { + if iver, err = strconv.Atoi(sver); err != nil { + rbody.Write(400, rbody.FromError(err), w) + return + } + } else { + iver = -2 + } + + src := cdata.NewNode() + errCode, err := core.UnmarshalBody(src, r.Body) + if errCode != 0 && err != nil { + rbody.Write(400, rbody.FromError(err), w) + return + } + + var res cdata.ConfigDataNode + if styp == "" { + res = s.configManager.MergePluginConfigDataNodeAll(src) + } else { + res = s.configManager.MergePluginConfigDataNode(typ, name, iver, src) + } + + item := &rbody.SetPluginConfigItem{ConfigDataNode: res} + rbody.Write(200, item, w) +} + +func getPluginType(t string) (core.PluginType, error) { + if ityp, err := strconv.Atoi(t); err == nil { + return core.PluginType(ityp), nil + } + ityp, err := core.ToPluginType(t) + if err != nil { + return core.PluginType(-1), err + } + return ityp, nil +} diff --git a/mgmt/rest/fixtures/mock_config_manager.go b/mgmt/rest/v1/fixtures/mock_config_manager.go similarity index 100% rename from mgmt/rest/fixtures/mock_config_manager.go rename to mgmt/rest/v1/fixtures/mock_config_manager.go diff --git a/mgmt/rest/fixtures/mock_metric_manager.go b/mgmt/rest/v1/fixtures/mock_metric_manager.go similarity index 100% rename from mgmt/rest/fixtures/mock_metric_manager.go rename to mgmt/rest/v1/fixtures/mock_metric_manager.go diff --git a/mgmt/rest/fixtures/mock_task_manager.go b/mgmt/rest/v1/fixtures/mock_task_manager.go similarity index 100% rename from mgmt/rest/fixtures/mock_task_manager.go rename to mgmt/rest/v1/fixtures/mock_task_manager.go diff --git a/mgmt/rest/fixtures/mock_tribe_manager.go b/mgmt/rest/v1/fixtures/mock_tribe_manager.go similarity index 93% rename from mgmt/rest/fixtures/mock_tribe_manager.go rename to mgmt/rest/v1/fixtures/mock_tribe_manager.go index 10a9717f1..a18f3b5b9 100644 --- a/mgmt/rest/fixtures/mock_tribe_manager.go +++ b/mgmt/rest/v1/fixtures/mock_tribe_manager.go @@ -35,6 +35,19 @@ var ( ) func init() { + mockTribeMember = agreement.NewMember(&memberlist.Node{ + Name: "Imma_Mock", + Addr: net.ParseIP("193.11.22.11"), + Port: uint16(0), + Meta: []byte("meta"), // Metadata from the delegate for this node. + PMin: uint8(0), // Minimum protocol version this understands + PMax: uint8(0), // Maximum protocol version this understands + PCur: uint8(0), // Current version node is speaking + DMin: uint8(0), // Min protocol version for the delegate to understand + DMax: uint8(0), // Max protocol version for the delegate to understand + DCur: uint8(0), // Current version delegate is speaking + }) + mockTribeAgreement = agreement.New("Agree1") mockTribeAgreement.PluginAgreement.Add( agreement.Plugin{Name_: "mockVersion", Version_: 1, Type_: core.CollectorPluginType}) @@ -80,7 +93,7 @@ func (m *MockTribeManager) GetMembers() []string { return []string{"one", "two", "three"} } func (m *MockTribeManager) GetMember(name string) *agreement.Member { - return &agreement.Member{} + return mockTribeMember } // These constants are the expected tribe responses from running @@ -209,7 +222,7 @@ const ( "version": 1 }, "body": { - "name": "", + "name": "Imma_Mock", "plugin_agreement": "", "tags": null, "task_agreements": null diff --git a/mgmt/rest/metric.go b/mgmt/rest/v1/metric.go similarity index 59% rename from mgmt/rest/metric.go rename to mgmt/rest/v1/metric.go index b0863a0a4..1c0ced306 100644 --- a/mgmt/rest/metric.go +++ b/mgmt/rest/v1/metric.go @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rest +package v1 import ( "fmt" @@ -27,14 +27,13 @@ import ( "strconv" "strings" - "github.com/julienschmidt/httprouter" - "github.com/intelsdi-x/snap/core" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" "github.com/intelsdi-x/snap/pkg/stringutils" + "github.com/julienschmidt/httprouter" ) -func (s *Server) getMetrics(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { +func (s *apiV1) getMetrics(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { ver := 0 // 0: get all metrics // If we are provided a parameter with the name 'ns' we need to @@ -48,7 +47,7 @@ func (s *Server) getMetrics(w http.ResponseWriter, r *http.Request, _ httprouter var err error ver, err = strconv.Atoi(v) if err != nil { - respond(400, rbody.FromError(err), w) + rbody.Write(400, rbody.FromError(err), w) return } } @@ -59,24 +58,24 @@ func (s *Server) getMetrics(w http.ResponseWriter, r *http.Request, _ httprouter ns = ns[:len(ns)-1] } - mets, err := s.mm.FetchMetrics(core.NewNamespace(ns...), ver) + mts, err := s.metricManager.FetchMetrics(core.NewNamespace(ns...), ver) if err != nil { - respond(404, rbody.FromError(err), w) + rbody.Write(404, rbody.FromError(err), w) return } - respondWithMetrics(r.Host, mets, w) + respondWithMetrics(r.Host, mts, w) return } - mets, err := s.mm.MetricCatalog() + mts, err := s.metricManager.MetricCatalog() if err != nil { - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } - respondWithMetrics(r.Host, mets, w) + respondWithMetrics(r.Host, mts, w) } -func (s *Server) getMetricsFromTree(w http.ResponseWriter, r *http.Request, params httprouter.Params) { +func (s *apiV1) getMetricsFromTree(w http.ResponseWriter, r *http.Request, params httprouter.Params) { namespace := params.ByName("namespace") // we land here if the request contains a trailing slash, because it matches the tree @@ -103,40 +102,40 @@ func (s *Server) getMetricsFromTree(w http.ResponseWriter, r *http.Request, para } else { ver, err = strconv.Atoi(v) if err != nil { - respond(400, rbody.FromError(err), w) + rbody.Write(400, rbody.FromError(err), w) return } } - mets, err := s.mm.FetchMetrics(core.NewNamespace(ns[:len(ns)-1]...), ver) + mts, err := s.metricManager.FetchMetrics(core.NewNamespace(ns[:len(ns)-1]...), ver) if err != nil { - respond(404, rbody.FromError(err), w) + rbody.Write(404, rbody.FromError(err), w) return } - respondWithMetrics(r.Host, mets, w) + respondWithMetrics(r.Host, mts, w) return } // If no version was given, get all version that fall at this namespace. if v == "" { - mets, err := s.mm.FetchMetrics(core.NewNamespace(ns...), 0) + mts, err := s.metricManager.FetchMetrics(core.NewNamespace(ns...), 0) if err != nil { - respond(404, rbody.FromError(err), w) + rbody.Write(404, rbody.FromError(err), w) return } - respondWithMetrics(r.Host, mets, w) + respondWithMetrics(r.Host, mts, w) return } // if an explicit version is given, get that single one. ver, err = strconv.Atoi(v) if err != nil { - respond(400, rbody.FromError(err), w) + rbody.Write(400, rbody.FromError(err), w) return } - mt, err := s.mm.GetMetric(core.NewNamespace(ns...), ver) + mt, err := s.metricManager.GetMetric(core.NewNamespace(ns...), ver) if err != nil { - respond(404, rbody.FromError(err), w) + rbody.Write(404, rbody.FromError(err), w) return } @@ -154,64 +153,37 @@ func (s *Server) getMetricsFromTree(w http.ResponseWriter, r *http.Request, para Description: mt.Description(), Unit: mt.Unit(), LastAdvertisedTimestamp: mt.LastAdvertisedTime().Unix(), - Href: catalogedMetricURI(r.Host, mt), - } - rt := mt.Policy().RulesAsTable() - policies := make([]rbody.PolicyTable, 0, len(rt)) - for _, r := range rt { - policies = append(policies, rbody.PolicyTable{ - Name: r.Name, - Type: r.Type, - Default: r.Default, - Required: r.Required, - Minimum: r.Minimum, - Maximum: r.Maximum, - }) + Href: catalogedMetricURI(r.Host, version, mt), } + policies := rbody.PolicyTableSlice(mt.Policy().RulesAsTable()) mb.Policy = policies b.Metric = mb - respond(200, b, w) + rbody.Write(200, b, w) } -func respondWithMetrics(host string, mets []core.CatalogedMetric, w http.ResponseWriter) { +func respondWithMetrics(host string, mts []core.CatalogedMetric, w http.ResponseWriter) { b := rbody.NewMetricsReturned() - - for _, met := range mets { - rt := met.Policy().RulesAsTable() - policies := make([]rbody.PolicyTable, 0, len(rt)) - for _, r := range rt { - policies = append(policies, rbody.PolicyTable{ - Name: r.Name, - Type: r.Type, - Default: r.Default, - Required: r.Required, - Minimum: r.Minimum, - Maximum: r.Maximum, - }) - } - dyn, indexes := met.Namespace().IsDynamic() + for _, m := range mts { + policies := rbody.PolicyTableSlice(m.Policy().RulesAsTable()) + dyn, indexes := m.Namespace().IsDynamic() var dynamicElements []rbody.DynamicElement if dyn { - dynamicElements = getDynamicElements(met.Namespace(), indexes) + dynamicElements = getDynamicElements(m.Namespace(), indexes) } b = append(b, rbody.Metric{ - Namespace: met.Namespace().String(), - Version: met.Version(), - LastAdvertisedTimestamp: met.LastAdvertisedTime().Unix(), - Description: met.Description(), + Namespace: m.Namespace().String(), + Version: m.Version(), + LastAdvertisedTimestamp: m.LastAdvertisedTime().Unix(), + Description: m.Description(), Dynamic: dyn, DynamicElements: dynamicElements, - Unit: met.Unit(), + Unit: m.Unit(), Policy: policies, - Href: catalogedMetricURI(host, met), + Href: catalogedMetricURI(host, version, m), }) } sort.Sort(b) - respond(200, b, w) -} - -func catalogedMetricURI(host string, mt core.CatalogedMetric) string { - return fmt.Sprintf("%s://%s/v1/metrics?ns=%s&ver=%d", protocolPrefix, host, url.QueryEscape(mt.Namespace().String()), mt.Version()) + rbody.Write(200, b, w) } func getDynamicElements(ns core.Namespace, indexes []int) []rbody.DynamicElement { @@ -226,3 +198,13 @@ func getDynamicElements(ns core.Namespace, indexes []int) []rbody.DynamicElement } return elements } + +func catalogedMetricURI(host, version string, mt core.CatalogedMetric) string { + return fmt.Sprintf("%s://%s/%s/metrics?ns=%s&ver=%d", protocolPrefix, host, version, url.QueryEscape(mt.Namespace().String()), mt.Version()) +} + +func parseNamespace(ns string) []string { + fc := stringutils.GetFirstChar(ns) + ns = strings.Trim(ns, fc) + return strings.Split(ns, fc) +} diff --git a/mgmt/rest/v1/metric_test.go b/mgmt/rest/v1/metric_test.go new file mode 100644 index 000000000..d847621e8 --- /dev/null +++ b/mgmt/rest/v1/metric_test.go @@ -0,0 +1,69 @@ +// +build small + +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2015 Intel Corporation + +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 v1 + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestParseNamespace(t *testing.T) { + tcs := getNsTestCases() + + Convey("Test parseNamespace", t, func() { + for _, c := range tcs { + Convey("Test parseNamespace "+c.input, func() { + So(c.output, ShouldResemble, parseNamespace(c.input)) + }) + } + }) +} + +type nsTestCase struct { + input string + output []string +} + +func getNsTestCases() []nsTestCase { + tcs := []nsTestCase{ + { + input: "小a小b小c", + output: []string{"a", "b", "c"}}, + { + input: "%a%b%c", + output: []string{"a", "b", "c"}}, + { + input: "-aヒ-b/-c|", + output: []string{"aヒ", "b/", "c|"}}, + { + input: ">a>b=>c=", + output: []string{"a", "b=", "c="}}, + { + input: ">a>b<>c<", + output: []string{"a", "b<", "c<"}}, + { + input: "㊽a㊽b%㊽c/|", + output: []string{"a", "b%", "c/|"}}, + } + return tcs +} diff --git a/mgmt/rest/plugin.go b/mgmt/rest/v1/plugin.go similarity index 79% rename from mgmt/rest/plugin.go rename to mgmt/rest/v1/plugin.go index e07e0c087..6451084b0 100644 --- a/mgmt/rest/plugin.go +++ b/mgmt/rest/v1/plugin.go @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rest +package v1 import ( "compress/gzip" @@ -37,11 +37,11 @@ import ( "strings" log "github.com/Sirupsen/logrus" - "github.com/julienschmidt/httprouter" - "github.com/intelsdi-x/snap/core" "github.com/intelsdi-x/snap/core/serror" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/api" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" + "github.com/julienschmidt/httprouter" ) const PluginAlreadyLoaded = "plugin is already loaded" @@ -69,10 +69,10 @@ func (p *plugin) TypeName() string { return p.pluginType } -func (s *Server) loadPlugin(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { +func (s *apiV1) loadPlugin(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil { - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } if strings.HasPrefix(mediaType, "multipart/") { @@ -90,25 +90,25 @@ func (s *Server) loadPlugin(w http.ResponseWriter, r *http.Request, _ httprouter break } if err != nil { - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } if r.Header.Get("Plugin-Compression") == "gzip" { g, err := gzip.NewReader(p) defer g.Close() if err != nil { - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } b, err = ioutil.ReadAll(g) if err != nil { - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } } else { b, err = ioutil.ReadAll(p) if err != nil { - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } } @@ -125,11 +125,11 @@ func (s *Server) loadPlugin(w http.ResponseWriter, r *http.Request, _ httprouter case i == 0: if filepath.Ext(p.FileName()) == ".asc" { e := errors.New("Error: first file passed to load plugin api can not be signature file") - respond(500, rbody.FromError(e), w) + rbody.Write(500, rbody.FromError(e), w) return } if pluginPath, err = writeFile(p.FileName(), b); err != nil { - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } checkSum = sha256.Sum256(b) @@ -138,19 +138,19 @@ func (s *Server) loadPlugin(w http.ResponseWriter, r *http.Request, _ httprouter signature = b } else { e := errors.New("Error: second file passed was not a signature file") - respond(500, rbody.FromError(e), w) + rbody.Write(500, rbody.FromError(e), w) return } case i == 2: e := errors.New("Error: More than two files passed to the load plugin api") - respond(500, rbody.FromError(e), w) + rbody.Write(500, rbody.FromError(e), w) return } i++ } rp, err := core.NewRequestedPlugin(pluginPath) if err != nil { - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } rp.SetAutoLoaded(false) @@ -158,12 +158,12 @@ func (s *Server) loadPlugin(w http.ResponseWriter, r *http.Request, _ httprouter // as after it is written to disk. if rp.CheckSum() != checkSum { e := errors.New("Error: CheckSum mismatch on requested plugin to load") - respond(500, rbody.FromError(e), w) + rbody.Write(500, rbody.FromError(e), w) return } rp.SetSignature(signature) restLogger.Info("Loading plugin: ", rp.Path()) - pl, err := s.mm.Load(rp) + pl, err := s.metricManager.Load(rp) if err != nil { var ec int restLogger.Error(err) @@ -179,11 +179,11 @@ func (s *Server) loadPlugin(w http.ResponseWriter, r *http.Request, _ httprouter default: ec = 500 } - respond(ec, rb, w) + rbody.Write(ec, rb, w) return } - lp.LoadedPlugins = append(lp.LoadedPlugins, *catalogedPluginToLoaded(r.Host, pl)) - respond(201, lp, w) + lp.LoadedPlugins = append(lp.LoadedPlugins, catalogedPluginToLoaded(r.Host, pl)) + rbody.Write(201, lp, w) } } @@ -197,6 +197,9 @@ func writeFile(filename string, b []byte) (string, error) { if err != nil { return "", err } + // Close before load + defer f.Close() + n, err := f.Write(b) log.Debugf("wrote %v to %v", n, f.Name()) if err != nil { @@ -208,12 +211,10 @@ func writeFile(filename string, b []byte) (string, error) { return "", err } } - // Close before load - f.Close() return f.Name(), nil } -func (s *Server) unloadPlugin(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) unloadPlugin(w http.ResponseWriter, r *http.Request, p httprouter.Params) { plName := p.ByName("name") plType := p.ByName("type") plVersion, iErr := strconv.ParseInt(p.ByName("version"), 10, 0) @@ -226,30 +227,30 @@ func (s *Server) unloadPlugin(w http.ResponseWriter, r *http.Request, p httprout if iErr != nil { se := serror.New(errors.New("invalid version")) se.SetFields(f) - respond(400, rbody.FromSnapError(se), w) + rbody.Write(400, rbody.FromSnapError(se), w) return } if plName == "" { se := serror.New(errors.New("missing plugin name")) se.SetFields(f) - respond(400, rbody.FromSnapError(se), w) + rbody.Write(400, rbody.FromSnapError(se), w) return } if plType == "" { se := serror.New(errors.New("missing plugin type")) se.SetFields(f) - respond(400, rbody.FromSnapError(se), w) + rbody.Write(400, rbody.FromSnapError(se), w) return } - up, se := s.mm.Unload(&plugin{ + up, se := s.metricManager.Unload(&plugin{ name: plName, version: int(plVersion), pluginType: plType, }) if se != nil { se.SetFields(f) - respond(500, rbody.FromSnapError(se), w) + rbody.Write(500, rbody.FromSnapError(se), w) return } pr := &rbody.PluginUnloaded{ @@ -257,10 +258,10 @@ func (s *Server) unloadPlugin(w http.ResponseWriter, r *http.Request, p httprout Version: up.Version(), Type: up.TypeName(), } - respond(200, pr, w) + rbody.Write(200, pr, w) } -func (s *Server) getPlugins(w http.ResponseWriter, r *http.Request, params httprouter.Params) { +func (s *apiV1) getPlugins(w http.ResponseWriter, r *http.Request, params httprouter.Params) { var detail bool for k := range r.URL.Query() { if k == "details" { @@ -269,10 +270,10 @@ func (s *Server) getPlugins(w http.ResponseWriter, r *http.Request, params httpr } plName := params.ByName("name") plType := params.ByName("type") - respond(200, getPlugins(s.mm, detail, r.Host, plName, plType), w) + rbody.Write(200, getPlugins(s.metricManager, detail, r.Host, plName, plType), w) } -func getPlugins(mm managesMetrics, detail bool, h string, plName string, plType string) *rbody.PluginList { +func getPlugins(mm api.Metrics, detail bool, h string, plName string, plType string) *rbody.PluginList { plCatalog := mm.PluginCatalog() @@ -280,7 +281,7 @@ func getPlugins(mm managesMetrics, detail bool, h string, plName string, plType plugins.LoadedPlugins = make([]rbody.LoadedPlugin, len(plCatalog)) for i, p := range plCatalog { - plugins.LoadedPlugins[i] = *catalogedPluginToLoaded(h, p) + plugins.LoadedPlugins[i] = catalogedPluginToLoaded(h, p) } if detail { @@ -294,7 +295,7 @@ func getPlugins(mm managesMetrics, detail bool, h string, plName string, plType HitCount: p.HitCount(), LastHitTimestamp: p.LastHit().Unix(), ID: p.ID(), - Href: pluginURI(h, p), + Href: pluginURI(h, version, p), PprofPort: p.Port(), } } @@ -337,19 +338,19 @@ func getPlugins(mm managesMetrics, detail bool, h string, plName string, plType return &plugins } -func catalogedPluginToLoaded(host string, c core.CatalogedPlugin) *rbody.LoadedPlugin { - return &rbody.LoadedPlugin{ +func catalogedPluginToLoaded(host string, c core.CatalogedPlugin) rbody.LoadedPlugin { + return rbody.LoadedPlugin{ Name: c.Name(), Version: c.Version(), Type: c.TypeName(), Signed: c.IsSigned(), Status: c.Status(), LoadedTimestamp: c.LoadedTimestamp().Unix(), - Href: pluginURI(host, c), + Href: pluginURI(host, version, c), } } -func (s *Server) getPlugin(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) getPlugin(w http.ResponseWriter, r *http.Request, p httprouter.Params) { plName := p.ByName("name") plType := p.ByName("type") plVersion, iErr := strconv.ParseInt(p.ByName("version"), 10, 0) @@ -362,24 +363,24 @@ func (s *Server) getPlugin(w http.ResponseWriter, r *http.Request, p httprouter. if iErr != nil { se := serror.New(errors.New("invalid version")) se.SetFields(f) - respond(400, rbody.FromSnapError(se), w) + rbody.Write(400, rbody.FromSnapError(se), w) return } if plName == "" { se := serror.New(errors.New("missing plugin name")) se.SetFields(f) - respond(400, rbody.FromSnapError(se), w) + rbody.Write(400, rbody.FromSnapError(se), w) return } if plType == "" { se := serror.New(errors.New("missing plugin type")) se.SetFields(f) - respond(400, rbody.FromSnapError(se), w) + rbody.Write(400, rbody.FromSnapError(se), w) return } - pluginCatalog := s.mm.PluginCatalog() + pluginCatalog := s.metricManager.PluginCatalog() var plugin core.CatalogedPlugin for _, item := range pluginCatalog { if item.Name() == plName && @@ -391,7 +392,7 @@ func (s *Server) getPlugin(w http.ResponseWriter, r *http.Request, p httprouter. } if plugin == nil { se := serror.New(ErrPluginNotFound, f) - respond(404, rbody.FromSnapError(se), w) + rbody.Write(404, rbody.FromSnapError(se), w) return } @@ -421,7 +422,7 @@ func (s *Server) getPlugin(w http.ResponseWriter, r *http.Request, p httprouter. if err != nil { f["plugin-path"] = plugin.PluginPath() se := serror.New(err, f) - respond(500, rbody.FromSnapError(se), w) + rbody.Write(500, rbody.FromSnapError(se), w) return } @@ -432,7 +433,7 @@ func (s *Server) getPlugin(w http.ResponseWriter, r *http.Request, p httprouter. if err != nil { f["plugin-path"] = plugin.PluginPath() se := serror.New(err, f) - respond(500, rbody.FromSnapError(se), w) + rbody.Write(500, rbody.FromSnapError(se), w) return } return @@ -444,13 +445,13 @@ func (s *Server) getPlugin(w http.ResponseWriter, r *http.Request, p httprouter. Signed: plugin.IsSigned(), Status: plugin.Status(), LoadedTimestamp: plugin.LoadedTimestamp().Unix(), - Href: pluginURI(r.Host, plugin), + Href: pluginURI(r.Host, version, plugin), ConfigPolicy: configPolicy, } - respond(200, pluginRet, w) + rbody.Write(200, pluginRet, w) } } -func pluginURI(host string, c core.Plugin) string { - return fmt.Sprintf("%s://%s/v1/plugins/%s/%s/%d", protocolPrefix, host, c.TypeName(), c.Name(), c.Version()) +func pluginURI(host, version string, c core.Plugin) string { + return fmt.Sprintf("%s://%s/%s/plugins/%s/%s/%d", protocolPrefix, host, version, c.TypeName(), c.Name(), c.Version()) } diff --git a/mgmt/rest/plugin_test.go b/mgmt/rest/v1/plugin_test.go similarity index 97% rename from mgmt/rest/plugin_test.go rename to mgmt/rest/v1/plugin_test.go index 772a3edce..0e8f4d245 100644 --- a/mgmt/rest/plugin_test.go +++ b/mgmt/rest/v1/plugin_test.go @@ -1,4 +1,4 @@ -// +build legacy +// +build medium /* http://www.apache.org/licenses/LICENSE-2.0.txt @@ -19,12 +19,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rest +package v1 import ( "testing" - "github.com/intelsdi-x/snap/mgmt/rest/fixtures" + "github.com/intelsdi-x/snap/mgmt/rest/v1/fixtures" . "github.com/smartystreets/goconvey/convey" ) diff --git a/mgmt/rest/rbody/body.go b/mgmt/rest/v1/rbody/body.go similarity index 88% rename from mgmt/rest/rbody/body.go rename to mgmt/rest/v1/rbody/body.go index 7d26981dc..1827a8cd6 100644 --- a/mgmt/rest/rbody/body.go +++ b/mgmt/rest/v1/rbody/body.go @@ -23,7 +23,13 @@ import ( "encoding/json" "errors" + "bytes" + "fmt" + "net/http" + + "github.com/Sirupsen/logrus" "github.com/intelsdi-x/snap/core/cdata" + "github.com/urfave/negroni" ) type Body interface { @@ -33,6 +39,29 @@ type Body interface { ResponseBodyType() string } +func Write(code int, b Body, w http.ResponseWriter) { + w.Header().Set("Deprecated", "true") + resp := &APIResponse{ + Meta: &APIResponseMeta{ + Code: code, + Message: b.ResponseBodyMessage(), + Type: b.ResponseBodyType(), + Version: 1, + }, + Body: b, + } + if !w.(negroni.ResponseWriter).Written() { + w.WriteHeader(code) + } + + j, err := json.MarshalIndent(resp, "", " ") + if err != nil { + logrus.Fatalln(err) + } + j = bytes.Replace(j, []byte("\\u0026"), []byte("&"), -1) + fmt.Fprint(w, string(j)) +} + var ( ErrCannotUnmarshalBody = errors.New("Cannot unmarshal body: invalid type") ) diff --git a/mgmt/rest/rbody/config.go b/mgmt/rest/v1/rbody/config.go similarity index 100% rename from mgmt/rest/rbody/config.go rename to mgmt/rest/v1/rbody/config.go diff --git a/mgmt/rest/rbody/error.go b/mgmt/rest/v1/rbody/error.go similarity index 100% rename from mgmt/rest/rbody/error.go rename to mgmt/rest/v1/rbody/error.go diff --git a/mgmt/rest/rbody/metric.go b/mgmt/rest/v1/rbody/metric.go similarity index 86% rename from mgmt/rest/rbody/metric.go rename to mgmt/rest/v1/rbody/metric.go index 68d94c995..06e958cc6 100644 --- a/mgmt/rest/rbody/metric.go +++ b/mgmt/rest/v1/rbody/metric.go @@ -19,21 +19,20 @@ limitations under the License. package rbody -import "fmt" +import ( + "fmt" + + "github.com/intelsdi-x/snap/control/plugin/cpolicy" +) const ( MetricsReturnedType = "metrics_returned" MetricReturnedType = "metric_returned" ) -type PolicyTable struct { - Name string `json:"name"` - Type string `json:"type"` - Default interface{} `json:"default,omitempty"` - Required bool `json:"required"` - Minimum interface{} `json:"minimum,omitempty"` - Maximum interface{} `json:"maximum,omitempty"` -} +type PolicyTable cpolicy.RuleTable + +type PolicyTableSlice []cpolicy.RuleTable type Metric struct { LastAdvertisedTimestamp int64 `json:"last_advertised_timestamp,omitempty"` @@ -43,7 +42,7 @@ type Metric struct { DynamicElements []DynamicElement `json:"dynamic_elements,omitempty"` Description string `json:"description,omitempty"` Unit string `json:"unit,omitempty"` - Policy []PolicyTable `json:"policy,omitempty"` + Policy PolicyTableSlice `json:"policy,omitempty"` Href string `json:"href"` } diff --git a/mgmt/rest/rbody/plugin.go b/mgmt/rest/v1/rbody/plugin.go similarity index 100% rename from mgmt/rest/rbody/plugin.go rename to mgmt/rest/v1/rbody/plugin.go diff --git a/mgmt/rest/rbody/task.go b/mgmt/rest/v1/rbody/task.go similarity index 100% rename from mgmt/rest/rbody/task.go rename to mgmt/rest/v1/rbody/task.go diff --git a/mgmt/rest/rbody/tribe.go b/mgmt/rest/v1/rbody/tribe.go similarity index 100% rename from mgmt/rest/rbody/tribe.go rename to mgmt/rest/v1/rbody/tribe.go diff --git a/mgmt/rest/task.go b/mgmt/rest/v1/task.go similarity index 70% rename from mgmt/rest/task.go rename to mgmt/rest/v1/task.go index 9f5ba590d..90da2182d 100644 --- a/mgmt/rest/task.go +++ b/mgmt/rest/v1/task.go @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rest +package v1 import ( "errors" @@ -28,10 +28,9 @@ import ( "time" log "github.com/Sirupsen/logrus" - "github.com/julienschmidt/httprouter" - "github.com/intelsdi-x/snap/core" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" + "github.com/julienschmidt/httprouter" ) var ( @@ -41,21 +40,23 @@ var ( ErrStreamingUnsupported = errors.New("Streaming unsupported") ErrTaskNotFound = errors.New("Task not found") ErrTaskDisabledNotRunnable = errors.New("Task is disabled. Cannot be started") + ErrNoActionSpecified = errors.New("No action was specified in the request") + ErrWrongAction = errors.New("Wrong action requested") ) -func (s *Server) addTask(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - task, err := core.CreateTaskFromContent(r.Body, nil, s.mt.CreateTask) +func (s *apiV1) addTask(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + task, err := core.CreateTaskFromContent(r.Body, nil, s.taskManager.CreateTask) if err != nil { - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } taskB := rbody.AddSchedulerTaskFromTask(task) - taskB.Href = taskURI(r.Host, task) - respond(201, taskB, w) + taskB.Href = taskURI(r.Host, version, task) + rbody.Write(201, taskB, w) } -func (s *Server) getTasks(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - sts := s.mt.GetTasks() +func (s *apiV1) getTasks(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + sts := s.taskManager.GetTasks() tasks := &rbody.ScheduledTaskListReturned{} tasks.ScheduledTasks = make([]rbody.ScheduledTask, len(sts)) @@ -63,27 +64,27 @@ func (s *Server) getTasks(w http.ResponseWriter, r *http.Request, _ httprouter.P i := 0 for _, t := range sts { tasks.ScheduledTasks[i] = *rbody.SchedulerTaskFromTask(t) - tasks.ScheduledTasks[i].Href = taskURI(r.Host, t) + tasks.ScheduledTasks[i].Href = taskURI(r.Host, version, t) i++ } sort.Sort(tasks) - respond(200, tasks, w) + rbody.Write(200, tasks, w) } -func (s *Server) getTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) getTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { id := p.ByName("id") - t, err1 := s.mt.GetTask(id) + t, err1 := s.taskManager.GetTask(id) if err1 != nil { - respond(404, rbody.FromError(err1), w) + rbody.Write(404, rbody.FromError(err1), w) return } task := &rbody.ScheduledTaskReturned{} task.AddScheduledTask = *rbody.AddSchedulerTaskFromTask(t) - task.Href = taskURI(r.Host, t) - respond(200, task, w) + task.Href = taskURI(r.Host, version, t) + rbody.Write(200, task, w) } -func (s *Server) watchTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) watchTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { s.wg.Add(1) defer s.wg.Done() logger := log.WithFields(log.Fields{ @@ -101,13 +102,13 @@ func (s *Server) watchTask(w http.ResponseWriter, r *http.Request, p httprouter. alive: true, mChan: make(chan rbody.StreamedTaskEvent), } - tc, err1 := s.mt.WatchTask(id, tw) + tc, err1 := s.taskManager.WatchTask(id, tw) if err1 != nil { if strings.Contains(err1.Error(), ErrTaskNotFound.Error()) { - respond(404, rbody.FromError(err1), w) + rbody.Write(404, rbody.FromError(err1), w) return } - respond(500, rbody.FromError(err1), w) + rbody.Write(500, rbody.FromError(err1), w) return } @@ -121,7 +122,7 @@ func (s *Server) watchTask(w http.ResponseWriter, r *http.Request, p httprouter. flusher, ok := w.(http.Flusher) if !ok { // This only works on ResponseWriters that support streaming - respond(500, rbody.FromError(ErrStreamingUnsupported), w) + rbody.Write(500, rbody.FromError(ErrStreamingUnsupported), w) return } // send initial stream open event @@ -156,7 +157,7 @@ func (s *Server) watchTask(w http.ResponseWriter, r *http.Request, p httprouter. // Close out watcher removing it from the scheduler tc.Close() // exit since this client is no longer listening - respond(200, &rbody.ScheduledTaskWatchingEnded{}, w) + rbody.Write(200, &rbody.ScheduledTaskWatchingEnded{}, w) } // If we are at least above our minimum buffer time we flush to send if time.Now().Sub(t).Seconds() > StreamingBufferWindow { @@ -172,7 +173,7 @@ func (s *Server) watchTask(w http.ResponseWriter, r *http.Request, p httprouter. // Close out watcher removing it from the scheduler tc.Close() // exit since this client is no longer listening - respond(200, &rbody.ScheduledTaskWatchingEnded{}, w) + rbody.Write(200, &rbody.ScheduledTaskWatchingEnded{}, w) return case <-s.killChan: logger.WithFields(log.Fields{ @@ -183,74 +184,74 @@ func (s *Server) watchTask(w http.ResponseWriter, r *http.Request, p httprouter. // Close out watcher removing it from the scheduler tc.Close() // exit since this client is no longer listening - respond(200, &rbody.ScheduledTaskWatchingEnded{}, w) + rbody.Write(200, &rbody.ScheduledTaskWatchingEnded{}, w) return } } } -func (s *Server) startTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) startTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { id := p.ByName("id") - errs := s.mt.StartTask(id) + errs := s.taskManager.StartTask(id) if errs != nil { if strings.Contains(errs[0].Error(), ErrTaskNotFound.Error()) { - respond(404, rbody.FromSnapErrors(errs), w) + rbody.Write(404, rbody.FromSnapErrors(errs), w) return } if strings.Contains(errs[0].Error(), ErrTaskDisabledNotRunnable.Error()) { - respond(409, rbody.FromSnapErrors(errs), w) + rbody.Write(409, rbody.FromSnapErrors(errs), w) return } - respond(500, rbody.FromSnapErrors(errs), w) + rbody.Write(500, rbody.FromSnapErrors(errs), w) return } // TODO should return resource - respond(200, &rbody.ScheduledTaskStarted{ID: id}, w) + rbody.Write(200, &rbody.ScheduledTaskStarted{ID: id}, w) } -func (s *Server) stopTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) stopTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { id := p.ByName("id") - errs := s.mt.StopTask(id) + errs := s.taskManager.StopTask(id) if errs != nil { if strings.Contains(errs[0].Error(), ErrTaskNotFound.Error()) { - respond(404, rbody.FromSnapErrors(errs), w) + rbody.Write(404, rbody.FromSnapErrors(errs), w) return } - respond(500, rbody.FromSnapErrors(errs), w) + rbody.Write(500, rbody.FromSnapErrors(errs), w) return } - respond(200, &rbody.ScheduledTaskStopped{ID: id}, w) + rbody.Write(200, &rbody.ScheduledTaskStopped{ID: id}, w) } -func (s *Server) removeTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) removeTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { id := p.ByName("id") - err := s.mt.RemoveTask(id) + err := s.taskManager.RemoveTask(id) if err != nil { if strings.Contains(err.Error(), ErrTaskNotFound.Error()) { - respond(404, rbody.FromError(err), w) + rbody.Write(404, rbody.FromError(err), w) return } - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } - respond(200, &rbody.ScheduledTaskRemoved{ID: id}, w) + rbody.Write(200, &rbody.ScheduledTaskRemoved{ID: id}, w) } //enableTask changes the task state from Disabled to Stopped -func (s *Server) enableTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) enableTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { id := p.ByName("id") - tsk, err := s.mt.EnableTask(id) + tsk, err := s.taskManager.EnableTask(id) if err != nil { if strings.Contains(err.Error(), ErrTaskNotFound.Error()) { - respond(404, rbody.FromError(err), w) + rbody.Write(404, rbody.FromError(err), w) return } - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } task := &rbody.ScheduledTaskEnabled{} task.AddScheduledTask = *rbody.AddSchedulerTaskFromTask(tsk) - respond(200, task, w) + rbody.Write(200, task, w) } type TaskWatchHandler struct { @@ -295,6 +296,6 @@ func (t *TaskWatchHandler) CatchTaskDisabled(why string) { } } -func taskURI(host string, t core.Task) string { - return fmt.Sprintf("%s://%s/v1/tasks/%s", protocolPrefix, host, t.ID()) +func taskURI(host, version string, t core.Task) string { + return fmt.Sprintf("%s://%s/%s/tasks/%s", protocolPrefix, host, version, t.ID()) } diff --git a/mgmt/rest/tribe.go b/mgmt/rest/v1/tribe.go similarity index 63% rename from mgmt/rest/tribe.go rename to mgmt/rest/v1/tribe.go index a9073247d..2ad32a1d1 100644 --- a/mgmt/rest/tribe.go +++ b/mgmt/rest/v1/tribe.go @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rest +package v1 import ( "encoding/json" @@ -28,7 +28,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/intelsdi-x/snap/core/serror" - "github.com/intelsdi-x/snap/mgmt/rest/rbody" + "github.com/intelsdi-x/snap/mgmt/rest/v1/rbody" "github.com/julienschmidt/httprouter" ) @@ -42,75 +42,75 @@ var ( ErrMemberNotFound = errors.New("Member not found") ) -func (s *Server) getAgreements(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { +func (s *apiV1) getAgreements(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { res := &rbody.TribeListAgreement{} - res.Agreements = s.tr.GetAgreements() - respond(200, res, w) + res.Agreements = s.tribeManager.GetAgreements() + rbody.Write(200, res, w) } -func (s *Server) getAgreement(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) getAgreement(w http.ResponseWriter, r *http.Request, p httprouter.Params) { tribeLogger = tribeLogger.WithField("_block", "getAgreement") name := p.ByName("name") - if _, ok := s.tr.GetAgreements()[name]; !ok { + if _, ok := s.tribeManager.GetAgreements()[name]; !ok { fields := map[string]interface{}{ "agreement_name": name, } tribeLogger.WithFields(fields).Error(ErrAgreementDoesNotExist) - respond(400, rbody.FromSnapError(serror.New(ErrAgreementDoesNotExist, fields)), w) + rbody.Write(400, rbody.FromSnapError(serror.New(ErrAgreementDoesNotExist, fields)), w) return } a := &rbody.TribeGetAgreement{} var serr serror.SnapError - a.Agreement, serr = s.tr.GetAgreement(name) + a.Agreement, serr = s.tribeManager.GetAgreement(name) if serr != nil { tribeLogger.Error(serr) - respond(400, rbody.FromSnapError(serr), w) + rbody.Write(400, rbody.FromSnapError(serr), w) return } - respond(200, a, w) + rbody.Write(200, a, w) } -func (s *Server) deleteAgreement(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) deleteAgreement(w http.ResponseWriter, r *http.Request, p httprouter.Params) { tribeLogger = tribeLogger.WithField("_block", "deleteAgreement") name := p.ByName("name") - if _, ok := s.tr.GetAgreements()[name]; !ok { + if _, ok := s.tribeManager.GetAgreements()[name]; !ok { fields := map[string]interface{}{ "agreement_name": name, } tribeLogger.WithFields(fields).Error(ErrAgreementDoesNotExist) - respond(400, rbody.FromSnapError(serror.New(ErrAgreementDoesNotExist, fields)), w) + rbody.Write(400, rbody.FromSnapError(serror.New(ErrAgreementDoesNotExist, fields)), w) return } var serr serror.SnapError - serr = s.tr.RemoveAgreement(name) + serr = s.tribeManager.RemoveAgreement(name) if serr != nil { tribeLogger.Error(serr) - respond(400, rbody.FromSnapError(serr), w) + rbody.Write(400, rbody.FromSnapError(serr), w) return } a := &rbody.TribeDeleteAgreement{} - a.Agreements = s.tr.GetAgreements() - respond(200, a, w) + a.Agreements = s.tribeManager.GetAgreements() + rbody.Write(200, a, w) } -func (s *Server) joinAgreement(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) joinAgreement(w http.ResponseWriter, r *http.Request, p httprouter.Params) { tribeLogger = tribeLogger.WithField("_block", "joinAgreement") name := p.ByName("name") - if _, ok := s.tr.GetAgreements()[name]; !ok { + if _, ok := s.tribeManager.GetAgreements()[name]; !ok { fields := map[string]interface{}{ "agreement_name": name, } tribeLogger.WithFields(fields).Error(ErrAgreementDoesNotExist) - respond(400, rbody.FromSnapError(serror.New(ErrAgreementDoesNotExist, fields)), w) + rbody.Write(400, rbody.FromSnapError(serror.New(ErrAgreementDoesNotExist, fields)), w) return } b, err := ioutil.ReadAll(r.Body) if err != nil { tribeLogger.Error(err) - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } @@ -125,37 +125,37 @@ func (s *Server) joinAgreement(w http.ResponseWriter, r *http.Request, p httprou } se := serror.New(ErrInvalidJSON, fields) tribeLogger.WithFields(fields).Error(ErrInvalidJSON) - respond(400, rbody.FromSnapError(se), w) + rbody.Write(400, rbody.FromSnapError(se), w) return } - serr := s.tr.JoinAgreement(name, m.MemberName) + serr := s.tribeManager.JoinAgreement(name, m.MemberName) if serr != nil { tribeLogger.Error(serr) - respond(400, rbody.FromSnapError(serr), w) + rbody.Write(400, rbody.FromSnapError(serr), w) return } - agreement, _ := s.tr.GetAgreement(name) - respond(200, &rbody.TribeJoinAgreement{Agreement: agreement}, w) + agreement, _ := s.tribeManager.GetAgreement(name) + rbody.Write(200, &rbody.TribeJoinAgreement{Agreement: agreement}, w) } -func (s *Server) leaveAgreement(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) leaveAgreement(w http.ResponseWriter, r *http.Request, p httprouter.Params) { tribeLogger = tribeLogger.WithField("_block", "leaveAgreement") name := p.ByName("name") - if _, ok := s.tr.GetAgreements()[name]; !ok { + if _, ok := s.tribeManager.GetAgreements()[name]; !ok { fields := map[string]interface{}{ "agreement_name": name, } tribeLogger.WithFields(fields).Error(ErrAgreementDoesNotExist) - respond(400, rbody.FromSnapError(serror.New(ErrAgreementDoesNotExist, fields)), w) + rbody.Write(400, rbody.FromSnapError(serror.New(ErrAgreementDoesNotExist, fields)), w) return } b, err := ioutil.ReadAll(r.Body) if err != nil { tribeLogger.Error(err) - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } @@ -170,35 +170,35 @@ func (s *Server) leaveAgreement(w http.ResponseWriter, r *http.Request, p httpro } se := serror.New(ErrInvalidJSON, fields) tribeLogger.WithFields(fields).Error(ErrInvalidJSON) - respond(400, rbody.FromSnapError(se), w) + rbody.Write(400, rbody.FromSnapError(se), w) return } - serr := s.tr.LeaveAgreement(name, m.MemberName) + serr := s.tribeManager.LeaveAgreement(name, m.MemberName) if serr != nil { tribeLogger.Error(serr) - respond(400, rbody.FromSnapError(serr), w) + rbody.Write(400, rbody.FromSnapError(serr), w) return } - agreement, _ := s.tr.GetAgreement(name) - respond(200, &rbody.TribeLeaveAgreement{Agreement: agreement}, w) + agreement, _ := s.tribeManager.GetAgreement(name) + rbody.Write(200, &rbody.TribeLeaveAgreement{Agreement: agreement}, w) } -func (s *Server) getMembers(w http.ResponseWriter, r *http.Request, p httprouter.Params) { - members := s.tr.GetMembers() - respond(200, &rbody.TribeMemberList{Members: members}, w) +func (s *apiV1) getMembers(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + members := s.tribeManager.GetMembers() + rbody.Write(200, &rbody.TribeMemberList{Members: members}, w) } -func (s *Server) getMember(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) getMember(w http.ResponseWriter, r *http.Request, p httprouter.Params) { tribeLogger = tribeLogger.WithField("_block", "getMember") name := p.ByName("name") - member := s.tr.GetMember(name) + member := s.tribeManager.GetMember(name) if member == nil { fields := map[string]interface{}{ "name": name, } tribeLogger.WithFields(fields).Error(ErrMemberNotFound) - respond(404, rbody.FromSnapError(serror.New(ErrMemberNotFound, fields)), w) + rbody.Write(404, rbody.FromSnapError(serror.New(ErrMemberNotFound, fields)), w) return } resp := &rbody.TribeMemberShow{ @@ -213,15 +213,15 @@ func (s *Server) getMember(w http.ResponseWriter, r *http.Request, p httprouter. resp.TaskAgreements = append(resp.TaskAgreements, k) } } - respond(200, resp, w) + rbody.Write(200, resp, w) } -func (s *Server) addAgreement(w http.ResponseWriter, r *http.Request, p httprouter.Params) { +func (s *apiV1) addAgreement(w http.ResponseWriter, r *http.Request, p httprouter.Params) { tribeLogger = tribeLogger.WithField("_block", "addAgreement") b, err := ioutil.ReadAll(r.Body) if err != nil { tribeLogger.Error(err) - respond(500, rbody.FromError(err), w) + rbody.Write(500, rbody.FromError(err), w) return } @@ -234,7 +234,7 @@ func (s *Server) addAgreement(w http.ResponseWriter, r *http.Request, p httprout } se := serror.New(ErrInvalidJSON, fields) tribeLogger.WithFields(fields).Error(ErrInvalidJSON) - respond(400, rbody.FromSnapError(se), w) + rbody.Write(400, rbody.FromSnapError(se), w) return } @@ -244,19 +244,19 @@ func (s *Server) addAgreement(w http.ResponseWriter, r *http.Request, p httprout } se := serror.New(ErrInvalidJSON, fields) tribeLogger.WithFields(fields).Error(ErrInvalidJSON) - respond(400, rbody.FromSnapError(se), w) + rbody.Write(400, rbody.FromSnapError(se), w) return } - err = s.tr.AddAgreement(a.Name) + err = s.tribeManager.AddAgreement(a.Name) if err != nil { tribeLogger.WithField("agreement-name", a.Name).Error(err) - respond(400, rbody.FromError(err), w) + rbody.Write(400, rbody.FromError(err), w) return } res := &rbody.TribeAddAgreement{} - res.Agreements = s.tr.GetAgreements() + res.Agreements = s.tribeManager.GetAgreements() - respond(200, res, w) + rbody.Write(200, res, w) } diff --git a/mgmt/rest/v2/api.go b/mgmt/rest/v2/api.go new file mode 100644 index 000000000..3fde05da8 --- /dev/null +++ b/mgmt/rest/v2/api.go @@ -0,0 +1,114 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 v2 + +import ( + "encoding/json" + "sync" + + "net/http" + + log "github.com/Sirupsen/logrus" + "github.com/intelsdi-x/snap/mgmt/rest/api" + "github.com/urfave/negroni" +) + +const ( + version = "v2" + prefix = "/" + version +) + +var ( + restLogger = log.WithField("_module", "_mgmt-rest-v2") + protocolPrefix = "http" +) + +type apiV2 struct { + metricManager api.Metrics + taskManager api.Tasks + configManager api.Config + + wg *sync.WaitGroup + killChan chan struct{} +} + +func New(wg *sync.WaitGroup, killChan chan struct{}, protocol string) *apiV2 { + protocolPrefix = protocol + return &apiV2{wg: wg, killChan: killChan} +} + +func (s *apiV2) GetRoutes() []api.Route { + routes := []api.Route{ + // plugin routes + api.Route{Method: "GET", Path: prefix + "/plugins", Handle: s.getPlugins}, + api.Route{Method: "GET", Path: prefix + "/plugins/:type/:name/:version", Handle: s.getPlugin}, + api.Route{Method: "POST", Path: prefix + "/plugins", Handle: s.loadPlugin}, + api.Route{Method: "DELETE", Path: prefix + "/plugins/:type/:name/:version", Handle: s.unloadPlugin}, + + api.Route{Method: "GET", Path: prefix + "/plugins/:type/:name/:version/config", Handle: s.getPluginConfigItem}, + api.Route{Method: "PUT", Path: prefix + "/plugins/:type/:name/:version/config", Handle: s.setPluginConfigItem}, + api.Route{Method: "DELETE", Path: prefix + "/plugins/:type/:name/:version/config", Handle: s.deletePluginConfigItem}, + + // metric routes + api.Route{Method: "GET", Path: prefix + "/metrics", Handle: s.getMetrics}, + + // task routes + api.Route{Method: "GET", Path: prefix + "/tasks", Handle: s.getTasks}, + api.Route{Method: "GET", Path: prefix + "/tasks/:id", Handle: s.getTask}, + api.Route{Method: "GET", Path: prefix + "/tasks/:id/watch", Handle: s.watchTask}, + api.Route{Method: "POST", Path: prefix + "/tasks", Handle: s.addTask}, + api.Route{Method: "PUT", Path: prefix + "/tasks/:id", Handle: s.updateTaskState}, + api.Route{Method: "DELETE", Path: prefix + "/tasks/:id", Handle: s.removeTask}, + } + return routes +} + +func (s *apiV2) BindMetricManager(metricManager api.Metrics) { + s.metricManager = metricManager +} + +func (s *apiV2) BindTaskManager(taskManager api.Tasks) { + s.taskManager = taskManager +} + +func (s *apiV2) BindTribeManager(tribeManager api.Tribe) {} + +func (s *apiV2) BindConfigManager(configManager api.Config) { + s.configManager = configManager +} + +func Write(code int, body interface{}, w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json; version=2; charset=utf-8") + w.Header().Set("Version", "beta") + + if !w.(negroni.ResponseWriter).Written() { + w.WriteHeader(code) + } + + if body != nil { + e := json.NewEncoder(w) + e.SetIndent("", " ") + e.SetEscapeHTML(false) + err := e.Encode(body) + if err != nil { + restLogger.Fatalln(err) + } + } +} diff --git a/mgmt/rest/v2/config.go b/mgmt/rest/v2/config.go new file mode 100644 index 000000000..134d6f5b1 --- /dev/null +++ b/mgmt/rest/v2/config.go @@ -0,0 +1,161 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 v2 + +import ( + "net/http" + "strconv" + + "github.com/intelsdi-x/snap/control/plugin/cpolicy" + "github.com/intelsdi-x/snap/core" + "github.com/intelsdi-x/snap/core/cdata" + "github.com/julienschmidt/httprouter" +) + +type PolicyTable cpolicy.RuleTable + +type PolicyTableSlice []cpolicy.RuleTable + +// cdata.ConfigDataNode implements it's own UnmarshalJSON +type PluginConfigItem struct { + cdata.ConfigDataNode +} + +func (s *apiV2) getPluginConfigItem(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + var err error + styp := p.ByName("type") + if styp == "" { + cdn := s.configManager.GetPluginConfigDataNodeAll() + item := &PluginConfigItem{ConfigDataNode: cdn} + Write(200, item, w) + return + } + + typ, err := getPluginType(styp) + if err != nil { + Write(400, FromError(err), w) + return + } + + name := p.ByName("name") + sver := p.ByName("version") + iver := -2 + if sver != "" { + if iver, err = strconv.Atoi(sver); err != nil { + Write(400, FromError(err), w) + return + } + } + + cdn := s.configManager.GetPluginConfigDataNode(typ, name, iver) + item := &PluginConfigItem{ConfigDataNode: cdn} + Write(200, item, w) +} + +func (s *apiV2) deletePluginConfigItem(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + var err error + var typ core.PluginType + styp := p.ByName("type") + if styp != "" { + typ, err = getPluginType(styp) + if err != nil { + Write(400, FromError(err), w) + return + } + } + + name := p.ByName("name") + sver := p.ByName("version") + iver := -2 + if sver != "" { + if iver, err = strconv.Atoi(sver); err != nil { + Write(400, FromError(err), w) + return + } + } + + src := []string{} + errCode, err := core.UnmarshalBody(&src, r.Body) + if errCode != 0 && err != nil { + Write(400, FromError(err), w) + return + } + + var res cdata.ConfigDataNode + if styp == "" { + res = s.configManager.DeletePluginConfigDataNodeFieldAll(src...) + } else { + res = s.configManager.DeletePluginConfigDataNodeField(typ, name, iver, src...) + } + + item := &PluginConfigItem{ConfigDataNode: res} + Write(200, item, w) +} + +func (s *apiV2) setPluginConfigItem(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + var err error + var typ core.PluginType + styp := p.ByName("type") + if styp != "" { + typ, err = getPluginType(styp) + if err != nil { + Write(400, FromError(err), w) + return + } + } + + name := p.ByName("name") + sver := p.ByName("version") + iver := -2 + if sver != "" { + if iver, err = strconv.Atoi(sver); err != nil { + Write(400, FromError(err), w) + return + } + } + + src := cdata.NewNode() + errCode, err := core.UnmarshalBody(src, r.Body) + if errCode != 0 && err != nil { + Write(400, FromError(err), w) + return + } + + var res cdata.ConfigDataNode + if styp == "" { + res = s.configManager.MergePluginConfigDataNodeAll(src) + } else { + res = s.configManager.MergePluginConfigDataNode(typ, name, iver, src) + } + + item := &PluginConfigItem{ConfigDataNode: res} + Write(200, item, w) +} + +func getPluginType(t string) (core.PluginType, error) { + if ityp, err := strconv.Atoi(t); err == nil { + return core.PluginType(ityp), nil + } + ityp, err := core.ToPluginType(t) + if err != nil { + return core.PluginType(-1), err + } + return ityp, nil +} diff --git a/mgmt/rest/v2/error.go b/mgmt/rest/v2/error.go new file mode 100644 index 000000000..61bd83eee --- /dev/null +++ b/mgmt/rest/v2/error.go @@ -0,0 +1,76 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2015 Intel Corporation + +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 v2 + +import ( + "fmt" + + "errors" + + "github.com/intelsdi-x/snap/core/serror" +) + +const ( + ErrPluginAlreadyLoaded = "plugin is already loaded" + ErrTaskNotFound = "task not found" + ErrTaskDisabledNotRunnable = "task is disabled" +) + +var ( + ErrPluginNotFound = errors.New("plugin not found") + ErrStreamingUnsupported = errors.New("streaming unsupported") + ErrNoActionSpecified = errors.New("no action was specified in the request") + ErrWrongAction = errors.New("wrong action requested") +) + +// Unsuccessful generic response to a failed API call +type Error struct { + ErrorMessage string `json:"message"` + Fields map[string]string `json:"fields"` +} + +func FromSnapError(pe serror.SnapError) *Error { + e := &Error{ErrorMessage: pe.Error(), Fields: make(map[string]string)} + // Convert into string format + for k, v := range pe.Fields() { + e.Fields[k] = fmt.Sprint(v) + } + return e +} + +func FromSnapErrors(errs []serror.SnapError) *Error { + fields := make(map[string]string) + var msg string + for i, err := range errs { + for k, v := range err.Fields() { + fields[fmt.Sprintf("%s_err_%d", k, i)] = fmt.Sprint(v) + } + msg = msg + fmt.Sprintf("error %d: %s ", i, err.Error()) + } + return &Error{ + ErrorMessage: msg, + Fields: fields, + } +} + +func FromError(err error) *Error { + e := &Error{ErrorMessage: err.Error(), Fields: make(map[string]string)} + return e +} diff --git a/mgmt/rest/v2/metric.go b/mgmt/rest/v2/metric.go new file mode 100644 index 000000000..6f334cd6a --- /dev/null +++ b/mgmt/rest/v2/metric.go @@ -0,0 +1,156 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 v2 + +import ( + "fmt" + "net/http" + "sort" + "strconv" + "strings" + + "net/url" + + "github.com/intelsdi-x/snap/core" + "github.com/intelsdi-x/snap/pkg/stringutils" + "github.com/julienschmidt/httprouter" +) + +type MetricsResonse struct { + Metrics Metrics `json:"metrics,omitempty"` +} + +type Metrics []Metric + +type Metric struct { + LastAdvertisedTimestamp int64 `json:"last_advertised_timestamp,omitempty"` + Namespace string `json:"namespace,omitempty"` + Version int `json:"version,omitempty"` + Dynamic bool `json:"dynamic"` + DynamicElements []DynamicElement `json:"dynamic_elements,omitempty"` + Description string `json:"description,omitempty"` + Unit string `json:"unit,omitempty"` + Policy PolicyTableSlice `json:"policy,omitempty"` + Href string `json:"href"` +} + +type DynamicElement struct { + Index int `json:"index,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` +} + +// Used to sort the metrics before marshalling the response +func (m Metrics) Len() int { + return len(m) +} + +func (m Metrics) Less(i, j int) bool { + return (fmt.Sprintf("%s:%d", m[i].Namespace, m[i].Version)) < (fmt.Sprintf("%s:%d", m[j].Namespace, m[j].Version)) +} + +func (m Metrics) Swap(i, j int) { + m[i], m[j] = m[j], m[i] +} + +func (s *apiV2) getMetrics(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + + // If we are provided a parameter with the name 'ns' we need to + // perform a query + q := r.URL.Query() + v := q.Get("ver") + ns_query := q.Get("ns") + if ns_query != "" { + ver := 0 // 0: get all versions + if v != "" { + var err error + ver, err = strconv.Atoi(v) + if err != nil { + Write(400, FromError(err), w) + return + } + } + // strip the leading char and split on the remaining. + fc := stringutils.GetFirstChar(ns_query) + ns := strings.Split(strings.TrimLeft(ns_query, fc), fc) + if ns[len(ns)-1] == "*" { + ns = ns[:len(ns)-1] + } + + mts, err := s.metricManager.FetchMetrics(core.NewNamespace(ns...), ver) + if err != nil { + Write(404, FromError(err), w) + return + } + respondWithMetrics(r.Host, mts, w) + return + } + + mts, err := s.metricManager.MetricCatalog() + if err != nil { + Write(500, FromError(err), w) + return + } + respondWithMetrics(r.Host, mts, w) +} + +func respondWithMetrics(host string, mts []core.CatalogedMetric, w http.ResponseWriter) { + b := MetricsResonse{Metrics: make(Metrics, 0)} + for _, m := range mts { + policies := PolicyTableSlice(m.Policy().RulesAsTable()) + dyn, indexes := m.Namespace().IsDynamic() + b.Metrics = append(b.Metrics, Metric{ + Namespace: m.Namespace().String(), + Version: m.Version(), + LastAdvertisedTimestamp: m.LastAdvertisedTime().Unix(), + Description: m.Description(), + Dynamic: dyn, + DynamicElements: getDynamicElements(m.Namespace(), indexes), + Unit: m.Unit(), + Policy: policies, + Href: catalogedMetricURI(host, m), + }) + } + sort.Sort(b.Metrics) + Write(200, b, w) +} + +func catalogedMetricURI(host string, mt core.CatalogedMetric) string { + return fmt.Sprintf("%s://%s/%s/metrics?ns=%s&ver=%d", protocolPrefix, host, version, url.QueryEscape(mt.Namespace().String()), mt.Version()) +} + +func getDynamicElements(ns core.Namespace, indexes []int) []DynamicElement { + elements := make([]DynamicElement, 0, len(indexes)) + for _, v := range indexes { + e := ns.Element(v) + elements = append(elements, DynamicElement{ + Index: v, + Name: e.Name, + Description: e.Description, + }) + } + return elements +} + +func parseNamespace(ns string) []string { + fc := stringutils.GetFirstChar(ns) + ns = strings.Trim(ns, fc) + return strings.Split(ns, fc) +} diff --git a/mgmt/rest/v2/metric_test.go b/mgmt/rest/v2/metric_test.go new file mode 100644 index 000000000..0474519ed --- /dev/null +++ b/mgmt/rest/v2/metric_test.go @@ -0,0 +1,69 @@ +// +build small + +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2015 Intel Corporation + +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 v2 + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestParseNamespace(t *testing.T) { + tcs := getNsTestCases() + + Convey("Test parseNamespace", t, func() { + for _, c := range tcs { + Convey("Test parseNamespace "+c.input, func() { + So(c.output, ShouldResemble, parseNamespace(c.input)) + }) + } + }) +} + +type nsTestCase struct { + input string + output []string +} + +func getNsTestCases() []nsTestCase { + tcs := []nsTestCase{ + { + input: "小a小b小c", + output: []string{"a", "b", "c"}}, + { + input: "%a%b%c", + output: []string{"a", "b", "c"}}, + { + input: "-aヒ-b/-c|", + output: []string{"aヒ", "b/", "c|"}}, + { + input: ">a>b=>c=", + output: []string{"a", "b=", "c="}}, + { + input: ">a>b<>c<", + output: []string{"a", "b<", "c<"}}, + { + input: "㊽a㊽b%㊽c/|", + output: []string{"a", "b%", "c/|"}}, + } + return tcs +} diff --git a/mgmt/rest/v2/mock/mock_config_manager.go b/mgmt/rest/v2/mock/mock_config_manager.go new file mode 100644 index 000000000..138ad2658 --- /dev/null +++ b/mgmt/rest/v2/mock/mock_config_manager.go @@ -0,0 +1,88 @@ +// +build legacy small medium large + +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + +Copyright 2016 Intel Corporation + +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 mock + +import ( + "github.com/intelsdi-x/snap/core" + "github.com/intelsdi-x/snap/core/cdata" + "github.com/intelsdi-x/snap/core/ctypes" +) + +var mockConfig *cdata.ConfigDataNode + +func init() { + mockConfig = cdata.NewNode() + mockConfig.AddItem("User", ctypes.ConfigValueStr{Value: "KELLY"}) + mockConfig.AddItem("Port", ctypes.ConfigValueInt{Value: 2}) +} + +type MockConfigManager struct{} + +func (MockConfigManager) GetPluginConfigDataNode(core.PluginType, string, int) cdata.ConfigDataNode { + return *mockConfig +} +func (MockConfigManager) GetPluginConfigDataNodeAll() cdata.ConfigDataNode { + return *mockConfig +} +func (MockConfigManager) MergePluginConfigDataNode( + pluginType core.PluginType, name string, ver int, cdn *cdata.ConfigDataNode) cdata.ConfigDataNode { + return *cdn +} +func (MockConfigManager) MergePluginConfigDataNodeAll(cdn *cdata.ConfigDataNode) cdata.ConfigDataNode { + return cdata.ConfigDataNode{} +} +func (MockConfigManager) DeletePluginConfigDataNodeField( + pluginType core.PluginType, name string, ver int, fields ...string) cdata.ConfigDataNode { + for _, field := range fields { + mockConfig.DeleteItem(field) + + } + return *mockConfig +} + +func (MockConfigManager) DeletePluginConfigDataNodeFieldAll(fields ...string) cdata.ConfigDataNode { + for _, field := range fields { + mockConfig.DeleteItem(field) + + } + return *mockConfig +} + +// These constants are the expected plugin config responses from running +// rest_v2_test.go on the plugin config routes found in mgmt/rest/server.go +const ( + SET_PLUGIN_CONFIG_ITEM = `{ + "user": "Jane" +} +` + + GET_PLUGIN_CONFIG_ITEM = `{ + "Port": 2, + "User": "KELLY" +} +` + + DELETE_PLUGIN_CONFIG_ITEM = `{ + "Port": 2, + "User": "KELLY" +} +` +) diff --git a/mgmt/rest/v2/mock/mock_metric_manager.go b/mgmt/rest/v2/mock/mock_metric_manager.go new file mode 100644 index 000000000..701b46585 --- /dev/null +++ b/mgmt/rest/v2/mock/mock_metric_manager.go @@ -0,0 +1,258 @@ +// +build legacy small medium large + +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + +Copyright 2016 Intel Corporation + +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 mock + +import ( + "errors" + "time" + + "github.com/intelsdi-x/snap/control/plugin/cpolicy" + "github.com/intelsdi-x/snap/core" + "github.com/intelsdi-x/snap/core/serror" +) + +var pluginCatalog []core.CatalogedPlugin = []core.CatalogedPlugin{ + MockLoadedPlugin{MyName: "foo", MyType: "collector", MyVersion: 2}, + MockLoadedPlugin{MyName: "bar", MyType: "publisher", MyVersion: 3}, + MockLoadedPlugin{MyName: "foo", MyType: "collector", MyVersion: 4}, + MockLoadedPlugin{MyName: "baz", MyType: "publisher", MyVersion: 5}, + MockLoadedPlugin{MyName: "foo", MyType: "processor", MyVersion: 6}, + MockLoadedPlugin{MyName: "foobar", MyType: "processor", MyVersion: 1}, +} + +var metricCatalog []core.CatalogedMetric = []core.CatalogedMetric{ + MockCatalogedMetric{}, +} + +//////MockLoadedPlugin///// + +type MockLoadedPlugin struct { + MyName string + MyType string + MyVersion int +} + +func (m MockLoadedPlugin) Name() string { return m.MyName } +func (m MockLoadedPlugin) Port() string { return "" } +func (m MockLoadedPlugin) TypeName() string { return m.MyType } +func (m MockLoadedPlugin) Version() int { return m.MyVersion } +func (m MockLoadedPlugin) Plugin() string { return "" } +func (m MockLoadedPlugin) IsSigned() bool { return false } +func (m MockLoadedPlugin) Status() string { return "" } +func (m MockLoadedPlugin) PluginPath() string { return "" } +func (m MockLoadedPlugin) LoadedTimestamp() *time.Time { + t := time.Date(2016, time.September, 6, 0, 0, 0, 0, time.UTC) + return &t +} +func (m MockLoadedPlugin) Policy() *cpolicy.ConfigPolicy { return cpolicy.New() } +func (m MockLoadedPlugin) HitCount() int { return 0 } +func (m MockLoadedPlugin) LastHit() time.Time { return time.Now() } +func (m MockLoadedPlugin) ID() uint32 { return 0 } + +//////MockCatalogedMetric///// + +type MockCatalogedMetric struct{} + +func (m MockCatalogedMetric) Namespace() core.Namespace { + return core.NewNamespace("one", "two", "three") +} +func (m MockCatalogedMetric) Version() int { return 5 } +func (m MockCatalogedMetric) LastAdvertisedTime() time.Time { return time.Time{} } +func (m MockCatalogedMetric) Policy() *cpolicy.ConfigPolicyNode { return cpolicy.NewPolicyNode() } +func (m MockCatalogedMetric) Description() string { return "This Is A Description" } +func (m MockCatalogedMetric) Unit() string { return "" } + +//////MockManagesMetrics///// + +type MockManagesMetrics struct{} + +func (m MockManagesMetrics) MetricCatalog() ([]core.CatalogedMetric, error) { + return metricCatalog, nil +} +func (m MockManagesMetrics) FetchMetrics(core.Namespace, int) ([]core.CatalogedMetric, error) { + return metricCatalog, nil +} +func (m MockManagesMetrics) GetMetricVersions(core.Namespace) ([]core.CatalogedMetric, error) { + return metricCatalog, nil +} +func (m MockManagesMetrics) GetMetric(core.Namespace, int) (core.CatalogedMetric, error) { + return MockCatalogedMetric{}, nil +} +func (m MockManagesMetrics) Load(*core.RequestedPlugin) (core.CatalogedPlugin, serror.SnapError) { + return MockLoadedPlugin{"foo", "collector", 1}, nil +} +func (m MockManagesMetrics) Unload(plugin core.Plugin) (core.CatalogedPlugin, serror.SnapError) { + for _, pl := range pluginCatalog { + if plugin.Name() == pl.Name() && + plugin.Version() == pl.Version() && + plugin.TypeName() == pl.TypeName() { + return pl, nil + } + } + return nil, serror.New(errors.New("plugin not found")) +} + +func (m MockManagesMetrics) PluginCatalog() core.PluginCatalog { + return pluginCatalog +} +func (m MockManagesMetrics) AvailablePlugins() []core.AvailablePlugin { + return []core.AvailablePlugin{ + MockLoadedPlugin{MyName: "foo", MyType: "collector", MyVersion: 2}, + MockLoadedPlugin{MyName: "bar", MyType: "publisher", MyVersion: 3}, + MockLoadedPlugin{MyName: "foo", MyType: "collector", MyVersion: 4}, + MockLoadedPlugin{MyName: "baz", MyType: "publisher", MyVersion: 5}, + MockLoadedPlugin{MyName: "foo", MyType: "processor", MyVersion: 6}, + MockLoadedPlugin{MyName: "foobar", MyType: "processor", MyVersion: 1}, + } +} +func (m MockManagesMetrics) GetAutodiscoverPaths() []string { + return nil +} + +// These constants are the expected plugin responses from running +// rest_v2_test.go on the plugin routes found in mgmt/rest/server.go +const ( + GET_PLUGINS_RESPONSE = `{ + "plugins": [ + { + "name": "foo", + "version": 2, + "type": "collector", + "signed": false, + "status": "", + "loaded_timestamp": 1473120000, + "href": "http://localhost:%d/v2/plugins/collector/foo/2" + }, + { + "name": "bar", + "version": 3, + "type": "publisher", + "signed": false, + "status": "", + "loaded_timestamp": 1473120000, + "href": "http://localhost:%d/v2/plugins/publisher/bar/3" + }, + { + "name": "foo", + "version": 4, + "type": "collector", + "signed": false, + "status": "", + "loaded_timestamp": 1473120000, + "href": "http://localhost:%d/v2/plugins/collector/foo/4" + }, + { + "name": "baz", + "version": 5, + "type": "publisher", + "signed": false, + "status": "", + "loaded_timestamp": 1473120000, + "href": "http://localhost:%d/v2/plugins/publisher/baz/5" + }, + { + "name": "foo", + "version": 6, + "type": "processor", + "signed": false, + "status": "", + "loaded_timestamp": 1473120000, + "href": "http://localhost:%d/v2/plugins/processor/foo/6" + }, + { + "name": "foobar", + "version": 1, + "type": "processor", + "signed": false, + "status": "", + "loaded_timestamp": 1473120000, + "href": "http://localhost:%d/v2/plugins/processor/foobar/1" + } + ] +} +` + + GET_PLUGINS_RESPONSE_TYPE = `{ + "plugins": [ + { + "name": "foo", + "version": 2, + "type": "collector", + "signed": false, + "status": "", + "loaded_timestamp": 1473120000, + "href": "http://localhost:%d/v2/plugins/collector/foo/2" + }, + { + "name": "foo", + "version": 4, + "type": "collector", + "signed": false, + "status": "", + "loaded_timestamp": 1473120000, + "href": "http://localhost:%d/v2/plugins/collector/foo/4" + } + ] +} +` + + GET_PLUGINS_RESPONSE_TYPE_NAME = `{ + "plugins": [ + { + "name": "bar", + "version": 3, + "type": "publisher", + "signed": false, + "status": "", + "loaded_timestamp": 1473120000, + "href": "http://localhost:%d/v2/plugins/publisher/bar/3" + } + ] +} +` + + GET_PLUGINS_RESPONSE_TYPE_NAME_VERSION = `{ + "name": "bar", + "version": 3, + "type": "publisher", + "signed": false, + "status": "", + "loaded_timestamp": 1473120000, + "href": "http://localhost:%d/v2/plugins/publisher/bar/3" +} +` + + GET_METRICS_RESPONSE = `{ + "metrics": [ + { + "last_advertised_timestamp": -62135596800, + "namespace": "/one/two/three", + "version": 5, + "dynamic": false, + "description": "This Is A Description", + "href": "http://localhost:%d/v2/metrics?ns=/one/two/three&ver=5" + } + ] +} +` + + UNLOAD_PLUGIN_RESPONSE = `` +) diff --git a/mgmt/rest/v2/mock/mock_task_manager.go b/mgmt/rest/v2/mock/mock_task_manager.go new file mode 100644 index 000000000..4e75ff90f --- /dev/null +++ b/mgmt/rest/v2/mock/mock_task_manager.go @@ -0,0 +1,264 @@ +// +build legacy small medium large + +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + +Copyright 2016 Intel Corporation + +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 mock + +import ( + "time" + + "github.com/intelsdi-x/snap/core" + "github.com/intelsdi-x/snap/core/serror" + "github.com/intelsdi-x/snap/pkg/schedule" + "github.com/intelsdi-x/snap/scheduler/wmap" +) + +var taskCatalog map[string]core.Task = map[string]core.Task{ + "Task1": &mockTask{ + MyID: "qwertyuiop", + MyName: "TASK1.0", + MyDeadline: "4", + MyCreationTimestamp: time.Now().Unix(), + MyLastRunTimestamp: time.Now().Unix(), + MyHitCount: 44, + MyMissCount: 8, + MyState: "failed", + MyHref: "http://localhost:8181/v2/tasks/qwertyuiop"}, + "Task2": &mockTask{ + MyID: "asdfghjkl", + MyName: "TASK2.0", + MyDeadline: "4", + MyCreationTimestamp: time.Now().Unix(), + MyLastRunTimestamp: time.Now().Unix(), + MyHitCount: 33, + MyMissCount: 7, + MyState: "passed", + MyHref: "http://localhost:8181/v2/tasks/asdfghjkl"}} + +type mockTask struct { + MyID string `json:"id"` + MyName string `json:"name"` + MyDeadline string `json:"deadline"` + MyWorkflow *wmap.WorkflowMap `json:"workflow,omitempty"` + MySchedule *core.Schedule `json:"schedule,omitempty"` + MyCreationTimestamp int64 `json:"creation_timestamp,omitempty"` + MyLastRunTimestamp int64 `json:"last_run_timestamp,omitempty"` + MyHitCount int `json:"hit_count,omitempty"` + MyMissCount int `json:"miss_count,omitempty"` + MyFailedCount int `json:"failed_count,omitempty"` + MyLastFailureMessage string `json:"last_failure_message,omitempty"` + MyState string `json:"task_state"` + MyHref string `json:"href"` +} + +func (t *mockTask) ID() string { return t.MyID } +func (t *mockTask) State() core.TaskState { return core.TaskSpinning } +func (t *mockTask) HitCount() uint { return 0 } +func (t *mockTask) GetName() string { return t.MyName } +func (t *mockTask) SetName(string) { return } +func (t *mockTask) SetID(string) { return } +func (t *mockTask) MissedCount() uint { return 0 } +func (t *mockTask) FailedCount() uint { return 0 } +func (t *mockTask) LastFailureMessage() string { return "" } +func (t *mockTask) LastRunTime() *time.Time { return &time.Time{} } +func (t *mockTask) CreationTime() *time.Time { return &time.Time{} } +func (t *mockTask) DeadlineDuration() time.Duration { return 4 } +func (t *mockTask) SetDeadlineDuration(time.Duration) { return } +func (t *mockTask) SetTaskID(id string) { return } +func (t *mockTask) SetStopOnFailure(int) { return } +func (t *mockTask) GetStopOnFailure() int { return 0 } +func (t *mockTask) Option(...core.TaskOption) core.TaskOption { + return core.TaskDeadlineDuration(0) +} +func (t *mockTask) WMap() *wmap.WorkflowMap { + return wmap.NewWorkflowMap() +} +func (t *mockTask) Schedule() schedule.Schedule { + return schedule.NewSimpleSchedule(time.Second * 1) +} +func (t *mockTask) MaxFailures() int { return 10 } + +type MockTaskManager struct{} + +func (m *MockTaskManager) GetTask(id string) (core.Task, error) { + href := "http://localhost:8181/v2/tasks/" + id + return &mockTask{ + MyID: id, + MyName: "NewTaskCreated", + MyCreationTimestamp: time.Now().Unix(), + MyLastRunTimestamp: time.Now().Unix(), + MyHitCount: 22, + MyMissCount: 4, + MyState: "failed", + MyHref: href}, nil +} +func (m *MockTaskManager) CreateTask( + sch schedule.Schedule, + wmap *wmap.WorkflowMap, + start bool, + opts ...core.TaskOption) (core.Task, core.TaskErrors) { + return &mockTask{ + MyID: "MyTaskID", + MyName: "NewTaskCreated", + MySchedule: &core.Schedule{}, + MyCreationTimestamp: time.Now().Unix(), + MyLastRunTimestamp: time.Now().Unix(), + MyHitCount: 99, + MyMissCount: 5, + MyState: "failed", + MyHref: "http://localhost:8181/v2/tasks/MyTaskID"}, nil +} +func (m *MockTaskManager) GetTasks() map[string]core.Task { + return taskCatalog +} +func (m *MockTaskManager) StartTask(id string) []serror.SnapError { return nil } +func (m *MockTaskManager) StopTask(id string) []serror.SnapError { return nil } +func (m *MockTaskManager) RemoveTask(id string) error { return nil } +func (m *MockTaskManager) WatchTask(id string, handler core.TaskWatcherHandler) (core.TaskWatcherCloser, error) { + return nil, nil +} +func (m *MockTaskManager) EnableTask(id string) (core.Task, error) { + return &mockTask{ + MyID: "alskdjf", + MyName: "Task2", + MyCreationTimestamp: time.Now().Unix(), + MyLastRunTimestamp: time.Now().Unix(), + MyHitCount: 44, + MyMissCount: 8, + MyState: "failed", + MyHref: "http://localhost:8181/v2/tasks/alskdjf"}, nil +} + +// Mock task used in the 'Add tasks' test in rest_v2_test.go +const TASK = `{ + "version": 1, + "schedule": { + "type": "simple", + "interval": "1s" + }, + "max-failures": 10, + "workflow": { + "collect": { + "metrics": { + "/one/two/three": {} + } + } + } +} +` + +// These constants are the expected responses from running the task tests in +// rest_v2_test.go on the task routes found in mgmt/rest/server.go +const ( + GET_TASKS_RESPONSE = `{ + "tasks": [ + { + "id": "qwertyuiop", + "name": "TASK1.0", + "deadline": "4ns", + "creation_timestamp": -62135596800, + "last_run_timestamp": -1, + "task_state": "Running", + "href": "http://localhost:%d/v2/tasks/qwertyuiop" + }, + { + "id": "asdfghjkl", + "name": "TASK2.0", + "deadline": "4ns", + "creation_timestamp": -62135596800, + "last_run_timestamp": -1, + "task_state": "Running", + "href": "http://localhost:%d/v2/tasks/asdfghjkl" + } + ] +} +` + + GET_TASKS_RESPONSE2 = `{ + "tasks": [ + { + "id": "asdfghjkl", + "name": "TASK2.0", + "deadline": "4ns", + "creation_timestamp": -62135596800, + "last_run_timestamp": -1, + "task_state": "Running", + "href": "http://localhost:%d/v2/tasks/asdfghjkl" + }, + { + "id": "qwertyuiop", + "name": "TASK1.0", + "deadline": "4ns", + "creation_timestamp": -62135596800, + "last_run_timestamp": -1, + "task_state": "Running", + "href": "http://localhost:%d/v2/tasks/qwertyuiop" + } + ] +} +` + + GET_TASK_RESPONSE = `{ + "id": ":1234", + "name": "NewTaskCreated", + "deadline": "4ns", + "workflow": { + "collect": { + "metrics": {} + } + }, + "schedule": { + "type": "simple", + "interval": "1s" + }, + "creation_timestamp": -62135596800, + "last_run_timestamp": -1, + "task_state": "Running", + "href": "http://localhost:%d/v2/tasks/:1234" +} +` + + ADD_TASK_RESPONSE = `{ + "id": "MyTaskID", + "name": "NewTaskCreated", + "deadline": "4ns", + "workflow": { + "collect": { + "metrics": {} + } + }, + "schedule": { + "type": "simple", + "interval": "1s" + }, + "creation_timestamp": -62135596800, + "last_run_timestamp": -1, + "task_state": "Running", + "href": "http://localhost:%d/v2/tasks/MyTaskID" +} +` + + START_TASK_RESPONSE_ID_START = `` + + STOP_TASK_RESPONSE_ID_STOP = `` + + ENABLE_TASK_RESPONSE_ID_ENABLE = `` + + REMOVE_TASK_RESPONSE_ID = `` +) diff --git a/mgmt/rest/v2/plugin.go b/mgmt/rest/v2/plugin.go new file mode 100644 index 000000000..78680ef3b --- /dev/null +++ b/mgmt/rest/v2/plugin.go @@ -0,0 +1,448 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 v2 + +import ( + "compress/gzip" + "crypto/sha256" + "errors" + "fmt" + "io" + "io/ioutil" + "mime" + "mime/multipart" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + + "path" + "runtime" + + log "github.com/Sirupsen/logrus" + "github.com/intelsdi-x/snap/control" + "github.com/intelsdi-x/snap/core" + "github.com/intelsdi-x/snap/core/serror" + "github.com/julienschmidt/httprouter" +) + +type PluginsResponse struct { + RunningPlugins []RunningPlugin `json:"running_plugins,omitempty"` + Plugins []Plugin `json:"plugins,omitempty"` +} + +type Plugin struct { + Name string `json:"name"` + Version int `json:"version"` + Type string `json:"type"` + Signed bool `json:"signed"` + Status string `json:"status"` + LoadedTimestamp int64 `json:"loaded_timestamp"` + Href string `json:"href"` + ConfigPolicy []PolicyTable `json:"policy,omitempty"` +} + +type RunningPlugin struct { + Name string `json:"name"` + Version int `json:"version"` + Type string `json:"type"` + HitCount int `json:"hitcount"` + LastHitTimestamp int64 `json:"last_hit_timestamp"` + ID uint32 `json:"id"` + Href string `json:"href"` + PprofPort string `json:"pprof_port"` +} + +type plugin struct { + name string + version int + pluginType string +} + +func (p *plugin) Name() string { + return p.name +} + +func (p *plugin) Version() int { + return p.version +} + +func (p *plugin) TypeName() string { + return p.pluginType +} + +func (s *apiV2) loadPlugin(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + Write(415, FromError(err), w) + return + } + if strings.HasPrefix(mediaType, "multipart/") { + var pluginPath string + var signature []byte + var checkSum [sha256.Size]byte + mr := multipart.NewReader(r.Body, params["boundary"]) + var i int + for { + var b []byte + p, err := mr.NextPart() + if err == io.EOF { + break + } + if err != nil { + Write(500, FromError(err), w) + return + } + if r.Header.Get("Plugin-Compression") == "gzip" { + g, err := gzip.NewReader(p) + defer g.Close() + if err != nil { + Write(500, FromError(err), w) + return + } + b, err = ioutil.ReadAll(g) + if err != nil { + Write(500, FromError(err), w) + return + } + } else { + b, err = ioutil.ReadAll(p) + if err != nil { + Write(500, FromError(err), w) + return + } + } + + // A little sanity checking for files being passed into the API server. + // First file passed in should be the plugin. If the first file is a signature + // file, an error is returned. The signature file should be the second + // file passed to the API server. If the second file does not have the ".asc" + // extension, an error is returned. + // If we loop around more than twice before receiving io.EOF, then + // an error is returned. + + switch { + case i == 0: + if filepath.Ext(p.FileName()) == ".asc" { + e := errors.New("Error: first file passed to load plugin api can not be signature file") + Write(400, FromError(e), w) + return + } + if pluginPath, err = writeFile(p.FileName(), b); err != nil { + Write(500, FromError(err), w) + return + } + checkSum = sha256.Sum256(b) + case i == 1: + if filepath.Ext(p.FileName()) == ".asc" { + signature = b + } else { + e := errors.New("Error: second file passed was not a signature file") + Write(400, FromError(e), w) + return + } + case i == 2: + e := errors.New("Error: More than two files passed to the load plugin api") + Write(400, FromError(e), w) + return + } + i++ + } + rp, err := core.NewRequestedPlugin(pluginPath) + if err != nil { + Write(500, FromError(err), w) + return + } + rp.SetAutoLoaded(false) + // Sanity check, verify the checkSum on the file sent is the same + // as after it is written to disk. + if rp.CheckSum() != checkSum { + e := errors.New("Error: CheckSum mismatch on requested plugin to load") + Write(400, FromError(e), w) + return + } + rp.SetSignature(signature) + restLogger.Info("Loading plugin: ", rp.Path()) + pl, err := s.metricManager.Load(rp) + if err != nil { + var ec int + restLogger.Error(err) + restLogger.Debugf("Removing file (%s)", rp.Path()) + err2 := os.RemoveAll(filepath.Dir(rp.Path())) + if err2 != nil { + restLogger.Error(err2) + } + rb := FromError(err) + switch rb.ErrorMessage { + case ErrPluginAlreadyLoaded: + ec = 409 + default: + ec = 500 + } + Write(ec, rb, w) + return + } + Write(201, catalogedPluginBody(r.Host, pl), w) + } +} + +func writeFile(filename string, b []byte) (string, error) { + // Create temporary directory + dir, err := ioutil.TempDir("", "") + if err != nil { + return "", err + } + f, err := os.Create(path.Join(dir, filename)) + if err != nil { + return "", err + } + // Close before load + defer f.Close() + + n, err := f.Write(b) + log.Debugf("wrote %v to %v", n, f.Name()) + if err != nil { + return "", err + } + if runtime.GOOS != "windows" { + err = f.Chmod(0700) + if err != nil { + return "", err + } + } + return f.Name(), nil +} + +func pluginParameters(p httprouter.Params) (string, string, int, map[string]interface{}, serror.SnapError) { + plName := p.ByName("name") + plType := p.ByName("type") + plVersion, err := strconv.ParseInt(p.ByName("version"), 10, 0) + f := map[string]interface{}{ + "plugin-name": plName, + "plugin-version": plVersion, + "plugin-type": plType, + } + + if err != nil || plName == "" || plType == "" { + se := serror.New(errors.New("missing or invalid parameter(s)")) + se.SetFields(f) + return "", "", 0, nil, se + } + return plType, plName, int(plVersion), f, nil +} + +func (s *apiV2) unloadPlugin(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + plType, plName, plVersion, f, se := pluginParameters(p) + if se != nil { + Write(400, FromSnapError(se), w) + return + } + + _, se = s.metricManager.Unload(&plugin{ + name: plName, + version: plVersion, + pluginType: plType, + }) + + // 404 - plugin not found + // 409 - plugin state is not plugin loaded + // 500 - removing plugin from /tmp failed + if se != nil { + se.SetFields(f) + statusCode := 500 + switch se.Error() { + case control.ErrPluginNotFound.Error(): + statusCode = 404 + case control.ErrPluginNotInLoadedState.Error(): + statusCode = 409 + } + Write(statusCode, FromSnapError(se), w) + return + } + Write(204, nil, w) +} + +func (s *apiV2) getPlugins(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + + // filter by plugin name or plugin type + q := r.URL.Query() + plName := q.Get("name") + plType := q.Get("type") + nbFilter := Btoi(plName != "") + Btoi(plType != "") + + if _, detail := r.URL.Query()["running"]; detail { + // get running plugins + plugins := runningPluginsBody(r.Host, s.metricManager.AvailablePlugins()) + filteredPlugins := []RunningPlugin{} + if nbFilter > 0 { + for _, p := range plugins { + if nbFilter == 1 && (p.Name == plName || p.Type == plType) || nbFilter == 2 && (p.Name == plName && p.Type == plType) { + filteredPlugins = append(filteredPlugins, p) + } + } + } else { + filteredPlugins = plugins + } + Write(200, PluginsResponse{RunningPlugins: filteredPlugins}, w) + } else { + // get plugins from the plugin catalog + plugins := pluginCatalogBody(r.Host, s.metricManager.PluginCatalog()) + filteredPlugins := []Plugin{} + + if nbFilter > 0 { + for _, p := range plugins { + if nbFilter == 1 && (p.Name == plName || p.Type == plType) || nbFilter == 2 && (p.Name == plName && p.Type == plType) { + filteredPlugins = append(filteredPlugins, p) + } + } + } else { + filteredPlugins = plugins + } + Write(200, PluginsResponse{Plugins: filteredPlugins}, w) + } +} + +func Btoi(b bool) int { + if b { + return 1 + } + return 0 +} + +func pluginCatalogBody(host string, c []core.CatalogedPlugin) []Plugin { + plugins := make([]Plugin, len(c)) + for i, p := range c { + plugins[i] = catalogedPluginBody(host, p) + } + return plugins +} + +func catalogedPluginBody(host string, c core.CatalogedPlugin) Plugin { + return Plugin{ + Name: c.Name(), + Version: c.Version(), + Type: c.TypeName(), + Signed: c.IsSigned(), + Status: c.Status(), + LoadedTimestamp: c.LoadedTimestamp().Unix(), + Href: pluginURI(host, c), + } +} + +func runningPluginsBody(host string, c []core.AvailablePlugin) []RunningPlugin { + plugins := make([]RunningPlugin, len(c)) + for i, p := range c { + plugins[i] = RunningPlugin{ + Name: p.Name(), + Version: p.Version(), + Type: p.TypeName(), + HitCount: p.HitCount(), + LastHitTimestamp: p.LastHit().Unix(), + ID: p.ID(), + Href: pluginURI(host, p), + PprofPort: p.Port(), + } + } + return plugins +} + +func pluginURI(host string, c core.Plugin) string { + return fmt.Sprintf("%s://%s/%s/plugins/%s/%s/%d", protocolPrefix, host, version, c.TypeName(), c.Name(), c.Version()) +} + +func (s *apiV2) getPlugin(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + plType, plName, plVersion, f, se := pluginParameters(p) + if se != nil { + Write(400, FromSnapError(se), w) + return + } + + pluginCatalog := s.metricManager.PluginCatalog() + var plugin core.CatalogedPlugin + for _, item := range pluginCatalog { + if item.Name() == plName && + item.Version() == int(plVersion) && + item.TypeName() == plType { + plugin = item + break + } + } + if plugin == nil { + se := serror.New(ErrPluginNotFound, f) + Write(404, FromSnapError(se), w) + return + } + + rd := r.FormValue("download") + d, _ := strconv.ParseBool(rd) + var configPolicy []PolicyTable + if plugin.TypeName() == "processor" || plugin.TypeName() == "publisher" { + rules := plugin.Policy().Get([]string{""}).RulesAsTable() + configPolicy = make([]PolicyTable, 0, len(rules)) + for _, r := range rules { + configPolicy = append(configPolicy, PolicyTable{ + Name: r.Name, + Type: r.Type, + Default: r.Default, + Required: r.Required, + Minimum: r.Minimum, + Maximum: r.Maximum, + }) + } + + } + + if d { + b, err := ioutil.ReadFile(plugin.PluginPath()) + if err != nil { + f["plugin-path"] = plugin.PluginPath() + se := serror.New(err, f) + Write(500, FromSnapError(se), w) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Encoding", "gzip") + gz := gzip.NewWriter(w) + defer gz.Close() + _, err = gz.Write(b) + if err != nil { + f["plugin-path"] = plugin.PluginPath() + se := serror.New(err, f) + Write(500, FromSnapError(se), w) + return + } + w.WriteHeader(200) + return + } else { + pluginRet := Plugin{ + Name: plugin.Name(), + Version: plugin.Version(), + Type: plugin.TypeName(), + Signed: plugin.IsSigned(), + Status: plugin.Status(), + LoadedTimestamp: plugin.LoadedTimestamp().Unix(), + Href: pluginURI(r.Host, plugin), + ConfigPolicy: configPolicy, + } + Write(200, pluginRet, w) + } +} diff --git a/mgmt/rest/v2/task.go b/mgmt/rest/v2/task.go new file mode 100644 index 000000000..77e13c0b2 --- /dev/null +++ b/mgmt/rest/v2/task.go @@ -0,0 +1,222 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 v2 + +import ( + "fmt" + "net/http" + "sort" + "strings" + "time" + + "github.com/intelsdi-x/snap/core" + "github.com/intelsdi-x/snap/core/serror" + "github.com/intelsdi-x/snap/pkg/schedule" + "github.com/intelsdi-x/snap/scheduler/wmap" + "github.com/julienschmidt/httprouter" +) + +type TasksResponse struct { + Tasks Tasks `json:"tasks"` +} + +type Task struct { + ID string `json:"id"` + Name string `json:"name"` + Deadline string `json:"deadline"` + Workflow *wmap.WorkflowMap `json:"workflow,omitempty"` + Schedule *core.Schedule `json:"schedule,omitempty"` + CreationTimestamp int64 `json:"creation_timestamp,omitempty"` + LastRunTimestamp int64 `json:"last_run_timestamp,omitempty"` + HitCount int `json:"hit_count,omitempty"` + MissCount int `json:"miss_count,omitempty"` + FailedCount int `json:"failed_count,omitempty"` + LastFailureMessage string `json:"last_failure_message,omitempty"` + State string `json:"task_state"` + Href string `json:"href"` +} + +type Tasks []Task + +func (s Tasks) Len() int { + return len(s) +} + +func (s Tasks) Less(i, j int) bool { + return s[j].CreationTime().After(s[i].CreationTime()) +} + +func (s Tasks) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s *Task) CreationTime() time.Time { + return time.Unix(s.CreationTimestamp, 0) +} + +func (s *apiV2) addTask(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + task, err := core.CreateTaskFromContent(r.Body, nil, s.taskManager.CreateTask) + if err != nil { + Write(500, FromError(err), w) + return + } + taskB := AddSchedulerTaskFromTask(task) + taskB.Href = taskURI(r.Host, task) + Write(201, taskB, w) +} + +func (s *apiV2) getTasks(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + // get tasks from the task manager + sts := s.taskManager.GetTasks() + + // create the task list response + tasks := make(Tasks, len(sts)) + i := 0 + for _, t := range sts { + tasks[i] = SchedulerTaskFromTask(t) + tasks[i].Href = taskURI(r.Host, t) + i++ + } + sort.Sort(tasks) + + Write(200, TasksResponse{Tasks: tasks}, w) +} + +func (s *apiV2) getTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + id := p.ByName("id") + t, err := s.taskManager.GetTask(id) + if err != nil { + Write(404, FromError(err), w) + return + } + task := AddSchedulerTaskFromTask(t) + task.Href = taskURI(r.Host, t) + Write(200, task, w) +} + +func (s *apiV2) updateTaskState(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + errs := make([]serror.SnapError, 0, 1) + id := p.ByName("id") + action, exist := r.URL.Query()["action"] + if !exist && len(action) > 0 { + errs = append(errs, serror.New(ErrNoActionSpecified)) + } else { + switch action[0] { + case "enable": + _, err := s.taskManager.EnableTask(id) + if err != nil { + errs = append(errs, serror.New(err)) + } + case "start": + errs = s.taskManager.StartTask(id) + case "stop": + errs = s.taskManager.StopTask(id) + default: + errs = append(errs, serror.New(ErrWrongAction)) + } + } + + if len(errs) > 0 { + statusCode := 500 + switch errs[0].Error() { + case ErrNoActionSpecified.Error(): + statusCode = 400 + case ErrWrongAction.Error(): + statusCode = 400 + case ErrTaskNotFound: + statusCode = 404 + case ErrTaskDisabledNotRunnable: + statusCode = 409 + } + Write(statusCode, FromSnapErrors(errs), w) + return + } + Write(204, nil, w) +} + +func (s *apiV2) removeTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + id := p.ByName("id") + err := s.taskManager.RemoveTask(id) + if err != nil { + if strings.Contains(err.Error(), ErrTaskNotFound) { + Write(404, FromError(err), w) + return + } + Write(500, FromError(err), w) + return + } + Write(204, nil, w) +} + +func taskURI(host string, t core.Task) string { + return fmt.Sprintf("%s://%s/%s/tasks/%s", protocolPrefix, host, version, t.ID()) +} + +// functions to convert a core.Task to a Task +func AddSchedulerTaskFromTask(t core.Task) Task { + st := SchedulerTaskFromTask(t) + (&st).assertSchedule(t.Schedule()) + st.Workflow = t.WMap() + return st +} + +func SchedulerTaskFromTask(t core.Task) Task { + st := Task{ + ID: t.ID(), + Name: t.GetName(), + Deadline: t.DeadlineDuration().String(), + CreationTimestamp: t.CreationTime().Unix(), + LastRunTimestamp: t.LastRunTime().Unix(), + HitCount: int(t.HitCount()), + MissCount: int(t.MissedCount()), + FailedCount: int(t.FailedCount()), + LastFailureMessage: t.LastFailureMessage(), + State: t.State().String(), + } + if st.LastRunTimestamp < 0 { + st.LastRunTimestamp = -1 + } + return st +} + +func (t *Task) assertSchedule(s schedule.Schedule) { + switch v := s.(type) { + case *schedule.SimpleSchedule: + t.Schedule = &core.Schedule{ + Type: "simple", + Interval: v.Interval.String(), + } + return + case *schedule.WindowedSchedule: + t.Schedule = &core.Schedule{ + Type: "windowed", + Interval: v.Interval.String(), + StartTimestamp: v.StartTime, + StopTimestamp: v.StopTime, + } + return + case *schedule.CronSchedule: + t.Schedule = &core.Schedule{ + Type: "cron", + Interval: v.Entry(), + } + return + } +} diff --git a/mgmt/rest/v2/watch.go b/mgmt/rest/v2/watch.go new file mode 100644 index 000000000..671e0f6a2 --- /dev/null +++ b/mgmt/rest/v2/watch.go @@ -0,0 +1,206 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 v2 + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/intelsdi-x/snap/core" + "github.com/julienschmidt/httprouter" +) + +const ( + // Event types for task watcher streaming + TaskWatchStreamOpen = "stream-open" + TaskWatchMetricEvent = "metric-event" + TaskWatchTaskDisabled = "task-disabled" + TaskWatchTaskStarted = "task-started" + TaskWatchTaskStopped = "task-stopped" +) + +// The amount of time to buffer streaming events before flushing in seconds +var StreamingBufferWindow = 0.1 + +func (s *apiV2) watchTask(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + s.wg.Add(1) + defer s.wg.Done() + + id := p.ByName("id") + + tw := &TaskWatchHandler{ + alive: true, + mChan: make(chan StreamedTaskEvent), + } + tc, err1 := s.taskManager.WatchTask(id, tw) + if err1 != nil { + if strings.Contains(err1.Error(), ErrTaskNotFound) { + Write(404, FromError(err1), w) + return + } + Write(500, FromError(err1), w) + return + } + + // Make this Server Sent Events compatible + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + + // get a flusher type + flusher, ok := w.(http.Flusher) + if !ok { + // This only works on ResponseWriters that support streaming + Write(500, FromError(ErrStreamingUnsupported), w) + return + } + // send initial stream open event + so := StreamedTaskEvent{ + EventType: TaskWatchStreamOpen, + Message: "Stream opened", + } + fmt.Fprintf(w, "data: %s\n\n", so.ToJSON()) + flusher.Flush() + + // Get a channel for if the client notifies us it is closing the connection + n := w.(http.CloseNotifier).CloseNotify() + t := time.Now() + for { + // Write to the ResponseWriter + select { + case e := <-tw.mChan: + switch e.EventType { + case TaskWatchMetricEvent, TaskWatchTaskStarted: + // The client can decide to stop receiving on the stream on Task Stopped. + // We write the event to the buffer + fmt.Fprintf(w, "data: %s\n\n", e.ToJSON()) + case TaskWatchTaskDisabled, TaskWatchTaskStopped: + // A disabled task should end the streaming and close the connection + fmt.Fprintf(w, "data: %s\n\n", e.ToJSON()) + // Flush since we are sending nothing new + flusher.Flush() + // Close out watcher removing it from the scheduler + tc.Close() + // exit since this client is no longer listening + Write(204, nil, w) + } + // If we are at least above our minimum buffer time we flush to send + if time.Now().Sub(t).Seconds() > StreamingBufferWindow { + flusher.Flush() + t = time.Now() + } + case <-n: + // Flush since we are sending nothing new + flusher.Flush() + // Close out watcher removing it from the scheduler + tc.Close() + // exit since this client is no longer listening + Write(204, nil, w) + return + case <-s.killChan: + // Flush since we are sending nothing new + flusher.Flush() + // Close out watcher removing it from the scheduler + tc.Close() + // exit since this client is no longer listening + Write(204, nil, w) + return + } + } +} + +type TaskWatchHandler struct { + streamCount int + alive bool + mChan chan StreamedTaskEvent +} + +func (t *TaskWatchHandler) CatchCollection(m []core.Metric) { + sm := make([]StreamedMetric, len(m)) + for i := range m { + sm[i] = StreamedMetric{ + Namespace: m[i].Namespace().String(), + Data: m[i].Data(), + Timestamp: m[i].Timestamp(), + Tags: m[i].Tags(), + } + } + t.mChan <- StreamedTaskEvent{ + EventType: TaskWatchMetricEvent, + Message: "", + Event: sm, + } +} + +func (t *TaskWatchHandler) CatchTaskStarted() { + t.mChan <- StreamedTaskEvent{ + EventType: TaskWatchTaskStarted, + } +} + +func (t *TaskWatchHandler) CatchTaskStopped() { + t.mChan <- StreamedTaskEvent{ + EventType: TaskWatchTaskStopped, + } +} + +func (t *TaskWatchHandler) CatchTaskDisabled(why string) { + t.mChan <- StreamedTaskEvent{ + EventType: TaskWatchTaskDisabled, + Message: why, + } +} + +type StreamedTaskEvent struct { + // Used to describe the event + EventType string `json:"type"` + Message string `json:"message"` + Event StreamedMetrics `json:"event,omitempty"` +} + +func (s *StreamedTaskEvent) ToJSON() string { + j, _ := json.Marshal(s) + return string(j) +} + +type StreamedMetric struct { + Namespace string `json:"namespace"` + Data interface{} `json:"data"` + Timestamp time.Time `json:"timestamp"` + Tags map[string]string `json:"tags"` +} + +type StreamedMetrics []StreamedMetric + +func (s StreamedMetrics) Len() int { + return len(s) +} + +func (s StreamedMetrics) Less(i, j int) bool { + return fmt.Sprintf("%s", s[i].Namespace) < fmt.Sprintf("%s", s[j].Namespace) +} + +func (s StreamedMetrics) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index 74805c267..1288555cf 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -211,9 +211,9 @@ func autoDiscoverTasks(taskFiles []os.FileInfo, fullPath string, continue } //TODO: see if the following is really mandatory - //in which case mgmt/rest/rbody/task.go contents might also + //in which case mgmt/rest/response/task.go contents might also //move into pkg/task - //rbody.AddSchedulerTaskFromTask(task) + //response.AddSchedulerTaskFromTask(task) log.WithFields(log.Fields{ "_block": "autoDiscoverTasks", "_module": "scheduler",