Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(275)

Unified Diff: testservices/novaservice/service_http.go

Issue 6924043: Second phase of nova testing service: HTTP API. (Closed)
Patch Set: Second phase of nova testing service: HTTP API. Created 11 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: testservices/novaservice/service_http.go
=== modified file 'testservices/novaservice/service_http.go'
--- testservices/novaservice/service_http.go 2012-12-10 14:50:50 +0000
+++ testservices/novaservice/service_http.go 2012-12-12 17:30:27 +0000
@@ -3,9 +3,320 @@
package novaservice
import (
+ "encoding/json"
+ "io/ioutil"
+ "launchpad.net/goose/nova"
"net/http"
-)
-
-// ServeHTTP is the main entry point in the HTTP request processing.
-func (n *Nova) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ "strconv"
+ "strings"
+)
+
+const authToken = "X-Auth-Token"
+
+// response defines a single HTTP response.
+type response struct {
+ code int
+ body string
+ contentType string
+ errorText string
+}
+
+// verbatim real Nova responses
+var (
+ unauthorizedResponse = response{
+ http.StatusUnauthorized,
+ `401 Unauthorized
+
+This server could not verify that you are authorized to access the ` +
+ `document you requested. Either you supplied the wrong ` +
+ `credentials (e.g., bad password), or your browser does ` +
+ `not understand how to supply the credentials required.
+
+ Authentication required
+`,
+ "text/plain; charset=UTF-8",
+ "",
+ }
+ forbiddenResponse = response{
+ http.StatusForbidden,
+ `{"forbidden": {"message": "Policy doesn't allow compute_extension:` +
+ `flavormanage to be performed.", "code": 403}}`,
+ "application/json; charset=UTF-8",
+ "",
+ }
+ badRequestResponse = response{
+ http.StatusBadRequest,
+ `{"badRequest": {"message": "Malformed request url", "code": 400}}`,
+ "application/json; charset=UTF-8",
+ "",
+ }
+ badRequest2Response = response{
+ http.StatusBadRequest,
+ `{"badRequest": {"message": "The server could not comply with the ` +
+ `request since it is either malformed or otherwise incorrect.", "code": 400}}`,
+ "application/json; charset=UTF-8",
+ "",
+ }
+ notFoundResponse = response{
+ http.StatusNotFound,
+ `404 Not Found
+
+The resource could not be found.
+
+
+`,
+ "text/plain; charset=UTF-8",
+ "",
+ }
+ notFoundJSONResponse = response{
+ http.StatusNotFound,
+ `{"itemNotFound": {"message": "The resource could not be found.", "code": 404}}`,
+ "application/json; charset=UTF-8",
+ "",
+ }
+ multipleChoicesResponse = response{
+ http.StatusMultipleChoices,
+ `{"choices": [{"status": "CURRENT", "media-types": [{"base": ` +
+ `"application/xml", "type": "application/vnd.openstack.compute+` +
+ `xml;version=2"}, {"base": "application/json", "type": "application/` +
+ `vnd.openstack.compute+json;version=2"}], "id": "v2.0", "links": ` +
+ `[{"href": "$ENDPOINT$$URL$", "rel": "self"}]}]}`,
+ "application/json",
+ "",
+ }
+ noVersionResponse = response{
+ http.StatusOK,
+ `{"versions": [{"status": "CURRENT", "updated": "2011-01-21` +
+ `T11:33:21Z", "id": "v2.0", "links": [{"href": "$ENDPOINT$", "rel": "self"}]}]}`,
+ "application/json",
+ "",
+ }
+ versionsLinksResponse = response{
+ http.StatusOK,
+ `{"version": {"status": "CURRENT", "updated": "2011-01-21T11` +
+ `:33:21Z", "media-types": [{"base": "application/xml", "type": ` +
+ `"application/vnd.openstack.compute+xml;version=2"}, {"base": ` +
+ `"application/json", "type": "application/vnd.openstack.compute` +
+ `+json;version=2"}], "id": "v2.0", "links": [{"href": "$ENDPOINT$"` +
+ `, "rel": "self"}, {"href": "http://docs.openstack.org/api/openstack` +
+ `-compute/1.1/os-compute-devguide-1.1.pdf", "type": "application/pdf` +
+ `", "rel": "describedby"}, {"href": "http://docs.openstack.org/api/` +
+ `openstack-compute/1.1/wadl/os-compute-1.1.wadl", "type": ` +
+ `"application/vnd.sun.wadl+xml", "rel": "describedby"}]}}`,
+ "application/json",
+ "",
+ }
+ createdResponse = response{
+ http.StatusCreated,
+ "201 Created",
+ "text/plain; charset=UTF-8",
+ "",
+ }
+ noContentResponse = response{
+ http.StatusNoContent,
+ "",
+ "text/plain; charset=UTF-8",
+ "",
+ }
+ errorResponse = response{
+ http.StatusInternalServerError,
+ `{"internalServerError":{"message":"$ERROR$",code:500}}`,
+ "application/json",
+ "", // set by sendError()
+ }
+)
+
+// endpoint returns the current testing server's endpoint URL.
+func endpoint() string {
+ return hostname + versionPath + "/"
+}
+
+// replaceVars replaces $ENDPOINT$, $URL$, and $ERROR$ in the response body
+// with their values, taking the original requset into account, and
+// returns the result as a []byte.
+func (resp response) replaceVars(r *http.Request) []byte {
+ url := strings.TrimLeft(r.URL.Path, "/")
+ body := resp.body
+ body = strings.Replace(body, "$ENDPOINT$", endpoint(), -1)
+ body = strings.Replace(body, "$URL$", url, -1)
+ if resp.errorText != "" {
+ body = strings.Replace(body, "$ERROR$", resp.errorText, -1)
+ }
+ return []byte(body)
+}
+
+// send serializes the response as needed and sends it.
+func (resp response) send(w http.ResponseWriter, r *http.Request) {
+ if resp.contentType != "" {
+ w.Header().Set("Content-Type", resp.contentType)
+ }
+ var body []byte
+ if resp.body != "" {
+ body = resp.replaceVars(r)
+ }
+ // workaround for https://code.google.com/p/go/issues/detail?id=4454
+ w.Header().Set("Content-Length", strconv.Itoa(len(body)))
+ if resp.code != 0 {
+ w.WriteHeader(resp.code)
+ }
+ if len(body) > 0 {
+ w.Write(body)
+ }
+}
+
+// sendError responds with the given error to the given http request.
+func sendError(err error, w http.ResponseWriter, r *http.Request) {
+ eresp := errorResponse
+ eresp.errorText = err.Error()
+ eresp.send(w, r)
+}
+
+// sendJSON sends the specified response serialized as JSON.
+func sendJSON(code int, resp interface{}, w http.ResponseWriter, r *http.Request) {
+ var data []byte
+ if resp != nil {
+ var err error
+ data, err = json.Marshal(resp)
+ if err != nil {
+ sendError(err, w, r)
+ return
+ }
+ }
+ // workaround for https://code.google.com/p/go/issues/detail?id=4454
+ w.Header().Set("Content-Length", strconv.Itoa(len(data)))
+ w.WriteHeader(code)
+ w.Write(data)
+}
+
+// handleUnauthorizedNotFound is called for each request to check for
+// common errors (X-Auth-Token and trailing slash in URL). Returns
+// true if it's OK, false if a response was sent.
+func (n *Nova) handleUnauthorizedNotFound(w http.ResponseWriter, r *http.Request) bool {
+ path := r.URL.Path
+ if r.Header.Get(authToken) != n.token {
+ unauthorizedResponse.send(w, r)
+ return false
+ }
+ if strings.HasSuffix(path, "/") && path != "/" {
+ notFoundResponse.send(w, r)
+ return false
+ }
+ return true
+}
+
+// handle registers the given Nova handler method for the URL prefix.
+func (n *Nova) handle(prefix string, handler func(*Nova, http.ResponseWriter, *http.Request)) http.Handler {
+ h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if n.handleUnauthorizedNotFound(w, r) {
+ handler(n, w, r)
+ }
+ })
+ return http.StripPrefix(prefix, h)
+}
+
+// respond returns an http Handler sending the given response.
+func (n *Nova) respond(resp response) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if n.handleUnauthorizedNotFound(w, r) {
+ resp.send(w, r)
+ }
+ })
+}
+
+// handleFlavors handles the flavors HTTP API.
+func (n *Nova) handleFlavors(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case "GET":
+ entities := n.allFlavorsAsEntities()
+ if len(entities) == 0 {
+ sendJSON(http.StatusNoContent, nil, w, r)
+ return
+ }
+ var resp struct {
+ Flavors []nova.Entity `json:"flavors"`
+ }
+ resp.Flavors = entities
+ sendJSON(http.StatusOK, resp, w, r)
+ case "POST":
+ body, err := ioutil.ReadAll(r.Body)
+ r.Body.Close()
+ if err != nil {
+ sendError(err, w, r)
+ return
+ }
+ if len(body) == 0 {
+ badRequest2Response.send(w, r)
+ return
+ }
+ var flavor struct {
+ Flavor nova.FlavorDetail
+ }
+ err = json.Unmarshal(body, &flavor)
+ if err != nil {
+ sendError(err, w, r)
+ return
+ }
+ n.buildFlavorLinks(&flavor.Flavor)
+ err = n.addFlavor(flavor.Flavor)
+ if err != nil {
+ sendError(err, w, r)
+ return
+ }
+ createdResponse.send(w, r)
+ case "PUT", "DELETE":
+ notFoundResponse.send(w, r)
+ default:
+ panic("unknown request method: " + r.Method)
+ }
+}
+
+// handleFlavorsDetail handles the flavors/detail HTTP API.
+func (n *Nova) handleFlavorsDetail(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case "GET":
+ flavors := n.allFlavors()
+ if len(flavors) == 0 {
+ sendJSON(http.StatusNoContent, nil, w, r)
+ return
+ }
+ var resp struct {
+ Flavors []nova.FlavorDetail `json:"flavors"`
+ }
+ resp.Flavors = flavors
+ sendJSON(http.StatusOK, resp, w, r)
+ case "POST":
+ notFoundResponse.send(w, r)
+ case "PUT":
+ notFoundJSONResponse.send(w, r)
+ case "DELETE":
+ forbiddenResponse.send(w, r)
+ default:
+ panic("unknown request method: " + r.Method)
+ }
+}
+
+// setupHTTP attaches all the needed handlers to provide the HTTP API.
+func (n *Nova) setupHTTP(mux *http.ServeMux) {
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ if !n.handleUnauthorizedNotFound(w, r) {
+ return
+ }
+ if r.URL.Path == "/" {
+ noVersionResponse.send(w, r)
+ } else {
+ multipleChoicesResponse.send(w, r)
+ }
+ })
+ urlVersion := "/" + n.versionPath + "/"
+ urlTenant := urlVersion + n.tenantId + "/"
+ mux.Handle(urlVersion, n.respond(badRequestResponse))
+ mux.HandleFunc(urlTenant, func(w http.ResponseWriter, r *http.Request) {
+ if !n.handleUnauthorizedNotFound(w, r) {
+ return
+ }
+ // any unknown path
+ notFoundResponse.send(w, r)
+ })
+ mux.Handle(urlTenant+"flavors", n.handle(urlTenant, (*Nova).handleFlavors))
+ mux.Handle(urlTenant+"flavors/detail", n.handle(urlTenant, (*Nova).handleFlavorsDetail))
}

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b