gopkg.in/goose.v2@v2.0.1/testservices/novaservice/service_http.go (about)

     1  // Nova double testing service - HTTP API implementation
     2  
     3  package novaservice
     4  
     5  import (
     6  	"crypto/rand"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"path"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"gopkg.in/goose.v2/neutron"
    19  	"gopkg.in/goose.v2/nova"
    20  	"gopkg.in/goose.v2/testservices"
    21  	"gopkg.in/goose.v2/testservices/identityservice"
    22  )
    23  
    24  const authToken = "X-Auth-Token"
    25  
    26  // errorResponse defines a single HTTP error response.
    27  type errorResponse struct {
    28  	code        int
    29  	body        string
    30  	contentType string
    31  	errorText   string
    32  	headers     map[string]string
    33  	nova        *Nova
    34  }
    35  
    36  var (
    37  	regexpVolumeAttachmentDevice = regexp.MustCompile("(^/dev/x{0,1}[a-z]{0,1}d{0,1})([a-z]+)[0-9]*$")
    38  )
    39  
    40  // verbatim real Nova responses (as errors).
    41  var (
    42  	errUnauthorized = &errorResponse{
    43  		http.StatusUnauthorized,
    44  		`401 Unauthorized
    45  
    46  This server could not verify that you are authorized to access the ` +
    47  			`document you requested. Either you supplied the wrong ` +
    48  			`credentials (e.g., bad password), or your browser does ` +
    49  			`not understand how to supply the credentials required.
    50  
    51   Authentication required
    52  `,
    53  		"text/plain; charset=UTF-8",
    54  		"unauthorized request",
    55  		nil,
    56  		nil,
    57  	}
    58  	errForbidden = &errorResponse{
    59  		http.StatusForbidden,
    60  		`{"forbidden": {"message": "Policy doesn't allow compute_extension:` +
    61  			`flavormanage to be performed.", "code": 403}}`,
    62  		"application/json; charset=UTF-8",
    63  		"forbidden flavors request",
    64  		nil,
    65  		nil,
    66  	}
    67  	errBadRequest = &errorResponse{
    68  		http.StatusBadRequest,
    69  		`{"badRequest": {"message": "Malformed request url", "code": 400}}`,
    70  		"application/json; charset=UTF-8",
    71  		"bad request base path or URL",
    72  		nil,
    73  		nil,
    74  	}
    75  	errBadRequest2 = &errorResponse{
    76  		http.StatusBadRequest,
    77  		`{"badRequest": {"message": "The server could not comply with the ` +
    78  			`request since it is either malformed or otherwise incorrect.", "code": 400}}`,
    79  		"application/json; charset=UTF-8",
    80  		"bad request URL",
    81  		nil,
    82  		nil,
    83  	}
    84  	errBadRequest3 = &errorResponse{
    85  		http.StatusBadRequest,
    86  		`{"badRequest": {"message": "Malformed request body", "code": 400}}`,
    87  		"application/json; charset=UTF-8",
    88  		"bad request body",
    89  		nil,
    90  		nil,
    91  	}
    92  	errBadRequestDuplicateValue = &errorResponse{
    93  		http.StatusBadRequest,
    94  		`{"badRequest": {"message": "entity already exists", "code": 400}}`,
    95  		"application/json; charset=UTF-8",
    96  		"duplicate value",
    97  		nil,
    98  		nil,
    99  	}
   100  	errBadRequestSrvName = &errorResponse{
   101  		http.StatusBadRequest,
   102  		`{"badRequest": {"message": "Server name is not defined", "code": 400}}`,
   103  		"application/json; charset=UTF-8",
   104  		"bad request - missing server name",
   105  		nil,
   106  		nil,
   107  	}
   108  	errBadRequestSrvFlavor = &errorResponse{
   109  		http.StatusBadRequest,
   110  		`{"badRequest": {"message": "Missing flavorRef attribute", "code": 400}}`,
   111  		"application/json; charset=UTF-8",
   112  		"bad request - missing flavorRef",
   113  		nil,
   114  		nil,
   115  	}
   116  	errBadRequestSrvImage = &errorResponse{
   117  		http.StatusBadRequest,
   118  		`{"badRequest": {"message": "Missing imageRef attribute", "code": 400}}`,
   119  		"application/json; charset=UTF-8",
   120  		"bad request - missing imageRef",
   121  		nil,
   122  		nil,
   123  	}
   124  	errNotFound = &errorResponse{
   125  		http.StatusNotFound,
   126  		`404 Not Found
   127  
   128  The resource could not be found.
   129  
   130  
   131  `,
   132  		"text/plain; charset=UTF-8",
   133  		"resource not found",
   134  		nil,
   135  		nil,
   136  	}
   137  	errNotFoundJSON = &errorResponse{
   138  		http.StatusNotFound,
   139  		`{"itemNotFound": {"message": "The resource could not be found.", "code": 404}}`,
   140  		"application/json; charset=UTF-8",
   141  		"resource not found",
   142  		nil,
   143  		nil,
   144  	}
   145  	errNotFoundJSONSG = &errorResponse{
   146  		http.StatusNotFound,
   147  		`{"itemNotFound": {"message": "Security group $ID$ not found.", "code": 404}}`,
   148  		"application/json; charset=UTF-8",
   149  		"",
   150  		nil,
   151  		nil,
   152  	}
   153  	errNotFoundJSONSGR = &errorResponse{
   154  		http.StatusNotFound,
   155  		`{"itemNotFound": {"message": "Rule ($ID$) not found.", "code": 404}}`,
   156  		"application/json; charset=UTF-8",
   157  		"security rule not found",
   158  		nil,
   159  		nil,
   160  	}
   161  	errMultipleChoices = &errorResponse{
   162  		http.StatusMultipleChoices,
   163  		`{"choices": [{"status": "CURRENT", "media-types": [{"base": ` +
   164  			`"application/xml", "type": "application/vnd.openstack.compute+` +
   165  			`xml;version=2"}, {"base": "application/json", "type": "application/` +
   166  			`vnd.openstack.compute+json;version=2"}], "id": "v2.0", "links": ` +
   167  			`[{"href": "$ENDPOINT$$URL$", "rel": "self"}]}]}`,
   168  		"application/json",
   169  		"multiple URL redirection choices",
   170  		nil,
   171  		nil,
   172  	}
   173  	errNoVersion = &errorResponse{
   174  		http.StatusOK,
   175  		`{"versions": [` +
   176  			`{"id": "v2.0", "links": [{"href": "v2", "rel": "self"}], "status": "SUPPORTED", "updated": "2011-01-21T11:33:21Z"}]}`,
   177  		"application/json",
   178  		"no version specified in URL",
   179  		nil,
   180  		nil,
   181  	}
   182  	errVersionsLinks = &errorResponse{
   183  		http.StatusOK,
   184  		`{"version": {"status": "CURRENT", "updated": "2011-01-21T11` +
   185  			`:33:21Z", "media-types": [{"base": "application/xml", "type": ` +
   186  			`"application/vnd.openstack.compute+xml;version=2"}, {"base": ` +
   187  			`"application/json", "type": "application/vnd.openstack.compute` +
   188  			`+json;version=2"}], "id": "v2.0", "links": [{"href": "$ENDPOINT$"` +
   189  			`, "rel": "self"}, {"href": "http://docs.openstack.org/api/openstack` +
   190  			`-compute/1.1/os-compute-devguide-1.1.pdf", "type": "application/pdf` +
   191  			`", "rel": "describedby"}, {"href": "http://docs.openstack.org/api/` +
   192  			`openstack-compute/1.1/wadl/os-compute-1.1.wadl", "type": ` +
   193  			`"application/vnd.sun.wadl+xml", "rel": "describedby"}]}}`,
   194  		"application/json",
   195  		"version missing from URL",
   196  		nil,
   197  		nil,
   198  	}
   199  	errNotImplemented = &errorResponse{
   200  		http.StatusNotImplemented,
   201  		"501 Not Implemented",
   202  		"text/plain; charset=UTF-8",
   203  		"not implemented",
   204  		nil,
   205  		nil,
   206  	}
   207  	errNoGroupId = &errorResponse{
   208  		errorText: "no security group id given",
   209  	}
   210  	errRateLimitExceeded = &errorResponse{
   211  		http.StatusRequestEntityTooLarge,
   212  		"",
   213  		"text/plain; charset=UTF-8",
   214  		"too many requests",
   215  		// RFC says that Retry-After should be an int, but we don't want to wait an entire second during the test suite.
   216  		map[string]string{"Retry-After": "0.001"},
   217  		nil,
   218  	}
   219  	errMaxRequestRateExceeded = &errorResponse{
   220  		http.StatusServiceUnavailable,
   221  		"",
   222  		"text/plain; charset=UTF-8",
   223  		"The maximum request receiving rate is exceeded",
   224  		// RFC says that Retry-After should be an int, but we don't want to wait an entire second during the test suite.
   225  		map[string]string{"Retry-After": "0.001"},
   226  		nil,
   227  	}
   228  	errTooManyRequests = &errorResponse{
   229  		http.StatusTooManyRequests,
   230  		"",
   231  		"text/plain; charset=UTF-8",
   232  		"too man requests",
   233  		// RFC says that Retry-After should be an int, but we don't want to wait an entire second during the test suite.
   234  		map[string]string{"Retry-After": "0.001"},
   235  		nil,
   236  	}
   237  	errForbiddenRetryAfter = &errorResponse{
   238  		http.StatusForbidden,
   239  		"",
   240  		"text/plain; charset=UTF-8",
   241  		"Forbidden, please retry",
   242  		// RFC says that Retry-After should be an int, but we don't want to wait an entire second during the test suite.
   243  		map[string]string{"Retry-After": "0.001"},
   244  		nil,
   245  	}
   246  	errNoMoreFloatingIPs = &errorResponse{
   247  		http.StatusNotFound,
   248  		"Zero floating ips available.",
   249  		"text/plain; charset=UTF-8",
   250  		"zero floating ips available",
   251  		nil,
   252  		nil,
   253  	}
   254  	errIPLimitExceeded = &errorResponse{
   255  		http.StatusRequestEntityTooLarge,
   256  		"Maximum number of floating ips exceeded.",
   257  		"text/plain; charset=UTF-8",
   258  		"maximum number of floating ips exceeded",
   259  		nil,
   260  		nil,
   261  	}
   262  )
   263  
   264  func (e *errorResponse) Error() string {
   265  	return e.errorText
   266  }
   267  
   268  // requestBody returns the body for the error response, replacing
   269  // $ENDPOINT$, $URL$, $ID$, and $ERROR$ in e.body with the values from
   270  // the request.
   271  func (e *errorResponse) requestBody(r *http.Request) []byte {
   272  	url := strings.TrimLeft(r.URL.Path, "/")
   273  	body := e.body
   274  	if body != "" {
   275  		if e.nova != nil {
   276  			body = strings.Replace(body, "$ENDPOINT$", e.nova.endpointURL(true, "/"), -1)
   277  		}
   278  		body = strings.Replace(body, "$URL$", url, -1)
   279  		body = strings.Replace(body, "$ERROR$", e.Error(), -1)
   280  		if slash := strings.LastIndex(url, "/"); slash != -1 {
   281  			body = strings.Replace(body, "$ID$", url[slash+1:], -1)
   282  		}
   283  	}
   284  	return []byte(body)
   285  }
   286  
   287  func (e *errorResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   288  	if e.contentType != "" {
   289  		w.Header().Set("Content-Type", e.contentType)
   290  	}
   291  	body := e.requestBody(r)
   292  	if e.headers != nil {
   293  		for h, v := range e.headers {
   294  			w.Header().Set(h, v)
   295  		}
   296  	}
   297  	// workaround for https://code.google.com/p/go/issues/detail?id=4454
   298  	w.Header().Set("Content-Length", strconv.Itoa(len(body)))
   299  	if e.code != 0 {
   300  		w.WriteHeader(e.code)
   301  	}
   302  	if len(body) > 0 {
   303  		w.Write(body)
   304  	}
   305  }
   306  
   307  type novaHandler struct {
   308  	n      *Nova
   309  	method func(n *Nova, w http.ResponseWriter, r *http.Request) error
   310  }
   311  
   312  func userInfo(i identityservice.IdentityService, r *http.Request) (*identityservice.UserInfo, error) {
   313  	return i.FindUser(r.Header.Get(authToken))
   314  }
   315  
   316  func (h *novaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   317  	path := r.URL.Path
   318  	// handle invalid X-Auth-Token header
   319  	_, err := userInfo(h.n.IdentityService, r)
   320  	if err != nil {
   321  		errUnauthorized.ServeHTTP(w, r)
   322  		return
   323  	}
   324  	// handle trailing slash in the path
   325  	if strings.HasSuffix(path, "/") && path != "/" {
   326  		errNotFound.ServeHTTP(w, r)
   327  		return
   328  	}
   329  	err = h.method(h.n, w, r)
   330  	if err == nil {
   331  		return
   332  	}
   333  	var resp http.Handler
   334  
   335  	if err == testservices.RateLimitExceededError {
   336  		resp = errRateLimitExceeded
   337  	} else if err == testservices.ServiceUnavailRateLimitError {
   338  		resp = errMaxRequestRateExceeded
   339  	} else if err == testservices.TooManyRequestsError {
   340  		resp = errTooManyRequests
   341  	} else if err == testservices.ForbiddenRateLimitError {
   342  		resp = errForbiddenRetryAfter
   343  	} else if err == testservices.NoMoreFloatingIPs {
   344  		resp = errNoMoreFloatingIPs
   345  	} else if err == testservices.IPLimitExceeded {
   346  		resp = errIPLimitExceeded
   347  	} else {
   348  		resp, _ = err.(http.Handler)
   349  		if resp == nil {
   350  			code, encodedErr := errorJSONEncode(err)
   351  			resp = &errorResponse{
   352  				code,
   353  				encodedErr,
   354  				"application/json",
   355  				err.Error(),
   356  				nil,
   357  				h.n,
   358  			}
   359  		}
   360  	}
   361  	resp.ServeHTTP(w, r)
   362  }
   363  
   364  func writeResponse(w http.ResponseWriter, code int, body []byte) {
   365  	// workaround for https://code.google.com/p/go/issues/detail?id=4454
   366  	w.Header().Set("Content-Length", strconv.Itoa(len(body)))
   367  	w.WriteHeader(code)
   368  	w.Write(body)
   369  }
   370  
   371  // sendJSON sends the specified response serialized as JSON.
   372  func sendJSON(code int, resp interface{}, w http.ResponseWriter, r *http.Request) error {
   373  	data, err := json.Marshal(resp)
   374  	if err != nil {
   375  		return err
   376  	}
   377  	writeResponse(w, code, data)
   378  	return nil
   379  }
   380  
   381  func (n *Nova) handler(method func(n *Nova, w http.ResponseWriter, r *http.Request) error) http.Handler {
   382  	return &novaHandler{n, method}
   383  }
   384  
   385  func (n *Nova) handleRoot(w http.ResponseWriter, r *http.Request) error {
   386  	if r.URL.Path == "/" {
   387  		return errNoVersion
   388  	}
   389  	return errMultipleChoices
   390  }
   391  
   392  func (n *Nova) HandleRoot(w http.ResponseWriter, r *http.Request) {
   393  	n.handler((*Nova).handleRoot).ServeHTTP(w, r)
   394  }
   395  
   396  // handleFlavors handles the flavors HTTP API.
   397  func (n *Nova) handleFlavors(w http.ResponseWriter, r *http.Request) error {
   398  	switch r.Method {
   399  	case "GET":
   400  		if flavorId := path.Base(r.URL.Path); flavorId != "flavors" {
   401  			flavor, err := n.flavor(flavorId)
   402  			if err != nil {
   403  				return errNotFound
   404  			}
   405  			resp := struct {
   406  				Flavor nova.FlavorDetail `json:"flavor"`
   407  			}{*flavor}
   408  			return sendJSON(http.StatusOK, resp, w, r)
   409  		}
   410  		entities := n.allFlavorsAsEntities()
   411  		if len(entities) == 0 {
   412  			entities = []nova.Entity{}
   413  		}
   414  		resp := struct {
   415  			Flavors []nova.Entity `json:"flavors"`
   416  		}{entities}
   417  		return sendJSON(http.StatusOK, resp, w, r)
   418  	case "POST":
   419  		if flavorId := path.Base(r.URL.Path); flavorId != "flavors" {
   420  			return errNotFound
   421  		}
   422  		body, err := ioutil.ReadAll(r.Body)
   423  		if err != nil {
   424  			return err
   425  		}
   426  		if len(body) == 0 {
   427  			return errBadRequest2
   428  		}
   429  		return errNotImplemented
   430  	case "PUT":
   431  		if flavorId := path.Base(r.URL.Path); flavorId != "flavors" {
   432  			return errNotFoundJSON
   433  		}
   434  		return errNotFound
   435  	case "DELETE":
   436  		if flavorId := path.Base(r.URL.Path); flavorId != "flavors" {
   437  			return errForbidden
   438  		}
   439  		return errNotFound
   440  	}
   441  	return fmt.Errorf("unknown request method %q for %s", r.Method, r.URL.Path)
   442  }
   443  
   444  // handleFlavorsDetail handles the flavors/detail HTTP API.
   445  func (n *Nova) handleFlavorsDetail(w http.ResponseWriter, r *http.Request) error {
   446  	switch r.Method {
   447  	case "GET":
   448  		if flavorId := path.Base(r.URL.Path); flavorId != "detail" {
   449  			return errNotFound
   450  		}
   451  		flavors := n.allFlavors()
   452  		if len(flavors) == 0 {
   453  			flavors = []nova.FlavorDetail{}
   454  		}
   455  		resp := struct {
   456  			Flavors []nova.FlavorDetail `json:"flavors"`
   457  		}{flavors}
   458  		return sendJSON(http.StatusOK, resp, w, r)
   459  	case "POST":
   460  		return errNotFound
   461  	case "PUT":
   462  		if flavorId := path.Base(r.URL.Path); flavorId != "detail" {
   463  			return errNotFound
   464  		}
   465  		return errNotFoundJSON
   466  	case "DELETE":
   467  		if flavorId := path.Base(r.URL.Path); flavorId != "detail" {
   468  			return errNotFound
   469  		}
   470  		return errForbidden
   471  	}
   472  	return fmt.Errorf("unknown request method %q for %s", r.Method, r.URL.Path)
   473  }
   474  
   475  // handleServerActions handles the servers/<id>/action HTTP API.
   476  func (n *Nova) handleServerActions(server *nova.ServerDetail, w http.ResponseWriter, r *http.Request) error {
   477  	if server == nil {
   478  		return errNotFound
   479  	}
   480  	body, err := ioutil.ReadAll(r.Body)
   481  	if err != nil || len(body) == 0 {
   482  		return errNotFound
   483  	}
   484  	var action struct {
   485  		AddSecurityGroup *struct {
   486  			Name string
   487  		}
   488  		RemoveSecurityGroup *struct {
   489  			Name string
   490  		}
   491  		AddFloatingIP *struct {
   492  			Address string
   493  		}
   494  		RemoveFloatingIP *struct {
   495  			Address string
   496  		}
   497  	}
   498  	if err := json.Unmarshal(body, &action); err != nil {
   499  		return err
   500  	}
   501  	switch {
   502  	case action.AddSecurityGroup != nil:
   503  		name := action.AddSecurityGroup.Name
   504  		group, err := n.securityGroupByName(name)
   505  		if err != nil || n.hasServerSecurityGroup(server.Id, group.Id) {
   506  			return errNotFound
   507  		}
   508  		if err := n.addServerSecurityGroup(server.Id, group.Id); err != nil {
   509  			return err
   510  		}
   511  		writeResponse(w, http.StatusAccepted, nil)
   512  		return nil
   513  	case action.RemoveSecurityGroup != nil:
   514  		name := action.RemoveSecurityGroup.Name
   515  		group, err := n.securityGroupByName(name)
   516  		if err != nil || !n.hasServerSecurityGroup(server.Id, group.Id) {
   517  			return errNotFound
   518  		}
   519  		if err := n.removeServerSecurityGroup(server.Id, group.Id); err != nil {
   520  			return err
   521  		}
   522  		writeResponse(w, http.StatusAccepted, nil)
   523  		return nil
   524  	case action.AddFloatingIP != nil:
   525  		addr := action.AddFloatingIP.Address
   526  		if n.hasServerFloatingIP(server.Id, addr) {
   527  			return errNotFound
   528  		}
   529  		fip, err := n.floatingIPByAddr(addr)
   530  		if err != nil {
   531  			return errNotFound
   532  		}
   533  		if err := n.addServerFloatingIP(server.Id, fip.Id); err != nil {
   534  			return err
   535  		}
   536  		writeResponse(w, http.StatusAccepted, nil)
   537  		return nil
   538  	case action.RemoveFloatingIP != nil:
   539  		addr := action.RemoveFloatingIP.Address
   540  		if !n.hasServerFloatingIP(server.Id, addr) {
   541  			return errNotFound
   542  		}
   543  		fip, err := n.floatingIPByAddr(addr)
   544  		if err != nil {
   545  			return errNotFound
   546  		}
   547  		if err := n.removeServerFloatingIP(server.Id, fip.Id); err != nil {
   548  			return err
   549  		}
   550  		writeResponse(w, http.StatusAccepted, nil)
   551  		return nil
   552  	}
   553  	return fmt.Errorf("unknown server action: %q", string(body))
   554  }
   555  
   556  // handleServerMetadata handles the servers/<id>/action HTTP API.
   557  func (n *Nova) handleServerMetadata(server *nova.ServerDetail, w http.ResponseWriter, r *http.Request) error {
   558  	if server == nil {
   559  		return errNotFound
   560  	}
   561  	body, err := ioutil.ReadAll(r.Body)
   562  	if err != nil || len(body) == 0 {
   563  		return errNotFound
   564  	}
   565  	var req struct {
   566  		Metadata map[string]string `json:"metadata"`
   567  	}
   568  	if err := json.Unmarshal(body, &req); err != nil {
   569  		return err
   570  	}
   571  	if err := n.setServerMetadata(server.Id, req.Metadata); err != nil {
   572  		return err
   573  	}
   574  	writeResponse(w, http.StatusOK, nil)
   575  	return nil
   576  }
   577  
   578  // newUUID generates a random UUID conforming to RFC 4122.
   579  func newUUID() (string, error) {
   580  	uuid := make([]byte, 16)
   581  	if _, err := io.ReadFull(rand.Reader, uuid); err != nil {
   582  		return "", err
   583  	}
   584  	uuid[8] = uuid[8]&^0xc0 | 0x80 // variant bits; see section 4.1.1.
   585  	uuid[6] = uuid[6]&^0xf0 | 0x40 // version 4; see section 4.1.3.
   586  	return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil
   587  }
   588  
   589  // noGroupError constructs a bad request response for an invalid group.
   590  func noGroupError(groupName, tenantId string) error {
   591  	return &errorResponse{
   592  		http.StatusBadRequest,
   593  		`{"badRequest": {"message": "Security group ` + groupName + ` not found for project ` + tenantId + `.", "code": 400}}`,
   594  		"application/json; charset=UTF-8",
   595  		"bad request URL",
   596  		nil,
   597  		nil,
   598  	}
   599  }
   600  
   601  // handleRunServer handles creating and running a server.
   602  func (n *Nova) handleRunServer(body []byte, w http.ResponseWriter, r *http.Request) error {
   603  	var req struct {
   604  		Server struct {
   605  			FlavorRef          string
   606  			ImageRef           string
   607  			Name               string
   608  			Metadata           map[string]string
   609  			SecurityGroups     []map[string]string `json:"security_groups"`
   610  			Networks           []map[string]string
   611  			AvailabilityZone   string                    `json:"availability_zone"`
   612  			BlockDeviceMapping []nova.BlockDeviceMapping `json:"block_device_mapping_v2,omitempty"`
   613  		}
   614  	}
   615  	if err := json.Unmarshal(body, &req); err != nil {
   616  		return errBadRequest3
   617  	}
   618  	if req.Server.Name == "" {
   619  		return errBadRequestSrvName
   620  	}
   621  	if req.Server.ImageRef == "" && req.Server.BlockDeviceMapping == nil {
   622  		return errBadRequestSrvImage
   623  	}
   624  	if req.Server.FlavorRef == "" {
   625  		return errBadRequestSrvFlavor
   626  	}
   627  	if az := req.Server.AvailabilityZone; az != "" {
   628  		if !n.availabilityZones[az].State.Available {
   629  			return testservices.AvailabilityZoneIsNotAvailable
   630  		}
   631  	}
   632  	n.nextServerId++
   633  	id := strconv.Itoa(n.nextServerId)
   634  	uuid, err := newUUID()
   635  	if err != nil {
   636  		return err
   637  	}
   638  	// TODO(gz) some kind of sane handling of networks
   639  	// only networks with sub-nets should be used for boot
   640  	createSecurityGroups := true
   641  	for _, net := range req.Server.Networks {
   642  		var netPortSecurity *neutron.NetworkV2
   643  		var err error
   644  		if n.useNeutronNetworking {
   645  			netPortSecurity, err = n.neutronModel.Network(net["uuid"])
   646  			if err != nil {
   647  				return errNotFoundJSON
   648  			}
   649  			if createSecurityGroups && netPortSecurity.PortSecurityEnabled != nil {
   650  				createSecurityGroups = *netPortSecurity.PortSecurityEnabled
   651  			}
   652  		} else {
   653  			_, err = n.network(net["uuid"])
   654  			if err != nil {
   655  				return errNotFoundJSON
   656  			}
   657  		}
   658  	}
   659  	// TODO: (hml) - 2017-04-26
   660  	// If the test server had state for an instance, if createSecurityGroups is
   661  	// false and req.Server.SecurityGroups > 0, during the "build" process instance
   662  	// state should be set to ERROR and a server.fault should be filled in.
   663  	// Related to neutron network.port_security_enabled.
   664  	var groups []string
   665  	if len(req.Server.SecurityGroups) > 0 && createSecurityGroups {
   666  		for _, group := range req.Server.SecurityGroups {
   667  			groupName := group["name"]
   668  			if sg, err := n.securityGroupByName(groupName); err != nil {
   669  				return noGroupError(groupName, n.TenantId)
   670  			} else {
   671  				groups = append(groups, sg.Id)
   672  			}
   673  		}
   674  	}
   675  	// TODO(dimitern) - 2013-02-11 bug=1121684
   676  	// make sure flavor/image exist (if needed)
   677  	flavor := nova.FlavorDetail{Id: req.Server.FlavorRef}
   678  	n.buildFlavorLinks(&flavor)
   679  	flavorEnt := nova.Entity{Id: flavor.Id, Links: flavor.Links}
   680  	image := nova.Entity{Id: req.Server.ImageRef}
   681  	timestr := time.Now().Format(time.RFC3339)
   682  	userInfo, _ := userInfo(n.IdentityService, r)
   683  	server := nova.ServerDetail{
   684  		Id:               id,
   685  		UUID:             uuid,
   686  		Name:             req.Server.Name,
   687  		TenantId:         n.TenantId,
   688  		UserId:           userInfo.Id,
   689  		HostId:           "1",
   690  		Image:            image,
   691  		Flavor:           flavorEnt,
   692  		Status:           nova.StatusBuild,
   693  		Created:          timestr,
   694  		Updated:          timestr,
   695  		Addresses:        make(map[string][]nova.IPAddress),
   696  		AvailabilityZone: req.Server.AvailabilityZone,
   697  		Metadata:         req.Server.Metadata,
   698  	}
   699  	servers, err := n.allServers(nil)
   700  	if err != nil {
   701  		return err
   702  	}
   703  	nextServer := len(servers) + 1
   704  	n.buildServerLinks(&server)
   705  	// set some IP addresses
   706  	addr := fmt.Sprintf("127.10.0.%d", nextServer)
   707  	server.Addresses["public"] = []nova.IPAddress{{4, addr, "fixed"}, {6, "::dead:beef:f00d", "fixed"}}
   708  	addr = fmt.Sprintf("127.0.0.%d", nextServer)
   709  	server.Addresses["private"] = []nova.IPAddress{{4, addr, "fixed"}, {6, "::face::000f", "fixed"}}
   710  	if err := n.addServer(server); err != nil {
   711  		return err
   712  	}
   713  	var resp struct {
   714  		Server struct {
   715  			SecurityGroups []map[string]string `json:"security_groups"`
   716  			Id             string              `json:"id"`
   717  			Links          []nova.Link         `json:"links"`
   718  			AdminPass      string              `json:"adminPass"`
   719  		} `json:"server"`
   720  	}
   721  	if len(req.Server.SecurityGroups) > 0 {
   722  		for _, gid := range groups {
   723  			if err := n.addServerSecurityGroup(id, gid); err != nil {
   724  				return err
   725  			}
   726  		}
   727  		resp.Server.SecurityGroups = req.Server.SecurityGroups
   728  	} else {
   729  		if createSecurityGroups {
   730  			resp.Server.SecurityGroups = []map[string]string{{"name": "default"}}
   731  		} else {
   732  			resp.Server.SecurityGroups = []map[string]string{{}}
   733  		}
   734  	}
   735  	resp.Server.Id = id
   736  	resp.Server.Links = server.Links
   737  	resp.Server.AdminPass = "secret"
   738  	return sendJSON(http.StatusAccepted, resp, w, r)
   739  }
   740  
   741  // handleServers handles the servers HTTP API.
   742  func (n *Nova) handleServers(w http.ResponseWriter, r *http.Request) error {
   743  	// Handle os volume attachments as a leaf of a server.
   744  	if strings.Contains(r.URL.Path, "os-volume_attachments") {
   745  		switch r.Method {
   746  		case "GET":
   747  			return n.handleListVolumes(w, r)
   748  		case "POST":
   749  			return n.handleAttachVolumes(w, r)
   750  		case "DELETE":
   751  			return n.handleDetachVolumes(w, r)
   752  		}
   753  	}
   754  
   755  	// Handle os interfaces as a leaf of a server.
   756  	if strings.Contains(r.URL.Path, "os-interface") {
   757  		return n.handleOSInterfaces(w, r)
   758  	}
   759  
   760  	// Handle server related functionality directly.
   761  	switch r.Method {
   762  	case "GET":
   763  		if suffix := path.Base(r.URL.Path); suffix != "servers" {
   764  			groups := false
   765  			serverId := ""
   766  			if suffix == "os-security-groups" {
   767  				// handle GET /servers/<id>/os-security-groups
   768  				serverId = path.Base(strings.Replace(r.URL.Path, "/os-security-groups", "", 1))
   769  				groups = true
   770  			} else {
   771  				serverId = suffix
   772  			}
   773  			server, err := n.server(serverId)
   774  			if err != nil {
   775  				return err
   776  			}
   777  			if groups {
   778  				srvGroups := n.allServerSecurityGroups(serverId)
   779  				if len(srvGroups) == 0 {
   780  					srvGroups = []nova.SecurityGroup{}
   781  				}
   782  				resp := struct {
   783  					Groups []nova.SecurityGroup `json:"security_groups"`
   784  				}{srvGroups}
   785  				return sendJSON(http.StatusOK, resp, w, r)
   786  			}
   787  
   788  			resp := struct {
   789  				Server nova.ServerDetail `json:"server"`
   790  			}{*server}
   791  			return sendJSON(http.StatusOK, resp, w, r)
   792  		}
   793  		f := make(filter)
   794  		if err := r.ParseForm(); err == nil && len(r.Form) > 0 {
   795  			for filterKey, filterValues := range r.Form {
   796  				for _, value := range filterValues {
   797  					f[filterKey] = value
   798  				}
   799  			}
   800  		}
   801  		entities, err := n.allServersAsEntities(f)
   802  		if err != nil {
   803  			return err
   804  		}
   805  		if len(entities) == 0 {
   806  			entities = []nova.Entity{}
   807  		}
   808  		resp := struct {
   809  			Servers []nova.Entity `json:"servers"`
   810  		}{entities}
   811  		return sendJSON(http.StatusOK, resp, w, r)
   812  	case "POST":
   813  		if suffix := path.Base(r.URL.Path); suffix != "servers" {
   814  			serverId := ""
   815  			if suffix == "action" {
   816  				// handle POST /servers/<id>/action
   817  				serverId = path.Base(strings.Replace(r.URL.Path, "/action", "", 1))
   818  				server, _ := n.server(serverId)
   819  				return n.handleServerActions(server, w, r)
   820  			} else if suffix == "metadata" {
   821  				// handle POST /servers/<id>/metadata
   822  				serverId = path.Base(strings.Replace(r.URL.Path, "/metadata", "", 1))
   823  				server, _ := n.server(serverId)
   824  				return n.handleServerMetadata(server, w, r)
   825  			} else {
   826  				serverId = suffix
   827  			}
   828  			return errNotFound
   829  		}
   830  		body, err := ioutil.ReadAll(r.Body)
   831  		if err != nil {
   832  			return err
   833  		}
   834  		if len(body) == 0 {
   835  			return errBadRequest2
   836  		}
   837  		return n.handleRunServer(body, w, r)
   838  	case "PUT":
   839  		serverId := path.Base(r.URL.Path)
   840  		if serverId == "servers" {
   841  			return errNotFound
   842  		}
   843  
   844  		var req struct {
   845  			Server struct {
   846  				Name string `json:"name"`
   847  			} `json:"server"`
   848  		}
   849  
   850  		body, err := ioutil.ReadAll(r.Body)
   851  		if err != nil || len(body) == 0 {
   852  			return errBadRequest2
   853  		}
   854  		if err := json.Unmarshal(body, &req); err != nil {
   855  			return err
   856  		}
   857  
   858  		err = n.updateServerName(serverId, req.Server.Name)
   859  		if err != nil {
   860  			return err
   861  		}
   862  
   863  		server, err := n.server(serverId)
   864  		if err != nil {
   865  			return err
   866  		}
   867  		var resp struct {
   868  			Server nova.ServerDetail `json:"server"`
   869  		}
   870  		resp.Server = *server
   871  		return sendJSON(http.StatusOK, resp, w, r)
   872  	case "DELETE":
   873  		if serverId := path.Base(r.URL.Path); serverId != "servers" {
   874  			if _, err := n.server(serverId); err != nil {
   875  				return errNotFoundJSON
   876  			}
   877  			if err := n.removeServer(serverId); err != nil {
   878  				return err
   879  			}
   880  			writeResponse(w, http.StatusNoContent, nil)
   881  			return nil
   882  		}
   883  		return errNotFound
   884  	}
   885  	return fmt.Errorf("unknown request method %q for %s", r.Method, r.URL.Path)
   886  }
   887  
   888  // handleServersDetail handles the servers/detail HTTP API.
   889  func (n *Nova) handleServersDetail(w http.ResponseWriter, r *http.Request) error {
   890  	switch r.Method {
   891  	case "GET":
   892  		if serverId := path.Base(r.URL.Path); serverId != "detail" {
   893  			return errNotFound
   894  		}
   895  		f := make(filter)
   896  		if err := r.ParseForm(); err == nil && len(r.Form) > 0 {
   897  			for filterKey, filterValues := range r.Form {
   898  				for _, value := range filterValues {
   899  					f[filterKey] = value
   900  				}
   901  			}
   902  		}
   903  		servers, err := n.allServers(f)
   904  		if err != nil {
   905  			return err
   906  		}
   907  		if len(servers) == 0 {
   908  			servers = []nova.ServerDetail{}
   909  		}
   910  		resp := struct {
   911  			Servers []nova.ServerDetail `json:"servers"`
   912  		}{servers}
   913  		return sendJSON(http.StatusOK, resp, w, r)
   914  	case "POST":
   915  		return errNotFound
   916  	case "PUT":
   917  		if serverId := path.Base(r.URL.Path); serverId != "detail" {
   918  			return errNotFound
   919  		}
   920  		return errBadRequest2
   921  	case "DELETE":
   922  		if serverId := path.Base(r.URL.Path); serverId != "detail" {
   923  			return errNotFound
   924  		}
   925  		return errNotFoundJSON
   926  	}
   927  	return fmt.Errorf("unknown request method %q for %s", r.Method, r.URL.Path)
   928  }
   929  
   930  // processGroupId returns the group id from the given request.
   931  // If there was no group id specified in the path, it returns errNoGroupId
   932  func (n *Nova) processGroupId(w http.ResponseWriter, r *http.Request) (*nova.SecurityGroup, error) {
   933  	if groupId := path.Base(r.URL.Path); groupId != "os-security-groups" {
   934  		group, err := n.securityGroup(groupId)
   935  		if err != nil {
   936  			return nil, errNotFoundJSONSG
   937  		}
   938  		return group, nil
   939  	}
   940  	return nil, errNoGroupId
   941  }
   942  
   943  // handleSecurityGroups handles the os-security-groups HTTP API.
   944  func (n *Nova) handleSecurityGroups(w http.ResponseWriter, r *http.Request) error {
   945  	switch r.Method {
   946  	case "GET":
   947  		group, err := n.processGroupId(w, r)
   948  		if err == errNoGroupId {
   949  			groups := n.allSecurityGroups()
   950  			if len(groups) == 0 {
   951  				groups = []nova.SecurityGroup{}
   952  			}
   953  			resp := struct {
   954  				Groups []nova.SecurityGroup `json:"security_groups"`
   955  			}{groups}
   956  			return sendJSON(http.StatusOK, resp, w, r)
   957  		}
   958  		if err != nil {
   959  			return err
   960  		}
   961  		resp := struct {
   962  			Group nova.SecurityGroup `json:"security_group"`
   963  		}{*group}
   964  		return sendJSON(http.StatusOK, resp, w, r)
   965  	case "POST":
   966  		if groupId := path.Base(r.URL.Path); groupId != "os-security-groups" {
   967  			return errNotFound
   968  		}
   969  		body, err := ioutil.ReadAll(r.Body)
   970  		if err != nil || len(body) == 0 {
   971  			return errBadRequest2
   972  		}
   973  		var req struct {
   974  			Group struct {
   975  				Name        string
   976  				Description string
   977  			} `json:"security_group"`
   978  		}
   979  		if err := json.Unmarshal(body, &req); err != nil {
   980  			return err
   981  		} else {
   982  			_, err := n.securityGroupByName(req.Group.Name)
   983  			if err == nil {
   984  				return errBadRequestDuplicateValue
   985  			}
   986  			n.nextGroupId++
   987  			nextId := strconv.Itoa(n.nextGroupId)
   988  			err = n.addSecurityGroup(nova.SecurityGroup{
   989  				Id:          nextId,
   990  				Name:        req.Group.Name,
   991  				Description: req.Group.Description,
   992  				TenantId:    n.TenantId,
   993  			})
   994  			if err != nil {
   995  				return err
   996  			}
   997  			group, err := n.securityGroup(nextId)
   998  			if err != nil {
   999  				return err
  1000  			}
  1001  			var resp struct {
  1002  				Group nova.SecurityGroup `json:"security_group"`
  1003  			}
  1004  			resp.Group = *group
  1005  			return sendJSON(http.StatusOK, resp, w, r)
  1006  		}
  1007  	case "PUT":
  1008  		if groupId := path.Base(r.URL.Path); groupId == "os-security-groups" {
  1009  			return errNotFound
  1010  		}
  1011  		group, err := n.processGroupId(w, r)
  1012  		if err != nil {
  1013  			return err
  1014  		}
  1015  
  1016  		var req struct {
  1017  			Group struct {
  1018  				Name        string
  1019  				Description string
  1020  			} `json:"security_group"`
  1021  		}
  1022  		body, err := ioutil.ReadAll(r.Body)
  1023  		if err != nil || len(body) == 0 {
  1024  			return errBadRequest2
  1025  		}
  1026  		if err := json.Unmarshal(body, &req); err != nil {
  1027  			return err
  1028  		}
  1029  
  1030  		err = n.updateSecurityGroup(nova.SecurityGroup{
  1031  			Id:          group.Id,
  1032  			Name:        req.Group.Name,
  1033  			Description: req.Group.Description,
  1034  			TenantId:    group.TenantId,
  1035  		})
  1036  		if err != nil {
  1037  			return err
  1038  		}
  1039  		group, err = n.securityGroup(group.Id)
  1040  		if err != nil {
  1041  			return err
  1042  		}
  1043  		var resp struct {
  1044  			Group nova.SecurityGroup `json:"security_group"`
  1045  		}
  1046  		resp.Group = *group
  1047  		return sendJSON(http.StatusOK, resp, w, r)
  1048  
  1049  	case "DELETE":
  1050  		if group, err := n.processGroupId(w, r); group != nil {
  1051  			if err := n.removeSecurityGroup(group.Id); err != nil {
  1052  				return err
  1053  			}
  1054  			writeResponse(w, http.StatusAccepted, nil)
  1055  			return nil
  1056  		} else if err == errNoGroupId {
  1057  			return errNotFound
  1058  		} else {
  1059  			return err
  1060  		}
  1061  	}
  1062  	return fmt.Errorf("unknown request method %q for %s", r.Method, r.URL.Path)
  1063  }
  1064  
  1065  // handleSecurityGroupRules handles the os-security-group-rules HTTP API.
  1066  func (n *Nova) handleSecurityGroupRules(w http.ResponseWriter, r *http.Request) error {
  1067  	switch r.Method {
  1068  	case "GET":
  1069  		return errNotFoundJSON
  1070  	case "POST":
  1071  		if ruleId := path.Base(r.URL.Path); ruleId != "os-security-group-rules" {
  1072  			return errNotFound
  1073  		}
  1074  		body, err := ioutil.ReadAll(r.Body)
  1075  		if err != nil || len(body) == 0 {
  1076  			return errBadRequest2
  1077  		}
  1078  		var req struct {
  1079  			Rule nova.RuleInfo `json:"security_group_rule"`
  1080  		}
  1081  		if err = json.Unmarshal(body, &req); err != nil {
  1082  			return err
  1083  		}
  1084  		inrule := req.Rule
  1085  		group, err := n.securityGroup(inrule.ParentGroupId)
  1086  		if err != nil {
  1087  			return err // TODO: should be a 4XX error with details
  1088  		}
  1089  		for _, r := range group.Rules {
  1090  			// TODO: this logic is actually wrong, not what nova does at all
  1091  			// why are we reimplementing half of nova/api/openstack in go again?
  1092  			if r.IPProtocol != nil && *r.IPProtocol == inrule.IPProtocol &&
  1093  				r.FromPort != nil && *r.FromPort == inrule.FromPort &&
  1094  				r.ToPort != nil && *r.ToPort == inrule.ToPort {
  1095  				// TODO: Use a proper helper and sane error type
  1096  				return &errorResponse{
  1097  					http.StatusBadRequest,
  1098  					fmt.Sprintf(`{"badRequest": {"message": "This rule already exists in group %s", "code": 400}}`, group.Id),
  1099  					"application/json; charset=UTF-8",
  1100  					"rule already exists",
  1101  					nil,
  1102  					nil,
  1103  				}
  1104  			}
  1105  		}
  1106  		n.nextRuleId++
  1107  		nextId := strconv.Itoa(n.nextRuleId)
  1108  		err = n.addSecurityGroupRule(nextId, req.Rule)
  1109  		if err != nil {
  1110  			return err
  1111  		}
  1112  		rule, err := n.securityGroupRule(nextId)
  1113  		if err != nil {
  1114  			return err
  1115  		}
  1116  		var resp struct {
  1117  			Rule nova.SecurityGroupRule `json:"security_group_rule"`
  1118  		}
  1119  		resp.Rule = *rule
  1120  		return sendJSON(http.StatusOK, resp, w, r)
  1121  	case "PUT":
  1122  		if ruleId := path.Base(r.URL.Path); ruleId != "os-security-group-rules" {
  1123  			return errNotFoundJSON
  1124  		}
  1125  		return errNotFound
  1126  	case "DELETE":
  1127  		if ruleId := path.Base(r.URL.Path); ruleId != "os-security-group-rules" {
  1128  			if _, err := n.securityGroupRule(ruleId); err != nil {
  1129  				return errNotFoundJSONSGR
  1130  			}
  1131  			if err := n.removeSecurityGroupRule(ruleId); err != nil {
  1132  				return err
  1133  			}
  1134  			writeResponse(w, http.StatusAccepted, nil)
  1135  			return nil
  1136  		}
  1137  		return errNotFound
  1138  	}
  1139  	return fmt.Errorf("unknown request method %q for %s", r.Method, r.URL.Path)
  1140  }
  1141  
  1142  // handleFloatingIPs handles the os-floating-ips HTTP API.
  1143  func (n *Nova) handleFloatingIPs(w http.ResponseWriter, r *http.Request) error {
  1144  	switch r.Method {
  1145  	case "GET":
  1146  		if ipId := path.Base(r.URL.Path); ipId != "os-floating-ips" {
  1147  			fip, err := n.floatingIP(ipId)
  1148  			if err != nil {
  1149  				return errNotFoundJSON
  1150  			}
  1151  			resp := struct {
  1152  				IP nova.FloatingIP `json:"floating_ip"`
  1153  			}{*fip}
  1154  			return sendJSON(http.StatusOK, resp, w, r)
  1155  		}
  1156  		fips := n.allFloatingIPs()
  1157  		if len(fips) == 0 {
  1158  			fips = []nova.FloatingIP{}
  1159  		}
  1160  		resp := struct {
  1161  			IPs []nova.FloatingIP `json:"floating_ips"`
  1162  		}{fips}
  1163  		return sendJSON(http.StatusOK, resp, w, r)
  1164  	case "POST":
  1165  		if ipId := path.Base(r.URL.Path); ipId != "os-floating-ips" {
  1166  			return errNotFound
  1167  		}
  1168  		n.nextIPId++
  1169  		addr := fmt.Sprintf("10.0.0.%d", n.nextIPId)
  1170  		nextId := strconv.Itoa(n.nextIPId)
  1171  		fip := nova.FloatingIP{Id: nextId, IP: addr, Pool: "nova"}
  1172  		err := n.addFloatingIP(fip)
  1173  		if err != nil {
  1174  			return err
  1175  		}
  1176  		resp := struct {
  1177  			IP nova.FloatingIP `json:"floating_ip"`
  1178  		}{fip}
  1179  		return sendJSON(http.StatusOK, resp, w, r)
  1180  	case "PUT":
  1181  		if ipId := path.Base(r.URL.Path); ipId != "os-floating-ips" {
  1182  			return errNotFoundJSON
  1183  		}
  1184  		return errNotFound
  1185  	case "DELETE":
  1186  		if ipId := path.Base(r.URL.Path); ipId != "os-floating-ips" {
  1187  			if err := n.removeFloatingIP(ipId); err == nil {
  1188  				writeResponse(w, http.StatusAccepted, nil)
  1189  				return nil
  1190  			}
  1191  			return errNotFoundJSON
  1192  		}
  1193  		return errNotFound
  1194  	}
  1195  	return fmt.Errorf("unknown request method %q for %s", r.Method, r.URL.Path)
  1196  }
  1197  
  1198  // handleNetworks handles the os-networks HTTP API.
  1199  func (n *Nova) handleNetworks(w http.ResponseWriter, r *http.Request) error {
  1200  	switch r.Method {
  1201  	case "GET":
  1202  		if ipId := path.Base(r.URL.Path); ipId != "os-networks" {
  1203  			// TODO(gz): handle listing a single group
  1204  			return errNotFoundJSON
  1205  		}
  1206  		nets := n.allNetworks()
  1207  		if len(nets) == 0 {
  1208  			nets = []nova.Network{}
  1209  		}
  1210  		resp := struct {
  1211  			Network []nova.Network `json:"networks"`
  1212  		}{nets}
  1213  		return sendJSON(http.StatusOK, resp, w, r)
  1214  		// TODO(gz): proper handling of other methods
  1215  	}
  1216  	return fmt.Errorf("unknown request method %q for %s", r.Method, r.URL.Path)
  1217  }
  1218  
  1219  // handleAvailabilityZones handles the os-availability-zone HTTP API.
  1220  func (n *Nova) handleAvailabilityZones(w http.ResponseWriter, r *http.Request) error {
  1221  	switch r.Method {
  1222  	case "GET":
  1223  		if ipId := path.Base(r.URL.Path); ipId != "os-availability-zone" {
  1224  			return errNotFoundJSON
  1225  		}
  1226  		zones := n.allAvailabilityZones()
  1227  		if len(zones) == 0 {
  1228  			// If there are no availability zones defined, act as
  1229  			// if we don't support the availability zones extension.
  1230  			return errNotFoundJSON
  1231  		}
  1232  		resp := struct {
  1233  			Zones []nova.AvailabilityZone `json:"availabilityZoneInfo"`
  1234  		}{zones}
  1235  		return sendJSON(http.StatusOK, resp, w, r)
  1236  	}
  1237  	return fmt.Errorf("unknown request method %q for %s", r.Method, r.URL.Path)
  1238  }
  1239  
  1240  // handleOSInterfaces handles the os-interfaces HTTP API.
  1241  func (n *Nova) handleOSInterfaces(w http.ResponseWriter, r *http.Request) error {
  1242  	switch r.Method {
  1243  	case "GET":
  1244  		serverId := path.Base(strings.Replace(r.URL.Path, "/os-interface", "", 1))
  1245  
  1246  		interfaces := n.serverOSInterfaces(serverId)
  1247  		resp := struct {
  1248  			InterfaceAttachments []nova.OSInterface `json:"interfaceAttachments"`
  1249  		}{interfaces}
  1250  		return sendJSON(http.StatusOK, resp, w, r)
  1251  	}
  1252  	return fmt.Errorf("unknown request method %q for %s", r.Method, r.URL.Path)
  1253  }
  1254  
  1255  func (n *Nova) handleAttachVolumes(w http.ResponseWriter, r *http.Request) error {
  1256  	serverId := path.Base(strings.Replace(r.URL.Path, "/os-volume_attachments", "", 1))
  1257  
  1258  	bodyBytes, err := ioutil.ReadAll(r.Body)
  1259  	if err != nil {
  1260  		return err
  1261  	}
  1262  
  1263  	var attachment struct {
  1264  		VolumeAttachment nova.VolumeAttachment `json:"volumeAttachment"`
  1265  	}
  1266  	if err := json.Unmarshal(bodyBytes, &attachment); err != nil {
  1267  		return err
  1268  	}
  1269  
  1270  	if attachment.VolumeAttachment.Device != nil {
  1271  		if !regexpVolumeAttachmentDevice.MatchString(*attachment.VolumeAttachment.Device) {
  1272  			message := fmt.Sprintf(
  1273  				"Invalid input for field/attribute device. Value: '%s' does not match '%s'",
  1274  				*attachment.VolumeAttachment.Device, regexpVolumeAttachmentDevice,
  1275  			)
  1276  			return &errorResponse{
  1277  				http.StatusBadRequest,
  1278  				fmt.Sprintf(`{"badRequest": {"message": "%s", "code": 400}}`, message),
  1279  				"application/json; charset=UTF-8",
  1280  				message,
  1281  				nil,
  1282  				nil,
  1283  			}
  1284  		}
  1285  	}
  1286  
  1287  	var additionalProperties []string
  1288  	if attachment.VolumeAttachment.ServerId != "" {
  1289  		additionalProperties = append(additionalProperties, "'serverId'")
  1290  	}
  1291  	if len(additionalProperties) > 0 {
  1292  		message := fmt.Sprintf(
  1293  			"Additional properties are not allowed (%s were unexpected)",
  1294  			strings.Join(additionalProperties, ", "),
  1295  		)
  1296  		return &errorResponse{
  1297  			http.StatusBadRequest,
  1298  			fmt.Sprintf(`{"badRequest": {"message": "%s", "code": 400}}`, message),
  1299  			"application/json; charset=UTF-8",
  1300  			message,
  1301  			nil,
  1302  			nil,
  1303  		}
  1304  	}
  1305  
  1306  	n.nextAttachmentId++
  1307  	attachment.VolumeAttachment.Id = fmt.Sprintf("%d", n.nextAttachmentId)
  1308  	attachment.VolumeAttachment.ServerId = serverId
  1309  
  1310  	serverVols := n.serverIdToAttachedVolumes[serverId]
  1311  	serverVols = append(serverVols, attachment.VolumeAttachment)
  1312  	n.serverIdToAttachedVolumes[serverId] = serverVols
  1313  
  1314  	// Echo the request back with an attachment ID.
  1315  	resp, err := json.Marshal(&attachment)
  1316  	if err != nil {
  1317  		return err
  1318  	}
  1319  	_, err = w.Write(resp)
  1320  	return err
  1321  }
  1322  
  1323  func (n *Nova) handleDetachVolumes(w http.ResponseWriter, r *http.Request) error {
  1324  	attachId := path.Base(r.URL.Path)
  1325  	serverId := path.Base(strings.Replace(r.URL.Path, "/os-volume_attachments/"+attachId, "", 1))
  1326  	serverVols := n.serverIdToAttachedVolumes[serverId]
  1327  
  1328  	for volIdx, vol := range serverVols {
  1329  		if vol.Id == attachId {
  1330  			serverVols = append(serverVols[:volIdx], serverVols[volIdx+1:]...)
  1331  			n.serverIdToAttachedVolumes[serverId] = serverVols
  1332  			writeResponse(w, http.StatusAccepted, nil)
  1333  			return nil
  1334  		}
  1335  	}
  1336  
  1337  	writeResponse(w, http.StatusNotFound, nil)
  1338  	return nil
  1339  }
  1340  
  1341  func (n *Nova) handleListVolumes(w http.ResponseWriter, r *http.Request) error {
  1342  	serverId := path.Base(strings.Replace(r.URL.Path, "/os-volume_attachments", "", 1))
  1343  	serverVols := n.serverIdToAttachedVolumes[serverId]
  1344  
  1345  	resp, err := json.Marshal(struct {
  1346  		VolumeAttachments []nova.VolumeAttachment `json:"volumeAttachments"`
  1347  	}{serverVols})
  1348  	if err != nil {
  1349  		return err
  1350  	}
  1351  
  1352  	_, err = w.Write(resp)
  1353  	return err
  1354  }
  1355  
  1356  // SetupHTTP attaches all the needed handlers to provide the HTTP API.
  1357  //
  1358  // TODO (stickupkid): The following needs re-working for version 3 of goose.
  1359  // Instead we should be providing a router matching library. There is way too
  1360  // much ad-hock calling of methods in different call sites, that makes it almost
  1361  // impossible to follow the flow of this stub.
  1362  //
  1363  // Instead we should define our routes up front as a dependency of our tests.
  1364  //
  1365  // Example of this would be:
  1366  // handlers := map[string]http.Handler{
  1367  // 	"/{version}/{tenant_id}/servers/{server_id}/os-interfaces": n.handleOSInterfaces,
  1368  // }
  1369  func (n *Nova) SetupHTTP(mux *http.ServeMux) {
  1370  	handlers := map[string]http.Handler{
  1371  		"/$v/":                        errBadRequest,
  1372  		"/$v/$t/":                     errNotFound,
  1373  		"/$v/$t/flavors":              n.handler((*Nova).handleFlavors),
  1374  		"/$v/$t/flavors/detail":       n.handler((*Nova).handleFlavorsDetail),
  1375  		"/$v/$t/servers":              n.handler((*Nova).handleServers),
  1376  		"/$v/$t/servers/detail":       n.handler((*Nova).handleServersDetail),
  1377  		"/$v/$t/os-availability-zone": n.handler((*Nova).handleAvailabilityZones),
  1378  	}
  1379  	if !n.useNeutronNetworking {
  1380  		handlers["/$v/$t/os-security-groups"] = n.handler((*Nova).handleSecurityGroups)
  1381  		handlers["/$v/$t/os-security-group-rules"] = n.handler((*Nova).handleSecurityGroupRules)
  1382  		handlers["/$v/$t/os-floating-ips"] = n.handler((*Nova).handleFloatingIPs)
  1383  		handlers["/$v/$t/os-networks"] = n.handler((*Nova).handleNetworks)
  1384  	}
  1385  	for path, h := range handlers {
  1386  		path = strings.Replace(path, "$v", n.VersionPath, 1)
  1387  		path = strings.Replace(path, "$t", n.TenantId, 1)
  1388  		if !strings.HasSuffix(path, "/") {
  1389  			mux.Handle(path+"/", h)
  1390  		}
  1391  		mux.Handle(path, h)
  1392  	}
  1393  }
  1394  
  1395  func (n *Nova) SetupRootHandler(mux *http.ServeMux) {
  1396  	mux.Handle("/", n.handler((*Nova).handleRoot))
  1397  }