From 35f637cd97333a10eead61a95eb7ac61946cdab7 Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Tue, 12 May 2020 15:00:48 +0200 Subject: [PATCH 1/3] Plugin restapi: New plugin to provide a REST API. --- restapi/README.md | 36 ++++++++++++++++++++++ restapi/restapi.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 restapi/README.md create mode 100644 restapi/restapi.go diff --git a/restapi/README.md b/restapi/README.md new file mode 100644 index 0000000..59bcaed --- /dev/null +++ b/restapi/README.md @@ -0,0 +1,36 @@ +# restapi + +Plugin **restapi** provides a REST API to communicate with the *collectd* +daemon. + +## Description + +The *restapi plugin* starts a webserver and waits for incoming REST API +requests. + +## Building + +To build this plugin, the collectd header files are required. + +On Debian and Ubuntu, the collectd headers are available from the +`collectd-dev` package. Once installed, add the import paths to the +`CGI_CPPFLAGS`: + +```bash +export CGO_CPPFLAGS="-I/usr/include/collectd/core/daemon \ +-I/usr/include/collectd/core -I/usr/include/collectd" +``` + +Alternatively, you can grab the collectd sources, run the `configure` script, +and reference the header files from there: + +```bash +TOP_SRCDIR="${HOME}/collectd" +export CGO_CPPFLAGS="-I${TOP_SRCDIR}/src -I${TOP_SRCDIR}/src/daemon" +``` + +Then build the plugin with the "c-shared" buildmode: + +```bash +go build -buildmode=c-shared -o restapi.so +``` diff --git a/restapi/restapi.go b/restapi/restapi.go new file mode 100644 index 0000000..55a956e --- /dev/null +++ b/restapi/restapi.go @@ -0,0 +1,76 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "time" + + "collectd.org/api" + "collectd.org/plugin" + "go.uber.org/multierr" +) + +const pluginName = "restapi" + +type restapi struct { + srv *http.Server +} + +func init() { + mux := http.NewServeMux() + mux.HandleFunc("/valueList", valueListHandler) + + api := restapi{ + srv: &http.Server{ + Addr: ":8080", + Handler: mux, + }, + } + + go func() { + if err := api.srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + plugin.Errorf("%s plugin: ListenAndServe(): %v", pluginName, err) + } + }() + + plugin.RegisterShutdown(pluginName, api) +} + +func valueListHandler(w http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + w.WriteHeader(http.StatusNotImplemented) + fmt.Fprintln(w, "Only POST is currently supported.") + return + } + + var vls []api.ValueList + if err := json.NewDecoder(req.Body).Decode(&vls); err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintln(w, "parsing JSON failed:", err) + return + } + + var errs error + for _, vl := range vls { + errs = multierr.Append(errs, + plugin.Write(req.Context(), &vl)) + } + + if errs != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, "plugin.Write():", errs) + return + } +} + +func (api restapi) Shutdown(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + return api.srv.Shutdown(ctx) +} + +func main() {} // ignored From 59ad0be33ef9ca22b560e7289d91ef697102e01a Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Mon, 18 May 2020 13:33:29 +0200 Subject: [PATCH 2/3] Plugin restapi: Implement a config callback. --- restapi/README.md | 17 +++++++++++++ restapi/restapi.go | 59 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/restapi/README.md b/restapi/README.md index 59bcaed..f413f25 100644 --- a/restapi/README.md +++ b/restapi/README.md @@ -34,3 +34,20 @@ Then build the plugin with the "c-shared" buildmode: ```bash go build -buildmode=c-shared -o restapi.so ``` + +## Configuration + +### Synopsis + +``` +LoadPlugin restapi + + Port "8080" + +``` + +### Options + +* **Port** *Port* + + Post to listen to. Defaults to `8080`. diff --git a/restapi/restapi.go b/restapi/restapi.go index 55a956e..ed76336 100644 --- a/restapi/restapi.go +++ b/restapi/restapi.go @@ -9,6 +9,7 @@ import ( "time" "collectd.org/api" + "collectd.org/config" "collectd.org/plugin" "go.uber.org/multierr" ) @@ -20,23 +21,10 @@ type restapi struct { } func init() { - mux := http.NewServeMux() - mux.HandleFunc("/valueList", valueListHandler) - - api := restapi{ - srv: &http.Server{ - Addr: ":8080", - Handler: mux, - }, - } - - go func() { - if err := api.srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { - plugin.Errorf("%s plugin: ListenAndServe(): %v", pluginName, err) - } - }() + ra := &restapi{} - plugin.RegisterShutdown(pluginName, api) + plugin.RegisterConfig(pluginName, ra) + plugin.RegisterShutdown(pluginName, ra) } func valueListHandler(w http.ResponseWriter, req *http.Request) { @@ -66,11 +54,46 @@ func valueListHandler(w http.ResponseWriter, req *http.Request) { } } -func (api restapi) Shutdown(ctx context.Context) error { +func (ra *restapi) Configure(_ context.Context, rawConfig config.Block) error { + fmt.Printf("%s plugin: rawConfig = %v\n", pluginName, rawConfig) + + cfg := struct { + Args string // unused + Port string + }{ + Port: "8080", + } + + if err := rawConfig.Unmarshal(&cfg); err != nil { + return err + } + + mux := http.NewServeMux() + mux.HandleFunc("/valueList", valueListHandler) + + ra.srv = &http.Server{ + Addr: ":" + cfg.Port, + Handler: mux, + } + + go func() { + if err := ra.srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + plugin.Errorf("%s plugin: ListenAndServe(): %v", pluginName, err) + } + }() + + return nil +} + +func (ra *restapi) Shutdown(ctx context.Context) error { + if ra == nil || ra.srv == nil { + return nil + } + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - return api.srv.Shutdown(ctx) + return ra.srv.Shutdown(ctx) } func main() {} // ignored From 885ded5a9753d36c0cd0224199b34b7ed33adc0d Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Tue, 19 May 2020 23:14:32 +0200 Subject: [PATCH 3/3] Plugin restapi: Expand configuration to allow TLS setups. --- restapi/README.md | 16 ++++++++++++++-- restapi/restapi.go | 39 +++++++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/restapi/README.md b/restapi/README.md index f413f25..2928084 100644 --- a/restapi/README.md +++ b/restapi/README.md @@ -42,12 +42,24 @@ go build -buildmode=c-shared -o restapi.so ``` LoadPlugin restapi - Port "8080" + Addr "::" + Port "8443" + CertFile "/path/to/cert_file.pem" + KeyFile "/path/to/key_file.pem" ``` ### Options +* **Addr** *Network address* + + Addredd to listen to. Defaults to `""` (any address). * **Port** *Port* - Post to listen to. Defaults to `8080`. + Post to listen to. Defaults to `8080` (`8443` if **CertFile** is specified). +* **CertFile** *Path*
+ **KeyFile** *Path* + + TLS certificate and key files. Refer to + [`"net/http".ListenAndServeTLS`](https://golang.org/pkg/net/http/#ListenAndServeTLS) + for more information on the TLS setup. diff --git a/restapi/restapi.go b/restapi/restapi.go index ed76336..63762e2 100644 --- a/restapi/restapi.go +++ b/restapi/restapi.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net" "net/http" "time" @@ -54,30 +55,48 @@ func valueListHandler(w http.ResponseWriter, req *http.Request) { } } +type srvConfig struct { + Args string // unused + Addr string + Port string + CertFile string + KeyFile string +} + func (ra *restapi) Configure(_ context.Context, rawConfig config.Block) error { - fmt.Printf("%s plugin: rawConfig = %v\n", pluginName, rawConfig) + cfg := srvConfig{} + if err := rawConfig.Unmarshal(&cfg); err != nil { + return err + } - cfg := struct { - Args string // unused - Port string - }{ - Port: "8080", + if (cfg.CertFile == "") != (cfg.KeyFile == "") { + return fmt.Errorf("CertFile=%q, KeyFile=%q; need both for TLS setup", + cfg.CertFile, cfg.KeyFile) } - if err := rawConfig.Unmarshal(&cfg); err != nil { - return err + if cfg.Port == "" { + cfg.Port = "8080" + if cfg.CertFile != "" { + cfg.Port = "8443" + } } mux := http.NewServeMux() mux.HandleFunc("/valueList", valueListHandler) ra.srv = &http.Server{ - Addr: ":" + cfg.Port, + Addr: net.JoinHostPort(cfg.Addr, cfg.Port), Handler: mux, } go func() { - if err := ra.srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + var err error + if cfg.CertFile != "" { + err = ra.srv.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile) + } else { + err = ra.srv.ListenAndServe() + } + if !errors.Is(err, http.ErrServerClosed) { plugin.Errorf("%s plugin: ListenAndServe(): %v", pluginName, err) } }()