diff --git a/.travis-coverage b/.travis-coverage index efaf7c1b9e..22e6bf2e8b 100755 --- a/.travis-coverage +++ b/.travis-coverage @@ -4,7 +4,7 @@ # the tool requires that it be used only on one package when # capturing the coverage # This is why we need this little script here. -packages="./handlers ./plugins" +packages="./rest" COVERFILE=packagecover.out coverage() diff --git a/GLOCKFILE b/GLOCKFILE new file mode 100644 index 0000000000..989b8d3615 --- /dev/null +++ b/GLOCKFILE @@ -0,0 +1,5 @@ +github.com/codegangsta/negroni c7477ad8e330bef55bf1ebe300cf8aa67c492d1b +github.com/gorilla/context 215affda49addc4c8ef7e2534915df2c8c35c6cd +github.com/gorilla/mux 47e8f450ef38c857cdd922ec08862ca9d65a1c6d +github.com/lpabon/godbc 9577782540c1398b710ddae1b86268ba03a19b0c +golang.org/x/crypto 1e856cbfdf9bc25eefca75f83f25d55e35ae72e0 diff --git a/clients/python/heketi.py b/_prototype/clients/python/heketi.py similarity index 100% rename from clients/python/heketi.py rename to _prototype/clients/python/heketi.py diff --git a/handlers/nodes.go b/_prototype/handlers/nodes.go similarity index 100% rename from handlers/nodes.go rename to _prototype/handlers/nodes.go diff --git a/handlers/nodes_test.go b/_prototype/handlers/nodes_test.go similarity index 100% rename from handlers/nodes_test.go rename to _prototype/handlers/nodes_test.go diff --git a/handlers/volumes.go b/_prototype/handlers/volumes.go similarity index 100% rename from handlers/volumes.go rename to _prototype/handlers/volumes.go diff --git a/plugins/glusterfs/bricks.go b/_prototype/plugins/glusterfs/bricks.go similarity index 100% rename from plugins/glusterfs/bricks.go rename to _prototype/plugins/glusterfs/bricks.go diff --git a/plugins/glusterfs/glusterfs.go b/_prototype/plugins/glusterfs/glusterfs.go similarity index 100% rename from plugins/glusterfs/glusterfs.go rename to _prototype/plugins/glusterfs/glusterfs.go diff --git a/plugins/glusterfs/glusterfsdb.go b/_prototype/plugins/glusterfs/glusterfsdb.go similarity index 100% rename from plugins/glusterfs/glusterfsdb.go rename to _prototype/plugins/glusterfs/glusterfsdb.go diff --git a/plugins/glusterfs/glusterfsdb_test.go b/_prototype/plugins/glusterfs/glusterfsdb_test.go similarity index 100% rename from plugins/glusterfs/glusterfsdb_test.go rename to _prototype/plugins/glusterfs/glusterfsdb_test.go diff --git a/plugins/glusterfs/handler_node.go b/_prototype/plugins/glusterfs/handler_node.go similarity index 100% rename from plugins/glusterfs/handler_node.go rename to _prototype/plugins/glusterfs/handler_node.go diff --git a/plugins/glusterfs/handler_volume.go b/_prototype/plugins/glusterfs/handler_volume.go similarity index 100% rename from plugins/glusterfs/handler_volume.go rename to _prototype/plugins/glusterfs/handler_volume.go diff --git a/plugins/glusterfs/node.go b/_prototype/plugins/glusterfs/node.go similarity index 100% rename from plugins/glusterfs/node.go rename to _prototype/plugins/glusterfs/node.go diff --git a/plugins/glusterfs/ring.go b/_prototype/plugins/glusterfs/ring.go similarity index 100% rename from plugins/glusterfs/ring.go rename to _prototype/plugins/glusterfs/ring.go diff --git a/plugins/glusterfs/scripts/ring.py b/_prototype/plugins/glusterfs/scripts/ring.py similarity index 100% rename from plugins/glusterfs/scripts/ring.py rename to _prototype/plugins/glusterfs/scripts/ring.py diff --git a/plugins/glusterfs/volume.go b/_prototype/plugins/glusterfs/volume.go similarity index 100% rename from plugins/glusterfs/volume.go rename to _prototype/plugins/glusterfs/volume.go diff --git a/plugins/mock/mock.go b/_prototype/plugins/mock/mock.go similarity index 100% rename from plugins/mock/mock.go rename to _prototype/plugins/mock/mock.go diff --git a/plugins/mock/node.go b/_prototype/plugins/mock/node.go similarity index 100% rename from plugins/mock/node.go rename to _prototype/plugins/mock/node.go diff --git a/plugins/mock/volume.go b/_prototype/plugins/mock/volume.go similarity index 100% rename from plugins/mock/volume.go rename to _prototype/plugins/mock/volume.go diff --git a/plugins/plugins.go b/_prototype/plugins/plugins.go similarity index 100% rename from plugins/plugins.go rename to _prototype/plugins/plugins.go diff --git a/apps/doc.go b/apps/doc.go new file mode 100644 index 0000000000..830e2458a4 --- /dev/null +++ b/apps/doc.go @@ -0,0 +1,2 @@ +// Location for applications for Heketi +package apps diff --git a/apps/glusterfs/app.go b/apps/glusterfs/app.go new file mode 100644 index 0000000000..e01083deaf --- /dev/null +++ b/apps/glusterfs/app.go @@ -0,0 +1,78 @@ +// +// Copyright (c) 2015 The heketi Authors +// +// 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 glusterfs + +import ( + "fmt" + "github.com/heketi/heketi/rest" + "net/http" +) + +type App struct { + hello string +} + +func NewApp() *App { + return &App{} +} + +// Interface rest.App +func (a *App) GetRoutes() rest.Routes { + + return rest.Routes{ + + // HelloWorld + rest.Route{"Hello", "GET", "/hello", a.Hello}, + + // Cluster + rest.Route{"ClusterCreate", "POST", "/clusters", a.NotImplemented}, + rest.Route{"ClusterInfo", "GET", "/clusters/{id:[A-Fa-f0-9]+}", a.NotImplemented}, + rest.Route{"ClusterList", "GET", "/clusters", a.NotImplemented}, + rest.Route{"ClusterDelete", "DELETE", "/clusters/{id:[A-Fa-f0-9]+}", a.NotImplemented}, + + // Node + rest.Route{"NodeAdd", "POST", "/nodes", a.NotImplemented}, + rest.Route{"NodeInfo", "GET", "/nodes/{id:[A-Fa-f0-9]+}", a.NotImplemented}, + rest.Route{"NodeDelete", "DELETE", "/nodes/{id:[A-Fa-f0-9]+}", a.NotImplemented}, + + // Devices + rest.Route{"DeviceAdd", "POST", "/devices", a.NotImplemented}, + rest.Route{"DeviceInfo", "GET", "/devices/{id:[A-Fa-f0-9]+}", a.NotImplemented}, + rest.Route{"DeviceDelete", "DELETE", "/devices/{id:[A-Fa-f0-9]+}", a.NotImplemented}, + + // Volume + rest.Route{"VolumeCreate", "POST", "/volumes", a.NotImplemented}, + rest.Route{"VolumeInfo", "GET", "/volumes/{id:[A-Fa-f0-9]+}", a.NotImplemented}, + rest.Route{"VolumeExpand", "POST", "/volumes/{id:[A-Fa-f0-9]+}/expand", a.NotImplemented}, + rest.Route{"VolumeDelete", "DELETE", "/volumes/{id:[A-Fa-f0-9]+}", a.NotImplemented}, + rest.Route{"VolumeList", "GET", "/volumes", a.NotImplemented}, + } +} + +func (a *App) Close() { + +} + +func (a *App) Hello(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=UTF-8") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "HelloWorld from GlusterFS Application") +} + +func (a *App) NotImplemented(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Function not yet supported", http.StatusNotImplemented) +} diff --git a/handlers/asynchttp.go b/handlers/asynchttp.go deleted file mode 100644 index c9fbbedc6e..0000000000 --- a/handlers/asynchttp.go +++ /dev/null @@ -1,158 +0,0 @@ -// -// Copyright (c) 2014 The heketi Authors -// -// 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 handlers - -import ( - "github.com/gorilla/mux" - "github.com/heketi/heketi/utils" - "github.com/lpabon/godbc" - "net/http" - "sync" -) - -type AsyncHttpHandler struct { - err error - completed bool - manager *AsyncHttpManager - location, id string -} - -type AsyncHttpManager struct { - lock sync.RWMutex - route string - handlers map[string]*AsyncHttpHandler -} - -func NewAsyncHttpManager(route string) *AsyncHttpManager { - return &AsyncHttpManager{ - route: route, - handlers: make(map[string]*AsyncHttpHandler), - } -} - -func (a *AsyncHttpManager) NewHandler() *AsyncHttpHandler { - handler := &AsyncHttpHandler{ - manager: a, - id: utils.GenUUID(), - } - - a.lock.Lock() - defer a.lock.Unlock() - - a.handlers[handler.id] = handler - - return handler -} - -func (a *AsyncHttpManager) AsyncHttpRedirectFunc(w http.ResponseWriter, - r *http.Request, - handlerfunc func() (string, error)) { - - handler := a.NewHandler() - go func() { - url, err := handlerfunc() - if err != nil { - handler.CompletedWithError(err) - } else if url != "" { - handler.CompletedWithLocation(url) - } else { - handler.Completed() - } - }() - http.Redirect(w, r, handler.Url(), http.StatusAccepted) -} - -func (a *AsyncHttpManager) HandlerStatus(w http.ResponseWriter, r *http.Request) { - // Get the id from the URL - vars := mux.Vars(r) - id := vars["id"] - - a.lock.Lock() - defer a.lock.Unlock() - - if handler, ok := a.handlers[id]; ok { - if handler.completed { - if handler.err != nil { - http.Error(w, handler.err.Error(), http.StatusInternalServerError) - } else { - if handler.location != "" { - http.Redirect(w, r, handler.location, http.StatusSeeOther) - } else { - w.WriteHeader(http.StatusNoContent) - } - } - - // It has been completed, we can now remove it from the map - delete(a.handlers, id) - } else { - // Still pending - // Could add a JSON body here later - w.WriteHeader(http.StatusOK) - } - - } else { - http.Error(w, "Id not found", http.StatusNotFound) - } -} - -func (h *AsyncHttpHandler) Url() string { - h.manager.lock.RLock() - defer h.manager.lock.RUnlock() - - return h.manager.route + "/" + h.id -} - -func (h *AsyncHttpHandler) CompletedWithError(err error) { - - h.manager.lock.RLock() - defer h.manager.lock.RUnlock() - - godbc.Require(h.completed == false) - - h.err = err - h.completed = true - - godbc.Ensure(h.completed == true) -} - -func (h *AsyncHttpHandler) CompletedWithLocation(location string) { - - h.manager.lock.RLock() - defer h.manager.lock.RUnlock() - - godbc.Require(h.completed == false) - - h.location = location - h.completed = true - - godbc.Ensure(h.completed == true) - godbc.Ensure(h.location == location) - godbc.Ensure(h.err == nil) -} - -func (h *AsyncHttpHandler) Completed() { - - h.manager.lock.RLock() - defer h.manager.lock.RUnlock() - - godbc.Require(h.completed == false) - - h.completed = true - - godbc.Ensure(h.completed == true) - godbc.Ensure(h.location == "") - godbc.Ensure(h.err == nil) -} diff --git a/main.go b/main.go index f5af2855a8..8f937c34a0 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2014 The heketi Authors +// Copyright (c) 2015 The heketi Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,8 +20,8 @@ import ( "fmt" "github.com/codegangsta/negroni" "github.com/gorilla/mux" - "github.com/heketi/heketi/handlers" - "github.com/heketi/heketi/plugins" + "github.com/heketi/heketi/apps/glusterfs" + "github.com/heketi/heketi/rest" "log" "net/http" "os" @@ -30,20 +30,17 @@ import ( func main() { - // Get a mock node server - plugin := plugins.NewPlugin("glusterfs") + var app rest.Application - // - nodeserver := handlers.NewNodeServer(plugin) - volumeserver := handlers.NewVolumeServer(plugin) - - r := volumeserver.VolumeRoutes() - r = append(r, nodeserver.NodeRoutes()...) + // Setup a new GlusterFS application + app = glusterfs.NewApp() // Create a router and do not allow any routes // unless defined. router := mux.NewRouter().StrictSlash(true) - for _, route := range r { + + // Register all routes from the App + for _, route := range app.GetRoutes() { // Add routes from the table router. @@ -68,7 +65,7 @@ func main() { select { case <-signalch: fmt.Printf("Shutting down...") - plugin.Close() + app.Close() os.Exit(0) } }() diff --git a/rest/app.go b/rest/app.go new file mode 100644 index 0000000000..d1ab006d96 --- /dev/null +++ b/rest/app.go @@ -0,0 +1,22 @@ +// +// Copyright (c) 2015 The heketi Authors +// +// 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 + +type Application interface { + GetRoutes() Routes + Close() +} diff --git a/rest/asynchttp.go b/rest/asynchttp.go new file mode 100644 index 0000000000..d94dbc5047 --- /dev/null +++ b/rest/asynchttp.go @@ -0,0 +1,258 @@ +// +// Copyright (c) 2015 The heketi Authors +// +// 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 ( + "github.com/gorilla/mux" + "github.com/heketi/heketi/utils" + "github.com/lpabon/godbc" + "net/http" + "sync" +) + +// Contains information about the asynchronous operation +type AsyncHttpHandler struct { + err error + completed bool + manager *AsyncHttpManager + location, id string +} + +// Manager of asynchronous operations +type AsyncHttpManager struct { + lock sync.RWMutex + route string + handlers map[string]*AsyncHttpHandler +} + +// Creates a new manager +func NewAsyncHttpManager(route string) *AsyncHttpManager { + return &AsyncHttpManager{ + route: route, + handlers: make(map[string]*AsyncHttpHandler), + } +} + +// Use to create a new asynchronous operation handler. +// Only use this function if you need to do every step by hand. +// It is recommended to use AsyncHttpRedirectFunc() instead +func (a *AsyncHttpManager) NewHandler() *AsyncHttpHandler { + handler := &AsyncHttpHandler{ + manager: a, + id: utils.GenUUID(), + } + + a.lock.Lock() + defer a.lock.Unlock() + + a.handlers[handler.id] = handler + + return handler +} + +// Create an asynchronous operation handler and return the appropiate +// information the caller. +// This function will call handlerfunc() in a new go routine, then +// return to the caller a HTTP status 202 setting up the `Location` header +// to point to the new asynchronous handler. +// +// If handlerfunc() returns failure, the asynchronous handler will return +// an http status of 500 and save the error string in the body. +// If handlerfunc() is successful and returns a location url path in "string", +// the asynchronous handler will return 303 (See Other) with the Location +// header set to the value returned in the string. +// If handlerfunc() is successful and returns an empty string, then the +// asynchronous handler will return 204 to the caller. +// +// Example: +// package rest +// import ( +// "github.com/gorilla/mux" +// "github.com/heketi/heketi/rest" +// "net/http" +// "net/http/httptest" +// "time" +// ) +// +// // Setup asynchronous manager +// route := "/x" +// manager := rest.NewAsyncHttpManager(route) +// +// // Setup the route +// router := mux.NewRouter() +// router.HandleFunc(route+"/{id}", manager.HandlerStatus).Methods("GET") +// router.HandleFunc("/result", func(w http.ResponseWriter, r *http.Request) { +// w.Header().Set("Content-Type", "text/plain; charset=UTF-8") +// w.WriteHeader(http.StatusOK) +// fmt.Fprint(w, "HelloWorld") +// }).Methods("GET") +// +// router.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) { +// manager.AsyncHttpRedirectFunc(w, r, func() (string, error) { +// time.Sleep(100 * time.Millisecond) +// return "/result", nil +// }) +// }).Methods("GET") +// +// // Setup the server +// ts := httptest.NewServer(router) +// defer ts.Close() +// +func (a *AsyncHttpManager) AsyncHttpRedirectFunc(w http.ResponseWriter, + r *http.Request, + handlerfunc func() (string, error)) { + + handler := a.NewHandler() + go func() { + url, err := handlerfunc() + if err != nil { + handler.CompletedWithError(err) + } else if url != "" { + handler.CompletedWithLocation(url) + } else { + handler.Completed() + } + }() + http.Redirect(w, r, handler.Url(), http.StatusAccepted) +} + +// Handler for asynchronous operation status +// Register this handler with a router like Gorilla Mux +// +// Returns the following HTTP status codes +// 200 Operation is still pending +// 404 Id requested does not exist +// 500 Operation finished and has failed. Body will be filled in with the +// error in plain text. +// 303 Operation finished and has setup a new location to retreive data. +// 204 Operation finished and has no data to return +// +// Example: +// package rest +// import ( +// "github.com/gorilla/mux" +// "github.com/heketi/heketi/rest" +// "net/http" +// "net/http/httptest" +// "time" +// ) +// +// // Setup asynchronous manager +// route := "/x" +// manager := rest.NewAsyncHttpManager(route) +// +// // Setup the route +// router := mux.NewRouter() +// router.HandleFunc(route+"/{id}", manager.HandlerStatus).Methods("GET") +// +// // Setup the server +// ts := httptest.NewServer(router) +// defer ts.Close() +// +func (a *AsyncHttpManager) HandlerStatus(w http.ResponseWriter, r *http.Request) { + // Get the id from the URL + vars := mux.Vars(r) + id := vars["id"] + + a.lock.Lock() + defer a.lock.Unlock() + + // Check the id is in the map + if handler, ok := a.handlers[id]; ok { + + if handler.completed { + if handler.err != nil { + + // Return 500 status + http.Error(w, handler.err.Error(), http.StatusInternalServerError) + } else { + if handler.location != "" { + + // Redirect to new location + http.Redirect(w, r, handler.location, http.StatusSeeOther) + } else { + + // Return 204 status + w.WriteHeader(http.StatusNoContent) + } + } + + // It has been completed, we can now remove it from the map + delete(a.handlers, id) + } else { + // Still pending + // Could add a JSON body here later + w.WriteHeader(http.StatusOK) + } + + } else { + http.Error(w, "Id not found", http.StatusNotFound) + } +} + +// Returns the url for the specified asynchronous handler +func (h *AsyncHttpHandler) Url() string { + h.manager.lock.RLock() + defer h.manager.lock.RUnlock() + + return h.manager.route + "/" + h.id +} + +// Registers that the handler has completed with an error +func (h *AsyncHttpHandler) CompletedWithError(err error) { + + h.manager.lock.RLock() + defer h.manager.lock.RUnlock() + + godbc.Require(h.completed == false) + + h.err = err + h.completed = true + + godbc.Ensure(h.completed == true) +} + +// Registers that the handler has completed and has provided a location +// where information can be retreived +func (h *AsyncHttpHandler) CompletedWithLocation(location string) { + + h.manager.lock.RLock() + defer h.manager.lock.RUnlock() + + godbc.Require(h.completed == false) + + h.location = location + h.completed = true + + godbc.Ensure(h.completed == true) + godbc.Ensure(h.location == location) + godbc.Ensure(h.err == nil) +} + +// Registers that the handler has completed and no data needs to be returned +func (h *AsyncHttpHandler) Completed() { + + h.manager.lock.RLock() + defer h.manager.lock.RUnlock() + + godbc.Require(h.completed == false) + + h.completed = true + + godbc.Ensure(h.completed == true) + godbc.Ensure(h.location == "") + godbc.Ensure(h.err == nil) +} diff --git a/handlers/asynchttp_test.go b/rest/asynchttp_test.go similarity index 99% rename from handlers/asynchttp_test.go rename to rest/asynchttp_test.go index 737677eeeb..5fc44af45f 100644 --- a/handlers/asynchttp_test.go +++ b/rest/asynchttp_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2014 The heketi Authors +// Copyright (c) 2015 The heketi Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ // limitations under the License. // -package handlers +package rest import ( "errors" diff --git a/rest/doc.go b/rest/doc.go new file mode 100644 index 0000000000..639a2bf562 --- /dev/null +++ b/rest/doc.go @@ -0,0 +1,2 @@ +// Generic RESTful application functions +package rest diff --git a/handlers/routes.go b/rest/routes.go similarity index 93% rename from handlers/routes.go rename to rest/routes.go index 7b60a3797b..027025134a 100644 --- a/handlers/routes.go +++ b/rest/routes.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2014 The heketi Authors +// Copyright (c) 2015 The heketi Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ // limitations under the License. // -package handlers +package rest import ( "net/http"