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 }