diff --git a/ciao-cli/event.go b/ciao-cli/event.go index b47158625..f2a046fee 100644 --- a/ciao-cli/event.go +++ b/ciao-cli/event.go @@ -22,7 +22,7 @@ import ( "net/http" "os" - "github.com/01org/ciao/payloads" + "github.com/01org/ciao/ciao-controller/types" ) var eventCommand = &command{ @@ -68,7 +68,7 @@ func (cmd *eventListCommand) run(args []string) error { cmd.usage() } - var events payloads.CiaoEvents + var events types.CiaoEvents var url string if cmd.all == true { diff --git a/ciao-cli/instance.go b/ciao-cli/instance.go index 541552d95..77c90484a 100644 --- a/ciao-cli/instance.go +++ b/ciao-cli/instance.go @@ -26,7 +26,8 @@ import ( "os" "text/tabwriter" - "github.com/01org/ciao/payloads" + "github.com/01org/ciao/ciao-controller/types" + "github.com/01org/ciao/openstack/compute" ) const ( @@ -85,11 +86,11 @@ func (cmd *instanceAddCommand) run(args []string) error { cmd.usage() } - var server payloads.ComputeCreateServer - var servers payloads.ComputeServers + var server compute.CreateServerRequest + var servers compute.Servers server.Server.Name = cmd.label - server.Server.Workload = cmd.workload + server.Server.Flavor = cmd.workload server.Server.MaxInstances = cmd.instances server.Server.MinInstances = 1 @@ -318,7 +319,7 @@ func (cmd *instanceListCommand) run(args []string) error { return listNodeInstances(cmd.cn) } - var servers payloads.ComputeServers + var servers compute.Servers var url string if cmd.workload != "" { @@ -417,7 +418,7 @@ func (cmd *instanceShowCommand) run(args []string) error { cmd.usage() } - var server payloads.ComputeServer + var server compute.Server url := buildComputeURL("%s/servers/%s", *tenantID, cmd.instance) resp, err := sendHTTPRequest("GET", url, nil, nil) @@ -433,7 +434,7 @@ func (cmd *instanceShowCommand) run(args []string) error { return nil } -func dumpInstance(server *payloads.Server) { +func dumpInstance(server *compute.ServerDetails) { fmt.Printf("\tUUID: %s\n", server.ID) fmt.Printf("\tStatus: %s\n", server.Status) fmt.Printf("\tPrivate IP: %s\n", server.Addresses.Private[0].Addr) @@ -452,7 +453,7 @@ func listNodeInstances(node string) error { fatalf("Missing required -cn parameter") } - var servers payloads.CiaoServersStats + var servers types.CiaoServersStats url := buildComputeURL("nodes/%s/servers/detail", node) resp, err := sendHTTPRequest("GET", url, nil, nil) @@ -480,7 +481,7 @@ func listNodeInstances(node string) error { } func actionAllTenantInstance(tenant string, osAction string) error { - var action payloads.CiaoServersAction + var action types.CiaoServersAction url := buildComputeURL("%s/servers/action", tenant) diff --git a/ciao-cli/node.go b/ciao-cli/node.go index a2d055a4f..6202509e9 100644 --- a/ciao-cli/node.go +++ b/ciao-cli/node.go @@ -21,7 +21,7 @@ import ( "fmt" "os" - "github.com/01org/ciao/payloads" + "github.com/01org/ciao/ciao-controller/types" ) var nodeCommand = &command{ @@ -70,7 +70,7 @@ func (cmd *nodeListCommand) run(args []string) error { } func listComputeNodes() error { - var nodes payloads.CiaoComputeNodes + var nodes types.CiaoComputeNodes url := buildComputeURL("nodes") @@ -100,7 +100,7 @@ func listComputeNodes() error { } func listCNCINodes() error { - var cncis payloads.CiaoCNCIs + var cncis types.CiaoCNCIs url := buildComputeURL("cncis") @@ -146,7 +146,7 @@ func (cmd *nodeStatusCommand) parseArgs(args []string) []string { } func (cmd *nodeStatusCommand) run(args []string) error { - var status payloads.CiaoClusterStatus + var status types.CiaoClusterStatus url := buildComputeURL("nodes/summary") resp, err := sendHTTPRequest("GET", url, nil, nil) @@ -207,7 +207,7 @@ func showCNCINode(cnciID string) error { fatalf("Missing required -cnci-id parameter") } - var cnci payloads.CiaoCNCI + var cnci types.CiaoCNCI url := buildComputeURL("cncis/%s/detail", cnciID) diff --git a/ciao-cli/tenant.go b/ciao-cli/tenant.go index 730bd79d6..8faea1853 100644 --- a/ciao-cli/tenant.go +++ b/ciao-cli/tenant.go @@ -22,7 +22,7 @@ import ( "os" "time" - "github.com/01org/ciao/payloads" + "github.com/01org/ciao/ciao-controller/types" ) var tenantCommand = &command{ @@ -104,7 +104,7 @@ func listTenantQuotas() error { fatalf("Missing required -tenant-id parameter") } - var resources payloads.CiaoTenantResources + var resources types.CiaoTenantResources url := buildComputeURL("%s/quotas", *tenantID) resp, err := sendHTTPRequest("GET", url, nil, nil) @@ -131,7 +131,7 @@ func listTenantResources() error { fatalf("Missing required -tenant-id parameter") } - var usage payloads.CiaoUsageHistory + var usage types.CiaoUsageHistory url := buildComputeURL("%s/resources", *tenantID) now := time.Now() diff --git a/ciao-cli/trace.go b/ciao-cli/trace.go index c6cd1ff62..5d2cd0cc2 100644 --- a/ciao-cli/trace.go +++ b/ciao-cli/trace.go @@ -22,7 +22,7 @@ import ( "fmt" "os" - "github.com/01org/ciao/payloads" + "github.com/01org/ciao/ciao-controller/types" ) var traceCommand = &command{ @@ -51,7 +51,7 @@ func (cmd *traceListCommand) parseArgs(args []string) []string { } func (cmd *traceListCommand) run(args []string) error { - var traces payloads.CiaoTracesSummary + var traces types.CiaoTracesSummary url := buildComputeURL("traces") @@ -102,7 +102,7 @@ func (cmd *traceShowCommand) run(args []string) error { return errors.New("Missing required -label parameter") } - var traceData payloads.CiaoTraceData + var traceData types.CiaoTraceData url := buildComputeURL("traces/%s", cmd.label) diff --git a/ciao-cli/workload.go b/ciao-cli/workload.go index 689ddb061..fe3d91eed 100644 --- a/ciao-cli/workload.go +++ b/ciao-cli/workload.go @@ -21,7 +21,7 @@ import ( "fmt" "os" - "github.com/01org/ciao/payloads" + "github.com/01org/ciao/openstack/compute" ) var workloadCommand = &command{ @@ -53,7 +53,7 @@ func (cmd *workloadListCommand) run(args []string) error { fatalf("Missing required -tenant-id parameter") } - var flavors payloads.ComputeFlavorsDetails + var flavors compute.FlavorsDetails if *tenantID == "" { *tenantID = "faketenant" } diff --git a/ciao-controller/api.go b/ciao-controller/api.go new file mode 100644 index 000000000..7af811805 --- /dev/null +++ b/ciao-controller/api.go @@ -0,0 +1,705 @@ +// Copyright (c) 2016 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "sort" + "strconv" + "time" + + "github.com/01org/ciao/ciao-controller/types" + "github.com/01org/ciao/payloads" + "github.com/01org/ciao/ssntp" + "github.com/golang/glog" + "github.com/gorilla/mux" +) + +const ciaoAPIPort = 8889 + +// HTTPErrorData represents the HTTP response body for +// a compute API request error. +type HTTPErrorData struct { + Code int `json:"code"` + Name string `json:"name"` + Message string `json:"message"` +} + +// HTTPReturnErrorCode represents the unmarshalled version for Return codes +// when a API call is made and you need to return explicit data of +// the call as OpenStack format +// http://developer.openstack.org/api-guide/compute/faults.html +type HTTPReturnErrorCode struct { + Error HTTPErrorData `json:"error"` +} + +// APIResponse contains the http status and any response struct to be marshalled. +type APIResponse struct { + status int + response interface{} +} + +func errorResponse(err error) APIResponse { + switch err { + case types.ErrQuota: + return APIResponse{http.StatusForbidden, nil} + case types.ErrTenantNotFound, + types.ErrInstanceNotFound: + return APIResponse{http.StatusNotFound, nil} + default: + return APIResponse{http.StatusInternalServerError, nil} + } +} + +// APIHandler is a custom handler for the compute APIs. +// This custom handler allows us to more cleanly return an error and response, +// and pass some package level context into the handler. +type APIHandler struct { + *controller + Handler func(*controller, http.ResponseWriter, *http.Request) (APIResponse, error) + ContentType string +} + +func (h APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + resp, err := h.Handler(h.controller, w, r) + if err != nil { + data := HTTPErrorData{ + Code: resp.status, + Name: http.StatusText(resp.status), + Message: err.Error(), + } + + code := HTTPReturnErrorCode{ + Error: data, + } + + b, err := json.Marshal(code) + if err != nil { + http.Error(w, http.StatusText(resp.status), resp.status) + } + + http.Error(w, string(b), resp.status) + } + + b, err := json.Marshal(resp.response) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), + http.StatusInternalServerError) + } + + w.Header().Set("Content-Type", h.ContentType) + w.WriteHeader(resp.status) + w.Write(b) +} + +type pagerFilterType uint8 + +const ( + none pagerFilterType = 0 + workloadFilter = 0x1 + statusFilter = 0x2 +) + +type pager interface { + filter(filterType pagerFilterType, filter string, item interface{}) bool + nextPage(filterType pagerFilterType, filter string, r *http.Request) ([]byte, error) +} + +func pagerQueryParse(r *http.Request) (int, int, string) { + values := r.URL.Query() + limit := 0 + offset := 0 + marker := "" + if values["limit"] != nil { + l, err := strconv.ParseInt(values["limit"][0], 10, 32) + if err != nil { + limit = 0 + } else { + limit = (int)(l) + } + } + + if values["marker"] != nil { + marker = values["marker"][0] + } else if values["offset"] != nil { + o, err := strconv.ParseInt(values["offset"][0], 10, 32) + if err != nil { + offset = 0 + } else { + offset = (int)(o) + } + } + + return limit, offset, marker +} + +type nodePager struct { + context *controller + nodes []types.CiaoComputeNode +} + +func (pager *nodePager) getNodes(filterType pagerFilterType, filter string, nodes []types.CiaoComputeNode, limit int, offset int) (types.CiaoComputeNodes, error) { + computeNodes := types.NewCiaoComputeNodes() + + pageLength := 0 + + glog.V(2).Infof("Get nodes limit [%d] offset [%d]", limit, offset) + + if nodes == nil || offset >= len(nodes) { + return computeNodes, nil + } + + for _, node := range nodes[offset:] { + computeNodes.Nodes = append(computeNodes.Nodes, node) + + pageLength++ + if limit > 0 && pageLength >= limit { + break + } + } + + return computeNodes, nil +} + +func (pager *nodePager) filter(filterType pagerFilterType, filter string, node types.CiaoComputeNode) bool { + return false +} + +func (pager *nodePager) nextPage(filterType pagerFilterType, filter string, r *http.Request) (types.CiaoComputeNodes, error) { + limit, offset, lastSeen := pagerQueryParse(r) + + if lastSeen == "" { + if limit != 0 { + return pager.getNodes(filterType, filter, pager.nodes, + limit, offset) + } + + return pager.getNodes(filterType, filter, pager.nodes, 0, + offset) + } + + for i, node := range pager.nodes { + if node.ID == lastSeen { + if i >= len(pager.nodes)-1 { + return pager.getNodes(filterType, filter, nil, + limit, 0) + } + + return pager.getNodes(filterType, filter, + pager.nodes[i+1:], limit, 0) + } + } + + return types.CiaoComputeNodes{}, fmt.Errorf("Item %s not found", lastSeen) +} + +type nodeServerPager struct { + context *controller + instances []types.CiaoServerStats +} + +func (pager *nodeServerPager) getNodeServers(filterType pagerFilterType, filter string, instances []types.CiaoServerStats, + limit int, offset int) (types.CiaoServersStats, error) { + servers := types.NewCiaoServersStats() + + servers.TotalServers = len(instances) + pageLength := 0 + + glog.V(2).Infof("Get nodes limit [%d] offset [%d]", limit, offset) + + if instances == nil || offset >= len(instances) { + return servers, nil + } + + for _, instance := range instances[offset:] { + servers.Servers = append(servers.Servers, instance) + + pageLength++ + if limit > 0 && pageLength >= limit { + break + } + } + + return servers, nil +} + +func (pager *nodeServerPager) filter(filterType pagerFilterType, filter string, instance types.CiaoServerStats) bool { + return false +} + +func (pager *nodeServerPager) nextPage(filterType pagerFilterType, filter string, r *http.Request) (types.CiaoServersStats, error) { + limit, offset, lastSeen := pagerQueryParse(r) + + glog.V(2).Infof("Next page marker [%s] limit [%d] offset [%d]", + lastSeen, limit, offset) + + if lastSeen == "" { + if limit != 0 { + return pager.getNodeServers(filterType, filter, + pager.instances, limit, offset) + } + + return pager.getNodeServers(filterType, filter, + pager.instances, 0, offset) + } + + for i, instance := range pager.instances { + if instance.ID == lastSeen { + if i >= len(pager.instances)-1 { + return pager.getNodeServers(filterType, filter, + nil, limit, 0) + } + + return pager.getNodeServers(filterType, filter, + pager.instances[i+1:], limit, 0) + } + } + + return types.CiaoServersStats{}, fmt.Errorf("Item %s not found", lastSeen) +} + +const ( + instances int = 1 + vcpu = 2 + memory = 3 + disk = 4 +) + +func getResources(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + var tenantResource types.CiaoTenantResources + + vars := mux.Vars(r) + tenant := vars["tenant"] + + err := c.confirmTenant(tenant) + if err != nil { + return errorResponse(err), err + } + + t, err := c.ds.GetTenant(tenant) + if err != nil || t == nil { + return errorResponse(types.ErrTenantNotFound), types.ErrTenantNotFound + } + + resources := t.Resources + + tenantResource.ID = t.ID + + for _, resource := range resources { + switch resource.Rtype { + case instances: + tenantResource.InstanceLimit = resource.Limit + tenantResource.InstanceUsage = resource.Usage + + case vcpu: + tenantResource.VCPULimit = resource.Limit + tenantResource.VCPUUsage = resource.Usage + + case memory: + tenantResource.MemLimit = resource.Limit + tenantResource.MemUsage = resource.Usage + + case disk: + tenantResource.DiskLimit = resource.Limit + tenantResource.DiskUsage = resource.Usage + } + } + + return APIResponse{http.StatusOK, tenantResource}, nil +} + +func tenantQueryParse(r *http.Request) (time.Time, time.Time, error) { + values := r.URL.Query() + var startTime, endTime time.Time + + if values["start_date"] == nil || values["end_date"] == nil { + return startTime, endTime, fmt.Errorf("Missing date") + } + + startTime, err := time.Parse(time.RFC3339, values["start_date"][0]) + if err != nil { + return startTime, endTime, err + } + + endTime, err = time.Parse(time.RFC3339, values["end_date"][0]) + if err != nil { + return startTime, endTime, err + } + + return startTime, endTime, nil +} + +func getUsage(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + tenant := vars["tenant"] + var usage types.CiaoUsageHistory + + start, end, err := tenantQueryParse(r) + if err != nil { + return errorResponse(err), err + } + + glog.V(2).Infof("Start %v\n", start) + glog.V(2).Infof("End %v\n", end) + + usage.Usages, err = c.ds.GetTenantUsage(tenant, start, end) + if err != nil { + return errorResponse(err), err + } + + return APIResponse{http.StatusOK, usage}, nil +} + +type instanceAction func(string) error + +func serversAction(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + tenant := vars["tenant"] + var servers types.CiaoServersAction + var actionFunc instanceAction + var statusFilter string + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return errorResponse(err), err + } + + err = json.Unmarshal(body, &servers) + if err != nil { + return errorResponse(err), err + } + + if servers.Action == "os-start" { + actionFunc = c.restartInstance + statusFilter = payloads.Exited + } else if servers.Action == "os-stop" { + actionFunc = c.stopInstance + statusFilter = payloads.Running + } else if servers.Action == "os-delete" { + actionFunc = c.deleteInstance + statusFilter = "" + } else { + return APIResponse{http.StatusServiceUnavailable, nil}, + errors.New("Unsupported action") + } + + if len(servers.ServerIDs) > 0 { + for _, instanceID := range servers.ServerIDs { + // make sure the instance belongs to the tenant + instance, err := c.ds.GetInstance(instanceID) + + if err != nil { + return errorResponse(err), err + } + + if instance.TenantID != tenant { + return errorResponse(err), err + } + actionFunc(instanceID) + } + } else { + /* We want to act on all relevant instances */ + instances, err := c.ds.GetAllInstancesFromTenant(tenant) + if err != nil { + return errorResponse(err), err + } + + for _, instance := range instances { + if statusFilter != "" && + instance.State != statusFilter { + continue + } + + actionFunc(instance.ID) + } + } + + return APIResponse{http.StatusAccepted, nil}, nil +} + +func listTenants(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + var computeTenants types.CiaoComputeTenants + + tenants, err := c.ds.GetAllTenants() + if err != nil { + return errorResponse(err), err + } + + for _, tenant := range tenants { + computeTenants.Tenants = append(computeTenants.Tenants, + struct { + ID string `json:"id"` + Name string `json:"name"` + }{ + ID: tenant.ID, + Name: tenant.Name, + }, + ) + } + + return APIResponse{http.StatusOK, computeTenants}, nil +} + +func listNodes(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + computeNodes := c.ds.GetNodeLastStats() + + nodeSummary, err := c.ds.GetNodeSummary() + if err != nil { + return errorResponse(err), err + } + + for _, node := range nodeSummary { + for i := range computeNodes.Nodes { + if computeNodes.Nodes[i].ID != node.NodeID { + continue + } + + computeNodes.Nodes[i].TotalInstances = + node.TotalInstances + computeNodes.Nodes[i].TotalRunningInstances = + node.TotalRunningInstances + computeNodes.Nodes[i].TotalPendingInstances = + node.TotalPendingInstances + computeNodes.Nodes[i].TotalPausedInstances = + node.TotalPausedInstances + } + } + + sort.Sort(types.SortedComputeNodesByID(computeNodes.Nodes)) + + pager := nodePager{ + context: c, + nodes: computeNodes.Nodes, + } + + resp, err := pager.nextPage(none, "", r) + if err != nil { + return errorResponse(err), err + } + + return APIResponse{http.StatusOK, resp}, nil +} + +func nodesSummary(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + var nodesStatus types.CiaoClusterStatus + + computeNodes := c.ds.GetNodeLastStats() + + glog.V(2).Infof("nodesSummary %d nodes", len(computeNodes.Nodes)) + + nodesStatus.Status.TotalNodes = len(computeNodes.Nodes) + for _, node := range computeNodes.Nodes { + if node.Status == ssntp.READY.String() { + nodesStatus.Status.TotalNodesReady++ + } else if node.Status == ssntp.FULL.String() { + nodesStatus.Status.TotalNodesFull++ + } else if node.Status == ssntp.OFFLINE.String() { + nodesStatus.Status.TotalNodesOffline++ + } else if node.Status == ssntp.MAINTENANCE.String() { + nodesStatus.Status.TotalNodesMaintenance++ + } + } + + return APIResponse{http.StatusOK, nodesStatus}, nil +} + +func listNodeServers(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + nodeID := vars["node"] + + serversStats := c.ds.GetInstanceLastStats(nodeID) + + instances, err := c.ds.GetAllInstancesByNode(nodeID) + if err != nil { + return errorResponse(err), err + } + + for _, instance := range instances { + for i := range serversStats.Servers { + if serversStats.Servers[i].ID != instance.ID { + continue + } + + serversStats.Servers[i].TenantID = instance.TenantID + serversStats.Servers[i].IPv4 = instance.IPAddress + } + } + + pager := nodeServerPager{ + context: c, + instances: serversStats.Servers, + } + + resp, err := pager.nextPage(none, "", r) + if err != nil { + return errorResponse(err), err + } + + return APIResponse{http.StatusOK, resp}, nil +} + +func listCNCIs(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + var ciaoCNCIs types.CiaoCNCIs + + cncis, err := c.ds.GetTenantCNCISummary("") + if err != nil { + return errorResponse(err), err + } + + var subnets []types.CiaoCNCISubnet + + for _, cnci := range cncis { + if cnci.InstanceID == "" { + continue + } + + for _, subnet := range cnci.Subnets { + subnets = append(subnets, + types.CiaoCNCISubnet{ + Subnet: subnet, + }, + ) + } + + ciaoCNCIs.CNCIs = append(ciaoCNCIs.CNCIs, + types.CiaoCNCI{ + ID: cnci.InstanceID, + TenantID: cnci.TenantID, + IPv4: cnci.IPAddress, + Subnets: subnets, + }, + ) + } + + return APIResponse{http.StatusOK, ciaoCNCIs}, nil +} + +func listCNCIDetails(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + cnciID := vars["cnci"] + var ciaoCNCI types.CiaoCNCI + + cncis, err := c.ds.GetTenantCNCISummary(cnciID) + if err != nil { + return errorResponse(err), err + } + + if len(cncis) > 0 { + var subnets []types.CiaoCNCISubnet + cnci := cncis[0] + + for _, subnet := range cnci.Subnets { + subnets = append(subnets, + types.CiaoCNCISubnet{ + Subnet: subnet, + }, + ) + } + + ciaoCNCI = types.CiaoCNCI{ + ID: cnci.InstanceID, + TenantID: cnci.TenantID, + IPv4: cnci.IPAddress, + Subnets: subnets, + } + } + + return APIResponse{http.StatusOK, ciaoCNCI}, err +} + +func listTraces(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + var traces types.CiaoTracesSummary + + summaries, err := c.ds.GetBatchFrameSummary() + if err != nil { + return errorResponse(err), err + } + + for _, s := range summaries { + summary := types.CiaoTraceSummary{ + Label: s.BatchID, + Instances: s.NumInstances, + } + traces.Summaries = append(traces.Summaries, summary) + } + + return APIResponse{http.StatusOK, traces}, err +} + +func listEvents(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + tenant := vars["tenant"] + + events := types.NewCiaoEvents() + + logs, err := c.ds.GetEventLog() + if err != nil { + return errorResponse(err), err + } + + for _, l := range logs { + if tenant != "" && tenant != l.TenantID { + continue + } + + event := types.CiaoEvent{ + Timestamp: l.Timestamp, + TenantID: l.TenantID, + EventType: l.EventType, + Message: l.Message, + } + events.Events = append(events.Events, event) + } + + return APIResponse{http.StatusOK, events}, err +} + +func clearEvents(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + err := c.ds.ClearLog() + if err != nil { + return errorResponse(err), err + } + + return APIResponse{http.StatusAccepted, nil}, nil +} + +func traceData(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + label := vars["label"] + var traceData types.CiaoTraceData + + batchStats, err := c.ds.GetBatchFrameStatistics(label) + if err != nil { + return errorResponse(err), err + } + + traceData.Summary = types.CiaoBatchFrameStat{ + NumInstances: batchStats[0].NumInstances, + TotalElapsed: batchStats[0].TotalElapsed, + AverageElapsed: batchStats[0].AverageElapsed, + AverageControllerElapsed: batchStats[0].AverageControllerElapsed, + AverageLauncherElapsed: batchStats[0].AverageLauncherElapsed, + AverageSchedulerElapsed: batchStats[0].AverageSchedulerElapsed, + VarianceController: batchStats[0].VarianceController, + VarianceLauncher: batchStats[0].VarianceLauncher, + VarianceScheduler: batchStats[0].VarianceScheduler, + } + + return APIResponse{http.StatusOK, traceData}, nil +} diff --git a/ciao-controller/compute.go b/ciao-controller/compute.go deleted file mode 100644 index adf196e3b..000000000 --- a/ciao-controller/compute.go +++ /dev/null @@ -1,1790 +0,0 @@ -/* -// Copyright (c) 2016 Intel Corporation -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -*/ -// @SubApi Servers API [/v2.1/{tenant}/servers] -// @SubApi Flavors API [/v2.1/{tenant}/flavors] -// @SubApi Resources API [/v2.1/{tenant}/resources] -// @SubApi Quotas API [/v2.1/{tenant}/quotas] -// @SubApi Events API [/v2.1/{tenant}/events] -// @SubApi Nodes API [/v2.1/nodes] -// @SubApi Tenants API [/v2.1/tenants] -// @SubApi CNCIs API [/v2.1/cncis] -// @SubApi Traces API [/v2.1/traces] - -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/http/httputil" - "sort" - "strconv" - "strings" - "time" - - "github.com/01org/ciao/ciao-controller/types" - "github.com/01org/ciao/payloads" - "github.com/01org/ciao/ssntp" - "github.com/golang/glog" - "github.com/gorilla/mux" -) - -const openstackComputeAPIPort = 8774 - -type action uint8 - -const ( - computeActionStart action = iota - computeActionStop - computeActionDelete -) - -type pagerFilterType uint8 - -const ( - none pagerFilterType = 0 - workloadFilter = 0x1 - statusFilter = 0x2 -) - -type pager interface { - filter(filterType pagerFilterType, filter string, item interface{}) bool - nextPage(filterType pagerFilterType, filter string, r *http.Request) ([]byte, error) -} - -type serverPager struct { - context *controller - instances []*types.Instance -} - -func dumpRequestBody(r *http.Request, body bool) { - if glog.V(2) { - dump, err := httputil.DumpRequest(r, body) - if err != nil { - glog.Errorf("HTTP request dump error %s", err) - } - - glog.Infof("HTTP request [%q]", dump) - } -} - -func dumpRequest(r *http.Request) { - dumpRequestBody(r, false) -} - -func pagerQueryParse(r *http.Request) (int, int, string) { - values := r.URL.Query() - limit := 0 - offset := 0 - marker := "" - if values["limit"] != nil { - l, err := strconv.ParseInt(values["limit"][0], 10, 32) - if err != nil { - limit = 0 - } else { - limit = (int)(l) - } - } - - if values["marker"] != nil { - marker = values["marker"][0] - } else if values["offset"] != nil { - o, err := strconv.ParseInt(values["offset"][0], 10, 32) - if err != nil { - offset = 0 - } else { - offset = (int)(o) - } - } - - return limit, offset, marker -} - -func (pager *serverPager) getInstances(filterType pagerFilterType, filter string, instances []*types.Instance, limit int, offset int) ([]byte, error) { - servers := payloads.NewComputeServers() - - servers.TotalServers = len(instances) - pageLength := 0 - - glog.V(2).Infof("Get instances limit [%d] offset [%d]", limit, offset) - - if instances == nil || offset >= len(instances) { - b, err := json.Marshal(servers) - if err != nil { - return nil, err - } - - return b, nil - } - - for _, instance := range instances[offset:] { - if filterType != none && pager.filter(filterType, filter, instance) { - continue - } - - server, err := instanceToServer(pager.context, instance) - if err != nil { - return nil, err - } - - servers.Servers = append(servers.Servers, server) - pageLength++ - if limit > 0 && pageLength >= limit { - break - } - - } - - b, err := json.Marshal(servers) - if err != nil { - return nil, err - } - - return b, nil -} - -func (pager *serverPager) filter(filterType pagerFilterType, filter string, instance *types.Instance) bool { - switch filterType { - case workloadFilter: - if instance.WorkloadID != filter { - return true - } - } - - return false -} - -func (pager *serverPager) nextPage(filterType pagerFilterType, filter string, r *http.Request) ([]byte, error) { - limit, offset, lastSeen := pagerQueryParse(r) - - glog.V(2).Infof("Next page marker [%s] limit [%d] offset [%d]", lastSeen, limit, offset) - - if lastSeen == "" { - if limit != 0 { - return pager.getInstances(filterType, filter, pager.instances, limit, offset) - } - - return pager.getInstances(filterType, filter, pager.instances, 0, offset) - } - - for i, instance := range pager.instances { - if instance.ID == lastSeen { - if i >= len(pager.instances)-1 { - return pager.getInstances(filterType, filter, nil, limit, 0) - } - - return pager.getInstances(filterType, filter, pager.instances[i+1:], limit, 0) - } - } - - return nil, fmt.Errorf("Item %s not found", lastSeen) -} - -type nodePager struct { - context *controller - nodes []payloads.CiaoComputeNode -} - -func (pager *nodePager) getNodes(filterType pagerFilterType, filter string, nodes []payloads.CiaoComputeNode, limit int, offset int) ([]byte, error) { - computeNodes := payloads.NewCiaoComputeNodes() - - pageLength := 0 - - glog.V(2).Infof("Get nodes limit [%d] offset [%d]", limit, offset) - - if nodes == nil || offset >= len(nodes) { - b, err := json.Marshal(computeNodes) - if err != nil { - return nil, err - } - - return b, nil - } - - for _, node := range nodes[offset:] { - computeNodes.Nodes = append(computeNodes.Nodes, node) - - pageLength++ - if limit > 0 && pageLength >= limit { - break - } - } - - b, err := json.Marshal(computeNodes) - if err != nil { - return nil, err - } - - return b, nil -} - -func (pager *nodePager) filter(filterType pagerFilterType, filter string, node payloads.CiaoComputeNode) bool { - return false -} - -func (pager *nodePager) nextPage(filterType pagerFilterType, filter string, r *http.Request) ([]byte, error) { - limit, offset, lastSeen := pagerQueryParse(r) - - glog.V(2).Infof("Next page marker [%s] limit [%d] offset [%d]", lastSeen, limit, offset) - - if lastSeen == "" { - if limit != 0 { - return pager.getNodes(filterType, filter, pager.nodes, limit, offset) - } - - return pager.getNodes(filterType, filter, pager.nodes, 0, offset) - } - - for i, node := range pager.nodes { - if node.ID == lastSeen { - if i >= len(pager.nodes)-1 { - return pager.getNodes(filterType, filter, nil, limit, 0) - } - - return pager.getNodes(filterType, filter, pager.nodes[i+1:], limit, 0) - } - } - - return nil, fmt.Errorf("Item %s not found", lastSeen) -} - -type nodeServerPager struct { - context *controller - instances []payloads.CiaoServerStats -} - -func (pager *nodeServerPager) getNodeServers(filterType pagerFilterType, filter string, instances []payloads.CiaoServerStats, - limit int, offset int) ([]byte, error) { - servers := payloads.NewCiaoServersStats() - - servers.TotalServers = len(instances) - pageLength := 0 - - glog.V(2).Infof("Get nodes limit [%d] offset [%d]", limit, offset) - - if instances == nil || offset >= len(instances) { - b, err := json.Marshal(servers) - if err != nil { - return nil, err - } - - return b, nil - } - - for _, instance := range instances[offset:] { - servers.Servers = append(servers.Servers, instance) - - pageLength++ - if limit > 0 && pageLength >= limit { - break - } - } - - b, err := json.Marshal(servers) - if err != nil { - return nil, err - } - - return b, nil -} - -func (pager *nodeServerPager) filter(filterType pagerFilterType, filter string, instance payloads.CiaoServerStats) bool { - return false -} - -func (pager *nodeServerPager) nextPage(filterType pagerFilterType, filter string, r *http.Request) ([]byte, error) { - limit, offset, lastSeen := pagerQueryParse(r) - - glog.V(2).Infof("Next page marker [%s] limit [%d] offset [%d]", lastSeen, limit, offset) - - if lastSeen == "" { - if limit != 0 { - return pager.getNodeServers(filterType, filter, pager.instances, limit, offset) - } - - return pager.getNodeServers(filterType, filter, pager.instances, 0, offset) - } - - for i, instance := range pager.instances { - if instance.ID == lastSeen { - if i >= len(pager.instances)-1 { - return pager.getNodeServers(filterType, filter, nil, limit, 0) - } - - return pager.getNodeServers(filterType, filter, pager.instances[i+1:], limit, 0) - } - } - - return nil, fmt.Errorf("Item %s not found", lastSeen) -} - -func tenantToken(context *controller, r *http.Request, tenant string) bool { - var validServices = []struct { - serviceType string - serviceName string - }{ - { - serviceType: "compute", - serviceName: "ciao", - }, - { - serviceType: "compute", - serviceName: "nova", - }, - } - token := r.Header["X-Auth-Token"] - if token == nil { - return false - } - - /* TODO Caching or PKI */ - for _, s := range validServices { - if context.id.validateService(token[0], tenant, s.serviceType, s.serviceName) == true { - return true - } - - } - - for _, s := range validServices { - if context.id.validateService(token[0], tenant, s.serviceType, "") == true { - return true - } - - } - - return false -} - -func adminToken(context *controller, r *http.Request) bool { - var validAdmins = []struct { - project string - role string - }{ - { - project: "service", - role: "admin", - }, - { - project: "admin", - role: "admin", - }, - } - - token := r.Header["X-Auth-Token"] - if token == nil { - return false - } - - /* TODO Caching or PKI */ - for _, a := range validAdmins { - if context.id.validateProjectRole(token[0], a.project, a.role) == true { - return true - } - } - - vars := mux.Vars(r) - tenant := vars["tenant"] - glog.V(2).Infof("Invalid token for [%s]", tenant) - return false -} - -func validateToken(context *controller, r *http.Request) bool { - vars := mux.Vars(r) - tenant := vars["tenant"] - - glog.V(2).Infof("Token validation for [%s]", tenant) - - // We do not want to unconditionally check for an admin token, this is inefficient. - // We check for an admin token iff: - // - We do not have a tenant variable - // - We do have one but it does not match the token - - /* If we don't have a tenant parameter, are we admin ? */ - if tenant == "" { - return adminToken(context, r) - } - - /* If we have a tenant parameter that does not match the token are we admin ? */ - if tenantToken(context, r, tenant) == false { - return adminToken(context, r) - } - - return true -} - -func instanceToServer(context *controller, instance *types.Instance) (payloads.Server, error) { - workload, err := context.ds.GetWorkload(instance.WorkloadID) - if err != nil { - return payloads.Server{}, err - } - - imageID := workload.ImageID - - server := payloads.Server{ - HostID: instance.NodeID, - ID: instance.ID, - TenantID: instance.TenantID, - Flavor: payloads.Flavor{ - ID: instance.WorkloadID, - }, - Image: payloads.Image{ - ID: imageID, - }, - Status: instance.State, - Addresses: payloads.Addresses{ - Private: []payloads.PrivateAddresses{ - { - Addr: instance.IPAddress, - OSEXTIPSMACMacAddr: instance.MACAddress, - }, - }, - }, - SSHIP: instance.SSHIP, - SSHPort: instance.SSHPort, - } - - return server, nil -} - -// returnErrorCode returns error codes for the http call -func returnErrorCode(w http.ResponseWriter, httpError int, messageFormat string, messageArgs ...interface{}) { - var returnCode payloads.HTTPReturnErrorCode - returnCode.Error.Code = httpError - returnCode.Error.Name = http.StatusText(returnCode.Error.Code) - - returnCode.Error.Message = fmt.Sprintf(messageFormat, messageArgs...) - - b, err := json.Marshal(returnCode) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - http.Error(w, string(b), httpError) -} - -// @Title showServerDetails -// @Description Shows details for a server. -// @Accept json -// @Success 200 {object} payloads.ComputeServer "Returns details for a server." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/{tenant}/servers/{server} [get] -// @Resource /v2.1/{tenant}/servers -func showServerDetails(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - tenant := vars["tenant"] - instanceID := vars["server"] - var server payloads.ComputeServer - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - instance, err := context.ds.GetInstance(instanceID) - if err != nil { - returnErrorCode(w, http.StatusNotFound, "Instance could not be found") - return - } - - if instance.TenantID != tenant { - returnErrorCode(w, http.StatusNotFound, "Instance does not belong to tenant") - return - } - - server.Server, err = instanceToServer(context, instance) - if err != nil { - returnErrorCode(w, http.StatusNotFound, "Instance could not be found") - return - } - - b, err := json.Marshal(server) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title deleteServer -// @Description Deletes a server. -// @Accept json -// @Success 202 {object} string "This operation does not return a response body, returns the 202 StatusAccepted code." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/{tenant}/servers/{server} [delete] -// @Resource /v2.1/{tenant}/servers -func deleteServer(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - tenant := vars["tenant"] - instance := vars["server"] - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - /* First check that the instance belongs to this tenant */ - i, err := context.ds.GetInstance(instance) - if err != nil { - returnErrorCode(w, http.StatusNotFound, "Instance could not be found") - return - } - - if i.TenantID != tenant { - returnErrorCode(w, http.StatusNotFound, "Instance does not belong to tenant") - return - } - - err = context.deleteInstance(instance) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.WriteHeader(http.StatusAccepted) -} - -func buildFlavorDetails(workload *types.Workload) (payloads.FlavorDetails, error) { - var details payloads.FlavorDetails - - defaults := workload.Defaults - if len(defaults) == 0 { - return details, fmt.Errorf("Workload resources not set") - } - - details.OsFlavorAccessIsPublic = true - details.ID = workload.ID - details.Disk = workload.ImageID - details.Name = workload.Description - - for r := range defaults { - switch defaults[r].Type { - case payloads.VCPUs: - details.Vcpus = defaults[r].Value - case payloads.MemMB: - details.RAM = defaults[r].Value - } - } - - return details, nil -} - -// @Title listFlavors -// @Description Lists flavors. -// @Accept json -// @Success 200 {array} interface "Returns payloads.NewComputeFlavors() with the corresponding available flavors for the tenant." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/{tenant}/flavors [get] -// @Resource /v2.1/{tenant}/flavors -func listFlavors(w http.ResponseWriter, r *http.Request, context *controller) { - flavors := payloads.NewComputeFlavors() - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - workloads, err := context.ds.GetWorkloads() - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - for _, workload := range workloads { - flavors.Flavors = append(flavors.Flavors, - struct { - ID string `json:"id"` - Links []payloads.Link `json:"links"` - Name string `json:"name"` - }{ - ID: workload.ID, - Name: workload.Description, - }, - ) - } - - b, err := json.Marshal(flavors) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title listFlavorsDetails -// @Description Lists flavors with details. -// @Accept json -// @Success 200 {array} interface "Returns payloads.NewComputeFlavorsDetails() of flavor details." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/{tenant}/flavors/detail [get] -// @Resource /v2.1/{tenant}/flavors -func listFlavorsDetails(w http.ResponseWriter, r *http.Request, context *controller) { - var details payloads.FlavorDetails - flavors := payloads.NewComputeFlavorsDetails() - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - workloads, err := context.ds.GetWorkloads() - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - for _, workload := range workloads { - details, err = buildFlavorDetails(workload) - if err != nil { - continue - } - - flavors.Flavors = append(flavors.Flavors, details) - } - - b, err := json.Marshal(flavors) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title showFlavorDetails -// @Description Shows details for a flavor. -// @Accept json -// @Success 200 {object} payloads.ComputeFlavorDetails "Returns details for a flavor." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/{tenant}/flavors/{flavor} [get] -// @Resource /v2.1/{tenant}/flavors -func showFlavorDetails(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - workloadID := vars["flavor"] - var flavor payloads.ComputeFlavorDetails - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - workload, err := context.ds.GetWorkload(workloadID) - if err != nil { - returnErrorCode(w, http.StatusNotFound, "Workload not found") - return - } - - details, err := buildFlavorDetails(workload) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - flavor.Flavor = details - - b, err := json.Marshal(flavor) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title listFlavorServerDetail -// @Description Lists all servers with details. -// @Accept json -// @Success 200 {array} types.Instance "Returns a list of all servers." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/flavors/{flavor}/servers/detail [get] -// @Resource /v2.1/{tenant}/flavors -// listFlavorServerDetail is created with the only purpose of API documentation for method -// /v2.1/flavors/{flavor}/servers/detail [get] -func listFlavorServerDetail(w http.ResponseWriter, r *http.Request, context *controller) { - listServerDetails(w, r, context) -} - -const ( - instances int = 1 - vcpu = 2 - memory = 3 - disk = 4 -) - -// @Title listTenantQuotas -// @Description List the use of all resources used of a tenant from a start to end point of time. -// @Accept json -// @Success 200 {object} payloads.CiaoTenantResources "Returns the limits and usage of resources of a tenant." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/{tenant}/quotas [get] -// @Resource /v2.1/{tenant}/quotas -func listTenantQuotas(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - tenant := vars["tenant"] - var tenantResource payloads.CiaoTenantResources - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - t, err := context.ds.GetTenant(tenant) - if err != nil { - returnErrorCode(w, http.StatusNotFound, "Tenant could not be found") - return - } - - if t == nil { - if *noNetwork { - _, err := context.ds.AddTenant(tenant) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - } else { - err = context.addTenant(tenant) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - } - - t, err = context.ds.GetTenant(tenant) - if err != nil { - returnErrorCode(w, http.StatusNotFound, "Tenant could not be found") - return - } - } - - resources := t.Resources - - tenantResource.ID = t.ID - - for _, resource := range resources { - switch resource.Rtype { - case instances: - tenantResource.InstanceLimit = resource.Limit - tenantResource.InstanceUsage = resource.Usage - - case vcpu: - tenantResource.VCPULimit = resource.Limit - tenantResource.VCPUUsage = resource.Usage - - case memory: - tenantResource.MemLimit = resource.Limit - tenantResource.MemUsage = resource.Usage - - case disk: - tenantResource.DiskLimit = resource.Limit - tenantResource.DiskUsage = resource.Usage - } - } - - b, err := json.Marshal(tenantResource) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -func tenantQueryParse(r *http.Request) (time.Time, time.Time, error) { - values := r.URL.Query() - var startTime, endTime time.Time - - if values["start_date"] == nil || values["end_date"] == nil { - return startTime, endTime, fmt.Errorf("Missing date") - } - - startTime, err := time.Parse(time.RFC3339, values["start_date"][0]) - if err != nil { - return startTime, endTime, err - } - - endTime, err = time.Parse(time.RFC3339, values["end_date"][0]) - if err != nil { - return startTime, endTime, err - } - - return startTime, endTime, nil -} - -// @Title listTenantResources -// @Description List the use of all resources used of a tenant from a start to end point of time. -// @Accept json -// @Success 200 {object} payloads.CiaoUsageHistory "Returns the usage of resouces." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/{tenant}/resources [get] -// @Resource /v2.1/{tenant}/resources -func listTenantResources(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - tenant := vars["tenant"] - var usage payloads.CiaoUsageHistory - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - start, end, err := tenantQueryParse(r) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - glog.V(2).Infof("Start %v\n", start) - glog.V(2).Infof("End %v\n", end) - - usage.Usages, err = context.ds.GetTenantUsage(tenant, start, end) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - b, err := json.Marshal(usage) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title listServerDetails -// @Description Lists all servers with details. -// @Accept json -// @Success 200 {array} types.Instance "Returns details of all servers." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/{tenant}/servers/detail [get] -// @Resource /v2.1/{tenant}/servers -func listServerDetails(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - tenant := vars["tenant"] - workload := vars["flavor"] - var instances []*types.Instance - var err error - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - if tenant != "" { - instances, err = context.ds.GetAllInstancesFromTenant(tenant) - } else { - instances, err = context.ds.GetAllInstances() - } - - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - sort.Sort(types.SortedInstancesByID(instances)) - - pager := serverPager{ - context: context, - instances: instances, - } - - filterType := none - filter := "" - if workload != "" { - filterType = workloadFilter - filter = workload - } - - b, err := pager.nextPage(filterType, filter, r) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title createServer -// @Description Creates a server. -// @Accept json -// @Success 202 {object} payloads.Server "Returns payloads.ComputeCreateServer and payloads.ComputeServer with data of the created server." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/{tenant}/servers [post] -// @Resource /v2.1/{tenant}/servers -func createServer(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - tenant := vars["tenant"] - var server payloads.ComputeCreateServer - var servers payloads.ComputeServers - - dumpRequestBody(r, true) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - defer r.Body.Close() - - body, err := ioutil.ReadAll(r.Body) - if err != nil { - returnErrorCode(w, http.StatusBadRequest, "Service cannot read Request Body") - return - } - - err = json.Unmarshal(body, &server) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - nInstances := 1 - - if server.Server.MaxInstances > 0 { - nInstances = server.Server.MaxInstances - } else if server.Server.MinInstances > 0 { - nInstances = server.Server.MinInstances - } - - trace := false - label := "" - if server.Server.Name != "" { - trace = true - label = server.Server.Name - } - instances, err := context.startWorkload(server.Server.Workload, tenant, nInstances, trace, label) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - for _, instance := range instances { - server, err := instanceToServer(context, instance) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - servers.Servers = append(servers.Servers, server) - } - servers.TotalServers = len(instances) - - // set machine ID for OpenStack compatibility - server.Server.ID = instances[0].ID - - // builtServers is define to meet OpenStack compatibility on result format and keep CIAOs - builtServers := struct { - payloads.ComputeCreateServer - payloads.ComputeServers - }{ - payloads.ComputeCreateServer{ - Server: server.Server, - }, - payloads.ComputeServers{ - TotalServers: servers.TotalServers, - Servers: servers.Servers, - }, - } - - b, err := json.Marshal(builtServers) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusAccepted) - w.Write(b) -} - -type instanceAction func(string) error - -// @Title tenantServersAction -// @Description Runs the indicated action (os-start, os-stop, os-delete) in the servers. -// @Accept json -// @Success 202 {object} string "This operation does not return a response body, returns the 202 StatusAccepted code." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/{tenant}/servers/action [post] -// @Resource /v2.1/{tenant}/servers -// tenantServersAction will apply the operation sent in POST (as os-start, os-stop, os-delete) -// to all servers of a tenant or if ServersID size is greater than zero it will be applied -// only to the subset provided that also belongs to the tenant -func tenantServersAction(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - tenant := vars["tenant"] - var servers payloads.CiaoServersAction - var actionFunc instanceAction - var statusFilter string - - dumpRequestBody(r, true) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - defer r.Body.Close() - - body, err := ioutil.ReadAll(r.Body) - if err != nil { - returnErrorCode(w, http.StatusBadRequest, "Service cannot read Request Body") - return - } - - err = json.Unmarshal(body, &servers) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - if servers.Action == "os-start" { - actionFunc = context.restartInstance - statusFilter = payloads.ComputeStatusStopped - } else if servers.Action == "os-stop" { - actionFunc = context.stopInstance - statusFilter = payloads.ComputeStatusRunning - } else if servers.Action == "os-delete" { - actionFunc = context.deleteInstance - statusFilter = "" - } else { - returnErrorCode(w, http.StatusServiceUnavailable, "Unsupported action") - return - } - - if len(servers.ServerIDs) > 0 { - for _, instanceID := range servers.ServerIDs { - // make sure the instance belongs to the tenant - instance, err := context.ds.GetInstance(instanceID) - - if err != nil { - returnErrorCode(w, http.StatusNotFound, "Instance %s could not be found", instanceID) - return - } - - if instance.TenantID != tenant { - returnErrorCode(w, http.StatusNotFound, "Instance %s does not belong to tenant %s", instanceID, tenant) - return - } - actionFunc(instanceID) - } - } else { - /* We want to act on all relevant instances */ - instances, err := context.ds.GetAllInstancesFromTenant(tenant) - if err != nil { - returnErrorCode(w, http.StatusNotFound, "No instances for tenant") - return - } - - for _, instance := range instances { - if statusFilter != "" && instance.State != statusFilter { - continue - } - - actionFunc(instance.ID) - } - } - - w.WriteHeader(http.StatusAccepted) -} - -// @Title serverAction -// @Description Runs the indicated action (os-start, os-stop, os-delete) in the a server. -// @Accept json -// @Success 202 {object} string "This operation does not return a response body, returns the 202 StatusAccepted code." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/{tenant}/servers/{server}/action [post] -// @Resource /v2.1/{tenant}/servers -func serverAction(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - tenant := vars["tenant"] - instance := vars["server"] - var action action - - dumpRequestBody(r, true) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - /* First check that the instance belongs to this tenant */ - i, err := context.ds.GetInstance(instance) - if err != nil { - returnErrorCode(w, http.StatusNotFound, "Instance could not be found") - return - } - - if i.TenantID != tenant { - returnErrorCode(w, http.StatusNotFound, "Instance does not belong to tenant") - return - } - - defer r.Body.Close() - - body, err := ioutil.ReadAll(r.Body) - if err != nil { - returnErrorCode(w, http.StatusBadRequest, "Service cannot read Request Body") - return - } - - bodyString := string(body) - - if strings.Contains(bodyString, "os-start") { - action = computeActionStart - } else if strings.Contains(bodyString, "os-stop") { - action = computeActionStop - } else { - returnErrorCode(w, http.StatusServiceUnavailable, "Unsupported action") - return - } - - switch action { - case computeActionStart: - err = context.restartInstance(instance) - case computeActionStop: - err = context.stopInstance(instance) - } - - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.WriteHeader(http.StatusAccepted) -} - -// @Title listTenants -// @Description List all tenants. -// @Accept json -// @Success 200 {array} interface "Marshalled format of payloads.CiaoComputeTenants representing the list of all tentants." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/tenants [get] -// @Resource /v2.1/tenants -func listTenants(w http.ResponseWriter, r *http.Request, context *controller) { - var computeTenants payloads.CiaoComputeTenants - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - tenants, err := context.ds.GetAllTenants() - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - for _, tenant := range tenants { - computeTenants.Tenants = append(computeTenants.Tenants, - struct { - ID string `json:"id"` - Name string `json:"name"` - }{ - ID: tenant.ID, - Name: tenant.Name, - }, - ) - } - - b, err := json.Marshal(computeTenants) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title listNodes -// @Description Returns a list of all nodes. -// @Accept json -// @Success 200 {array} interface "Returns ciao-controller.nodePager with TotalInstances, TotalRunningInstances, TotalPendingInstances, TotalPausedInstances." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/nodes [get] -// @Resource /v2.1/nodes -func listNodes(w http.ResponseWriter, r *http.Request, context *controller) { - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - computeNodes := context.ds.GetNodeLastStats() - - nodeSummary, err := context.ds.GetNodeSummary() - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - for _, node := range nodeSummary { - for i := range computeNodes.Nodes { - if computeNodes.Nodes[i].ID != node.NodeID { - continue - } - - computeNodes.Nodes[i].TotalInstances = node.TotalInstances - computeNodes.Nodes[i].TotalRunningInstances = node.TotalRunningInstances - computeNodes.Nodes[i].TotalPendingInstances = node.TotalPendingInstances - computeNodes.Nodes[i].TotalPausedInstances = node.TotalPausedInstances - } - } - - sort.Sort(types.SortedComputeNodesByID(computeNodes.Nodes)) - - pager := nodePager{ - context: context, - nodes: computeNodes.Nodes, - } - - b, err := pager.nextPage(none, "", r) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title nodesSummary -// @Description A summary of all node stats. -// @Accept json -// @Success 200 {object} interface "Returns payloads.CiaoClusterStatus with TotalNodesReady, TotalNodesFull, TotalNodesOffline and TotalNodesMaintenance." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/nodes/summary [get] -// @Resource /v2.1/nodes -func nodesSummary(w http.ResponseWriter, r *http.Request, context *controller) { - var nodesStatus payloads.CiaoClusterStatus - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - computeNodes := context.ds.GetNodeLastStats() - - glog.V(2).Infof("nodesSummary %d nodes", len(computeNodes.Nodes)) - - nodesStatus.Status.TotalNodes = len(computeNodes.Nodes) - for _, node := range computeNodes.Nodes { - if node.Status == ssntp.READY.String() { - nodesStatus.Status.TotalNodesReady++ - } else if node.Status == ssntp.FULL.String() { - nodesStatus.Status.TotalNodesFull++ - } else if node.Status == ssntp.OFFLINE.String() { - nodesStatus.Status.TotalNodesOffline++ - } else if node.Status == ssntp.MAINTENANCE.String() { - nodesStatus.Status.TotalNodesMaintenance++ - } - } - - b, err := json.Marshal(nodesStatus) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title serverAction -// @Description Runs the indicated action (os-start, os-stop, os-delete) in a server. -// @Accept json -// @Success 202 {object} string "This operation does not return a response body, returns the 202 StatusAccepted code." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/nodes/{node}/servers/detail [get] -// @Resource /v2.1/nodes -func listNodeServers(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - nodeID := vars["node"] - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - serversStats := context.ds.GetInstanceLastStats(nodeID) - - instances, err := context.ds.GetAllInstancesByNode(nodeID) - if err != nil { - returnErrorCode(w, http.StatusNotFound, "Instances could not be found in node") - return - } - - for _, instance := range instances { - for i := range serversStats.Servers { - if serversStats.Servers[i].ID != instance.ID { - continue - } - - serversStats.Servers[i].TenantID = instance.TenantID - serversStats.Servers[i].IPv4 = instance.IPAddress - } - } - - pager := nodeServerPager{ - context: context, - instances: serversStats.Servers, - } - - b, err := pager.nextPage(none, "", r) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title listCNCIs -// @Description Lists all CNCI agents. -// @Accept json -// @Success 200 {array} payloads.CiaoCNCIs "Returns all CNCI agents data as InstanceId, TenantID, IPv4 and subnets." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/cncis [get] -// @Resource /v2.1/cncis -func listCNCIs(w http.ResponseWriter, r *http.Request, context *controller) { - var ciaoCNCIs payloads.CiaoCNCIs - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - cncis, err := context.ds.GetTenantCNCISummary("") - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - var subnets []payloads.CiaoCNCISubnet - - for _, cnci := range cncis { - if cnci.InstanceID == "" { - continue - } - - for _, subnet := range cnci.Subnets { - subnets = append(subnets, - payloads.CiaoCNCISubnet{ - Subnet: subnet, - }, - ) - } - - ciaoCNCIs.CNCIs = append(ciaoCNCIs.CNCIs, - payloads.CiaoCNCI{ - ID: cnci.InstanceID, - TenantID: cnci.TenantID, - IPv4: cnci.IPAddress, - Subnets: subnets, - }, - ) - } - - b, err := json.Marshal(ciaoCNCIs) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title listCNCIDetails -// @Description List details of a CNCI agent. -// @Accept json -// @Success 200 {array} payloads.CiaoCNCIs "Returns details of a CNCI agent as InstanceId, TenantID, IPv4 and subnets." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/cncis/{cnci}/detail [get] -// @Resource /v2.1/cncis -func listCNCIDetails(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - cnciID := vars["cnci"] - var ciaoCNCI payloads.CiaoCNCI - - dumpRequest(r) - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - cncis, err := context.ds.GetTenantCNCISummary(cnciID) - if err != nil { - returnErrorCode(w, http.StatusNotFound, "CNCI could not be found") - return - } - - if len(cncis) > 0 { - var subnets []payloads.CiaoCNCISubnet - cnci := cncis[0] - - for _, subnet := range cnci.Subnets { - subnets = append(subnets, - payloads.CiaoCNCISubnet{ - Subnet: subnet, - }, - ) - } - - ciaoCNCI = payloads.CiaoCNCI{ - ID: cnci.InstanceID, - TenantID: cnci.TenantID, - IPv4: cnci.IPAddress, - Subnets: subnets, - } - } - - b, err := json.Marshal(ciaoCNCI) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title listTraces -// @Description List all Traces. -// @Accept json -// @Success 200 {array} payloads.CiaoTracesSummary "Returns a summary of each trace in the system." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/traces [get] -// @Resource /v2.1/traces -func listTraces(w http.ResponseWriter, r *http.Request, context *controller) { - var traces payloads.CiaoTracesSummary - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - summaries, err := context.ds.GetBatchFrameSummary() - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - for _, s := range summaries { - summary := payloads.CiaoTraceSummary{ - Label: s.BatchID, - Instances: s.NumInstances, - } - traces.Summaries = append(traces.Summaries, summary) - } - - b, err := json.Marshal(traces) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title listEvents -// @Description List all Events. -// @Accept json -// @Success 200 {array} payloads.CiaoEvent "Returns all events from the log system." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/events [get] -// @Resource /v2.1/events -func listEvents(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - tenant := vars["tenant"] - - events := payloads.NewCiaoEvents() - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - logs, err := context.ds.GetEventLog() - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - for _, l := range logs { - if tenant != "" && tenant != l.TenantID { - continue - } - - event := payloads.CiaoEvent{ - Timestamp: l.Timestamp, - TenantID: l.TenantID, - EventType: l.EventType, - Message: l.Message, - } - events.Events = append(events.Events, event) - } - - b, err := json.Marshal(events) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -// @Title listTenantEvents -// @Description List Events. -// @Accept json -// @Success 200 {array} payloads.CiaoEvent "Returns the events of a tenant from the log system." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/{tenant}/events [get] -// @Resource /v2.1/events -// listTenantEvents is created with the only purpose of API documentation for method -// /v2.1/{tenant}/events -func listTenantEvents(w http.ResponseWriter, r *http.Request, context *controller) { - listEvents(w, r, context) -} - -// @Title clearEvents -// @Description Clear Events Log. -// @Accept json -// @Success 202 {object} string "This operation does not return a response body, returns the 202 StatusAccepted code." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/events [delete] -// @Resource /v2.1/events -func clearEvents(w http.ResponseWriter, r *http.Request, context *controller) { - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - err := context.ds.ClearLog() - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.WriteHeader(http.StatusAccepted) -} - -// @Title traceData -// @Description Trace data of a indicated trace. -// @Accept json -// @Success 200 {array} payloads.CiaoBatchFrameStat "Returns a summary of a trace in the system." -// @Failure 400 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." -// @Failure 500 {object} payloads.HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." -// @Router /v2.1/traces/{label} [get] -// @Resource /v2.1/traces -func traceData(w http.ResponseWriter, r *http.Request, context *controller) { - vars := mux.Vars(r) - label := vars["label"] - var traceData payloads.CiaoTraceData - - if validateToken(context, r) == false { - returnErrorCode(w, http.StatusUnauthorized, "Invalid token") - return - } - - batchStats, err := context.ds.GetBatchFrameStatistics(label) - if err != nil { - returnErrorCode(w, http.StatusNotFound, "Could not found trace with label") - return - } - - traceData.Summary = payloads.CiaoBatchFrameStat{ - NumInstances: batchStats[0].NumInstances, - TotalElapsed: batchStats[0].TotalElapsed, - AverageElapsed: batchStats[0].AverageElapsed, - AverageControllerElapsed: batchStats[0].AverageControllerElapsed, - AverageLauncherElapsed: batchStats[0].AverageLauncherElapsed, - AverageSchedulerElapsed: batchStats[0].AverageSchedulerElapsed, - VarianceController: batchStats[0].VarianceController, - VarianceLauncher: batchStats[0].VarianceLauncher, - VarianceScheduler: batchStats[0].VarianceScheduler, - } - - b, err := json.Marshal(traceData) - if err != nil { - returnErrorCode(w, http.StatusInternalServerError, err.Error()) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(b) -} - -func createComputeAPI(context *controller) { - r := mux.NewRouter() - - r.HandleFunc("/v2.1/{tenant}/servers", func(w http.ResponseWriter, r *http.Request) { - createServer(w, r, context) - }).Methods("POST") - - r.HandleFunc("/v2.1/{tenant}/servers/detail", func(w http.ResponseWriter, r *http.Request) { - listServerDetails(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/{tenant}/servers/{server}", func(w http.ResponseWriter, r *http.Request) { - showServerDetails(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/{tenant}/servers/{server}", func(w http.ResponseWriter, r *http.Request) { - deleteServer(w, r, context) - }).Methods("DELETE") - - r.HandleFunc("/v2.1/{tenant}/servers/action", func(w http.ResponseWriter, r *http.Request) { - tenantServersAction(w, r, context) - }).Methods("POST") - - r.HandleFunc("/v2.1/{tenant}/servers/{server}/action", func(w http.ResponseWriter, r *http.Request) { - serverAction(w, r, context) - }).Methods("POST") - - r.HandleFunc("/v2.1/{tenant}/flavors", func(w http.ResponseWriter, r *http.Request) { - listFlavors(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/{tenant}/flavors/detail", func(w http.ResponseWriter, r *http.Request) { - listFlavorsDetails(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/{tenant}/flavors/{flavor}", func(w http.ResponseWriter, r *http.Request) { - showFlavorDetails(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/{tenant}/resources", func(w http.ResponseWriter, r *http.Request) { - listTenantResources(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/{tenant}/quotas", func(w http.ResponseWriter, r *http.Request) { - listTenantQuotas(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/{tenant}/events", func(w http.ResponseWriter, r *http.Request) { - listTenantEvents(w, r, context) - }).Methods("GET") - - /* Avoid conflict with {tenant}/servers/detail */ - r.HandleFunc("/v2.1/nodes/{node}/servers/detail", func(w http.ResponseWriter, r *http.Request) { - listNodeServers(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/flavors/{flavor}/servers/detail", func(w http.ResponseWriter, r *http.Request) { - listFlavorServerDetail(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/tenants", func(w http.ResponseWriter, r *http.Request) { - listTenants(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/nodes", func(w http.ResponseWriter, r *http.Request) { - listNodes(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/nodes/summary", func(w http.ResponseWriter, r *http.Request) { - nodesSummary(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/cncis", func(w http.ResponseWriter, r *http.Request) { - listCNCIs(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/cncis/{cnci}/detail", func(w http.ResponseWriter, r *http.Request) { - listCNCIDetails(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/events", func(w http.ResponseWriter, r *http.Request) { - listEvents(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/events", func(w http.ResponseWriter, r *http.Request) { - clearEvents(w, r, context) - }).Methods("DELETE") - - r.HandleFunc("/v2.1/traces", func(w http.ResponseWriter, r *http.Request) { - listTraces(w, r, context) - }).Methods("GET") - - r.HandleFunc("/v2.1/traces/{label}", func(w http.ResponseWriter, r *http.Request) { - traceData(w, r, context) - }).Methods("GET") - - service := fmt.Sprintf(":%d", computeAPIPort) - log.Fatal(http.ListenAndServeTLS(service, httpsCAcert, httpsKey, r)) -} diff --git a/ciao-controller/compute_test.go b/ciao-controller/compute_test.go index b3b61cd1c..48dccae5d 100644 --- a/ciao-controller/compute_test.go +++ b/ciao-controller/compute_test.go @@ -26,6 +26,7 @@ import ( "time" "github.com/01org/ciao/ciao-controller/types" + "github.com/01org/ciao/openstack/compute" "github.com/01org/ciao/payloads" "github.com/01org/ciao/ssntp" "github.com/01org/ciao/testutil" @@ -70,7 +71,7 @@ func testHTTPRequest(t *testing.T, method string, URL string, expectedResponse i return body } -func testCreateServer(t *testing.T, n int) payloads.ComputeServers { +func testCreateServer(t *testing.T, n int) compute.Servers { tenant, err := context.ds.GetTenant(testutil.ComputeUser) if err != nil { t.Fatal(err) @@ -88,9 +89,9 @@ func testCreateServer(t *testing.T, n int) payloads.ComputeServers { url := testutil.ComputeURL + "/v2.1/" + tenant.ID + "/servers" - var server payloads.ComputeCreateServer + var server compute.CreateServerRequest server.Server.MaxInstances = n - server.Server.Workload = wls[0].ID + server.Server.Flavor = wls[0].ID b, err := json.Marshal(server) if err != nil { @@ -99,7 +100,7 @@ func testCreateServer(t *testing.T, n int) payloads.ComputeServers { body := testHTTPRequest(t, "POST", url, http.StatusAccepted, b, true) - servers := payloads.NewComputeServers() + servers := compute.NewServers() err = json.Unmarshal(body, &servers) if err != nil { @@ -113,12 +114,12 @@ func testCreateServer(t *testing.T, n int) payloads.ComputeServers { return servers } -func testListServerDetailsTenant(t *testing.T, tenantID string) payloads.ComputeServers { +func testListServerDetailsTenant(t *testing.T, tenantID string) compute.Servers { url := testutil.ComputeURL + "/v2.1/" + tenantID + "/servers/detail" body := testHTTPRequest(t, "GET", url, http.StatusOK, nil, true) - s := payloads.NewComputeServers() + s := compute.NewServers() err := json.Unmarshal(body, &s) if err != nil { t.Fatal(err) @@ -149,9 +150,9 @@ func TestCreateSingleServerInvalidToken(t *testing.T) { url := testutil.ComputeURL + "/v2.1/" + tenant.ID + "/servers" - var server payloads.ComputeCreateServer + var server compute.CreateServerRequest server.Server.MaxInstances = 1 - server.Server.Workload = wls[0].ID + server.Server.Flavor = wls[0].ID b, err := json.Marshal(server) if err != nil { @@ -214,7 +215,7 @@ func testListServerDetailsWorkload(t *testing.T, httpExpectedStatus int, validTo body := testHTTPRequest(t, "GET", url, httpExpectedStatus, nil, validToken) - var s payloads.ComputeServers + var s compute.Servers err = json.Unmarshal(body, &s) if err != nil { t.Fatal(err) @@ -257,7 +258,7 @@ func testShowServerDetails(t *testing.T, httpExpectedStatus int, validToken bool return } - var s2 payloads.ComputeServer + var s2 compute.Server err = json.Unmarshal(body, &s2) if err != nil { t.Fatal(err) @@ -321,7 +322,7 @@ func testDeleteServer(t *testing.T, httpExpectedStatus int, httpExpectedErrorSta } func TestDeleteServer(t *testing.T) { - testDeleteServer(t, http.StatusAccepted, http.StatusInternalServerError, true) + testDeleteServer(t, http.StatusNoContent, http.StatusInternalServerError, true) } func TestDeleteServerInvalidToken(t *testing.T) { @@ -367,7 +368,7 @@ func testServersActionStart(t *testing.T, httpExpectedStatus int, validToken boo var ids []string ids = append(ids, servers.Servers[0].ID) - cmd := payloads.CiaoServersAction{ + cmd := types.CiaoServersAction{ Action: "os-start", ServerIDs: ids, } @@ -416,7 +417,7 @@ func testServersActionStop(t *testing.T, httpExpectedStatus int, action string) var ids []string ids = append(ids, servers.Servers[0].ID) - cmd := payloads.CiaoServersAction{ + cmd := types.CiaoServersAction{ Action: action, ServerIDs: ids, } @@ -540,7 +541,7 @@ func testListFlavors(t *testing.T, httpExpectedStatus int, data []byte, validTok return } - var flavors payloads.ComputeFlavors + var flavors compute.Flavors err = json.Unmarshal(body, &flavors) if err != nil { t.Fatal(err) @@ -587,7 +588,7 @@ func testShowFlavorDetails(t *testing.T, httpExpectedStatus int, validToken bool } for _, w := range wls { - details := payloads.FlavorDetails{ + details := compute.FlavorDetails{ OsFlavorAccessIsPublic: true, ID: w.ID, Disk: w.ImageID, @@ -611,7 +612,7 @@ func testShowFlavorDetails(t *testing.T, httpExpectedStatus int, validToken bool return } - var f payloads.ComputeFlavorDetails + var f compute.Flavor err = json.Unmarshal(body, &f) if err != nil { @@ -651,7 +652,7 @@ func TestListFlavorsDetailsInvalidToken(t *testing.T) { } func testListTenantResources(t *testing.T, httpExpectedStatus int, validToken bool) { - var usage payloads.CiaoUsageHistory + var usage types.CiaoUsageHistory endTime := time.Now() startTime := endTime.Add(-15 * time.Minute) @@ -680,7 +681,7 @@ func testListTenantResources(t *testing.T, httpExpectedStatus int, validToken bo return } - var result payloads.CiaoUsageHistory + var result types.CiaoUsageHistory err = json.Unmarshal(body, &result) if err != nil { @@ -708,7 +709,7 @@ func testListTenantQuotas(t *testing.T, httpExpectedStatus int, validToken bool) url := testutil.ComputeURL + "/v2.1/" + tenant.ID + "/quotas" - var expected payloads.CiaoTenantResources + var expected types.CiaoTenantResources for _, resource := range tenant.Resources { switch resource.Rtype { @@ -738,7 +739,7 @@ func testListTenantQuotas(t *testing.T, httpExpectedStatus int, validToken bool) return } - var result payloads.CiaoTenantResources + var result types.CiaoTenantResources err = json.Unmarshal(body, &result) if err != nil { @@ -768,7 +769,7 @@ func testListEventsTenant(t *testing.T, httpExpectedStatus int, validToken bool) url := testutil.ComputeURL + "/v2.1/" + tenant.ID + "/events" - expected := payloads.NewCiaoEvents() + expected := types.NewCiaoEvents() logs, err := context.ds.GetEventLog() if err != nil { @@ -780,7 +781,7 @@ func testListEventsTenant(t *testing.T, httpExpectedStatus int, validToken bool) continue } - event := payloads.CiaoEvent{ + event := types.CiaoEvent{ Timestamp: l.Timestamp, TenantID: l.TenantID, EventType: l.EventType, @@ -791,7 +792,7 @@ func testListEventsTenant(t *testing.T, httpExpectedStatus int, validToken bool) body := testHTTPRequest(t, "GET", url, httpExpectedStatus, nil, validToken) - var result payloads.CiaoEvents + var result types.CiaoEvents err = json.Unmarshal(body, &result) if err != nil { @@ -824,7 +825,7 @@ func testListNodeServers(t *testing.T, httpExpectedStatus int, validToken bool) return } - var result payloads.CiaoServersStats + var result types.CiaoServersStats err = json.Unmarshal(body, &result) if err != nil { @@ -855,7 +856,7 @@ func testListTenants(t *testing.T, httpExpectedStatus int, validToken bool) { t.Fatal(err) } - expected := payloads.NewCiaoComputeTenants() + expected := types.NewCiaoComputeTenants() for _, tenant := range tenants { expected.Tenants = append(expected.Tenants, @@ -877,7 +878,7 @@ func testListTenants(t *testing.T, httpExpectedStatus int, validToken bool) { return } - var result payloads.CiaoComputeTenants + var result types.CiaoComputeTenants err = json.Unmarshal(body, &result) if err != nil { @@ -929,7 +930,7 @@ func testListNodes(t *testing.T, httpExpectedStatus int, validToken bool) { return } - var result payloads.CiaoComputeNodes + var result types.CiaoComputeNodes err = json.Unmarshal(body, &result) if err != nil { @@ -954,7 +955,7 @@ func TestListNodesInvalidToken(t *testing.T) { } func testNodeSummary(t *testing.T, httpExpectedStatus int, validToken bool) { - var expected payloads.CiaoClusterStatus + var expected types.CiaoClusterStatus computeNodes := context.ds.GetNodeLastStats() @@ -979,7 +980,7 @@ func testNodeSummary(t *testing.T, httpExpectedStatus int, validToken bool) { return } - var result payloads.CiaoClusterStatus + var result types.CiaoClusterStatus err := json.Unmarshal(body, &result) if err != nil { @@ -1000,14 +1001,14 @@ func TestNodeSummaryInvalidToken(t *testing.T) { } func testListCNCIs(t *testing.T, httpExpectedStatus int, validToken bool) { - var expected payloads.CiaoCNCIs + var expected types.CiaoCNCIs cncis, err := context.ds.GetTenantCNCISummary("") if err != nil { t.Fatal(err) } - var subnets []payloads.CiaoCNCISubnet + var subnets []types.CiaoCNCISubnet for _, cnci := range cncis { if cnci.InstanceID == "" { @@ -1016,14 +1017,14 @@ func testListCNCIs(t *testing.T, httpExpectedStatus int, validToken bool) { for _, subnet := range cnci.Subnets { subnets = append(subnets, - payloads.CiaoCNCISubnet{ + types.CiaoCNCISubnet{ Subnet: subnet, }, ) } expected.CNCIs = append(expected.CNCIs, - payloads.CiaoCNCI{ + types.CiaoCNCI{ ID: cnci.InstanceID, TenantID: cnci.TenantID, IPv4: cnci.IPAddress, @@ -1040,7 +1041,7 @@ func testListCNCIs(t *testing.T, httpExpectedStatus int, validToken bool) { return } - var result payloads.CiaoCNCIs + var result types.CiaoCNCIs err = json.Unmarshal(body, &result) if err != nil { @@ -1067,7 +1068,7 @@ func testListCNCIDetails(t *testing.T, httpExpectedStatus int, validToken bool) } for _, cnci := range cncis { - var expected payloads.CiaoCNCI + var expected types.CiaoCNCI cncis, err := context.ds.GetTenantCNCISummary(cnci.InstanceID) if err != nil { @@ -1075,18 +1076,18 @@ func testListCNCIDetails(t *testing.T, httpExpectedStatus int, validToken bool) } if len(cncis) > 0 { - var subnets []payloads.CiaoCNCISubnet + var subnets []types.CiaoCNCISubnet cnci := cncis[0] for _, subnet := range cnci.Subnets { subnets = append(subnets, - payloads.CiaoCNCISubnet{ + types.CiaoCNCISubnet{ Subnet: subnet, }, ) } - expected = payloads.CiaoCNCI{ + expected = types.CiaoCNCI{ ID: cnci.InstanceID, TenantID: cnci.TenantID, IPv4: cnci.IPAddress, @@ -1102,7 +1103,7 @@ func testListCNCIDetails(t *testing.T, httpExpectedStatus int, validToken bool) return } - var result payloads.CiaoCNCI + var result types.CiaoCNCI err = json.Unmarshal(body, &result) if err != nil { @@ -1124,7 +1125,7 @@ func TestListCNCIDetailsInvalidToken(t *testing.T) { } func testListTraces(t *testing.T, httpExpectedStatus int, validToken bool) { - var expected payloads.CiaoTracesSummary + var expected types.CiaoTracesSummary client := testStartTracedWorkload(t) defer client.Shutdown() @@ -1139,7 +1140,7 @@ func testListTraces(t *testing.T, httpExpectedStatus int, validToken bool) { } for _, s := range summaries { - summary := payloads.CiaoTraceSummary{ + summary := types.CiaoTraceSummary{ Label: s.BatchID, Instances: s.NumInstances, } @@ -1154,7 +1155,7 @@ func testListTraces(t *testing.T, httpExpectedStatus int, validToken bool) { return } - var result payloads.CiaoTracesSummary + var result types.CiaoTracesSummary err = json.Unmarshal(body, &result) if err != nil { @@ -1177,7 +1178,7 @@ func TestListTracesInvalidToken(t *testing.T) { func testListEvents(t *testing.T, httpExpectedStatus int, validToken bool) { url := testutil.ComputeURL + "/v2.1/events" - expected := payloads.NewCiaoEvents() + expected := types.NewCiaoEvents() logs, err := context.ds.GetEventLog() if err != nil { @@ -1185,7 +1186,7 @@ func testListEvents(t *testing.T, httpExpectedStatus int, validToken bool) { } for _, l := range logs { - event := payloads.CiaoEvent{ + event := types.CiaoEvent{ Timestamp: l.Timestamp, TenantID: l.TenantID, EventType: l.EventType, @@ -1200,7 +1201,7 @@ func testListEvents(t *testing.T, httpExpectedStatus int, validToken bool) { return } - var result payloads.CiaoEvents + var result types.CiaoEvents err = json.Unmarshal(body, &result) if err != nil { @@ -1261,14 +1262,14 @@ func testTraceData(t *testing.T, httpExpectedStatus int, validToken bool) { } for _, s := range summaries { - var expected payloads.CiaoTraceData + var expected types.CiaoTraceData batchStats, err := context.ds.GetBatchFrameStatistics(s.BatchID) if err != nil { t.Fatal(err) } - expected.Summary = payloads.CiaoBatchFrameStat{ + expected.Summary = types.CiaoBatchFrameStat{ NumInstances: batchStats[0].NumInstances, TotalElapsed: batchStats[0].TotalElapsed, AverageElapsed: batchStats[0].AverageElapsed, @@ -1288,7 +1289,7 @@ func testTraceData(t *testing.T, httpExpectedStatus int, validToken bool) { return } - var result payloads.CiaoTraceData + var result types.CiaoTraceData err = json.Unmarshal(body, &result) if err != nil { diff --git a/ciao-controller/controller_test.go b/ciao-controller/controller_test.go index a3d40457d..439b05dea 100644 --- a/ciao-controller/controller_test.go +++ b/ciao-controller/controller_test.go @@ -1327,7 +1327,7 @@ func TestMain(m *testing.M) { } _, _ = addComputeTestTenant() - go createComputeAPI(context) + go context.startComputeService() time.Sleep(1 * time.Second) diff --git a/ciao-controller/identity.go b/ciao-controller/identity.go index f88295fc5..5cb89f5ca 100644 --- a/ciao-controller/identity.go +++ b/ciao-controller/identity.go @@ -19,11 +19,8 @@ package main import ( "errors" - "github.com/golang/glog" - "github.com/mitchellh/mapstructure" "github.com/rackspace/gophercloud" "github.com/rackspace/gophercloud/openstack" - v3tokens "github.com/rackspace/gophercloud/openstack/identity/v3/tokens" ) type identity struct { @@ -36,187 +33,6 @@ type identityConfig struct { servicePassword string } -// Project holds project information extracted from the keystone response. -type Project struct { - ID string `mapstructure:"id"` - Name string `mapstructure:"name"` -} - -// RoleEntry contains the name of a role extracted from the keystone response. -type RoleEntry struct { - Name string `mapstructure:"name"` -} - -// Roles contains a list of role names extracted from the keystone response. -type Roles struct { - Entries []RoleEntry -} - -// Endpoint contains endpoint information extracted from the keystone response. -type Endpoint struct { - ID string `mapstructure:"id"` - Region string `mapstructure:"region"` - Interface string `mapstructure:"interface"` - URL string `mapstructure:"url"` -} - -// ServiceEntry contains information about a service extracted from the keystone response. -type ServiceEntry struct { - ID string `mapstructure:"id"` - Name string `mapstructure:"name"` - Type string `mapstructure:"type"` - Endpoints []Endpoint `mapstructure:"endpoints"` -} - -// Services is a list of ServiceEntry structs -// These structs contain information about the services keystone knows about. -type Services struct { - Entries []ServiceEntry -} - -type getResult struct { - v3tokens.GetResult -} - -// extractProject -// Ideally we would actually contribute this functionality -// back to the gophercloud project, but for now we extend -// their object to allow us to get project information out -// of the response from the GET token validation request. -func (r getResult) extractProject() (*Project, error) { - if r.Err != nil { - glog.V(2).Info(r.Err) - return nil, r.Err - } - - // can there be more than one project? You need to test. - var response struct { - Token struct { - ValidProject Project `mapstructure:"project"` - } `mapstructure:"token"` - } - - err := mapstructure.Decode(r.Body, &response) - if err != nil { - glog.V(2).Info(err) - return nil, err - } - - return &Project{ - ID: response.Token.ValidProject.ID, - Name: response.Token.ValidProject.Name, - }, nil -} - -func (r getResult) extractServices() (*Services, error) { - if r.Err != nil { - glog.V(2).Info(r.Err) - return nil, r.Err - } - - var response struct { - Token struct { - Entries []ServiceEntry `mapstructure:"catalog"` - } `mapstructure:"token"` - } - - err := mapstructure.Decode(r.Body, &response) - if err != nil { - glog.Errorf(err.Error()) - return nil, err - } - - return &Services{Entries: response.Token.Entries}, nil -} - -// extractRole -// Ideally we would actually contribute this functionality -// back to the gophercloud project, but for now we extend -// their object to allow us to get project information out -// of the response from the GET token validation request. -func (r getResult) extractRoles() (*Roles, error) { - if r.Err != nil { - glog.V(2).Info(r.Err) - return nil, r.Err - } - - var response struct { - Token struct { - ValidRoles []RoleEntry `mapstructure:"roles"` - } `mapstructure:"token"` - } - - err := mapstructure.Decode(r.Body, &response) - if err != nil { - glog.V(2).Info(err) - return nil, err - } - - return &Roles{Entries: response.Token.ValidRoles}, nil -} - -// validateServices -// Validates that a given user belonging to a tenant -// can access a service specified by its type and name. -func (i *identity) validateService(token string, tenantID string, serviceType string, serviceName string) bool { - r := v3tokens.Get(i.scV3, token) - result := getResult{r} - - p, err := result.extractProject() - if err != nil { - return false - } - - if p.ID != tenantID { - glog.Errorf("expected %s got %s\n", tenantID, p.ID) - return false - } - - services, err := result.extractServices() - if err != nil { - return false - } - - for _, e := range services.Entries { - if e.Type == serviceType { - if serviceName == "" { - return true - } - - if e.Name == serviceName { - return true - } - } - } - - return false -} - -func (i *identity) validateProjectRole(token string, project string, role string) bool { - r := v3tokens.Get(i.scV3, token) - result := getResult{r} - p, err := result.extractProject() - if err != nil { - return false - } - - if project != "" && p.Name != project { - return false - } - - roles, err := result.extractRoles() - if err != nil { - return false - } - - for i := range roles.Entries { - if roles.Entries[i].Name == role { - return true - } - } - return false -} - func newIdentityClient(config identityConfig) (*identity, error) { opt := gophercloud.AuthOptions{ IdentityEndpoint: config.endpoint + "/v3/", diff --git a/ciao-controller/internal/datastore/datastore.go b/ciao-controller/internal/datastore/datastore.go index c683afac4..a9c39501a 100644 --- a/ciao-controller/internal/datastore/datastore.go +++ b/ciao-controller/internal/datastore/datastore.go @@ -135,10 +135,10 @@ type Datastore struct { cnciAddedChans map[string]chan bool cnciAddedLock *sync.Mutex - nodeLastStat map[string]payloads.CiaoComputeNode + nodeLastStat map[string]types.CiaoComputeNode nodeLastStatLock *sync.RWMutex - instanceLastStat map[string]payloads.CiaoServerStats + instanceLastStat map[string]types.CiaoServerStats instanceLastStatLock *sync.RWMutex tenants map[string]*tenant @@ -155,7 +155,7 @@ type Datastore struct { instances map[string]*types.Instance instancesLock *sync.RWMutex - tenantUsage map[string][]payloads.CiaoUsage + tenantUsage map[string][]types.CiaoUsage tenantUsageLock *sync.RWMutex blockDevices map[string]types.BlockData @@ -184,10 +184,10 @@ func (ds *Datastore) Init(config Config) error { ds.cnciAddedChans = make(map[string]chan bool) ds.cnciAddedLock = &sync.Mutex{} - ds.nodeLastStat = make(map[string]payloads.CiaoComputeNode) + ds.nodeLastStat = make(map[string]types.CiaoComputeNode) ds.nodeLastStatLock = &sync.RWMutex{} - ds.instanceLastStat = make(map[string]payloads.CiaoServerStats) + ds.instanceLastStat = make(map[string]types.CiaoServerStats) ds.instanceLastStatLock = &sync.RWMutex{} // warning, do not use the tenant cache to get @@ -255,7 +255,7 @@ func (ds *Datastore) Init(config Config) error { ds.nodes[i.NodeID].instances[key] = i } - ds.tenantUsage = make(map[string][]payloads.CiaoUsage) + ds.tenantUsage = make(map[string][]types.CiaoUsage) ds.tenantUsageLock = &sync.RWMutex{} ds.blockDevices, err = ds.db.getAllBlockData() @@ -742,7 +742,7 @@ func (ds *Datastore) GetInstance(id string) (*types.Instance, error) { ds.instancesLock.RUnlock() if !ok { - return nil, errors.New("Instance Not Found") + return nil, types.ErrInstanceNotFound } return value, nil @@ -796,7 +796,7 @@ func (ds *Datastore) AddInstance(instance *types.Instance) error { ds.instances[instance.ID] = instance - instanceStat := payloads.CiaoServerStats{ + instanceStat := types.CiaoServerStats{ ID: instance.ID, TenantID: instance.TenantID, NodeID: instance.NodeID, @@ -1096,8 +1096,8 @@ func (ds *Datastore) HandleTraceReport(trace payloads.Trace) error { // GetInstanceLastStats retrieves the last instances stats received for this node. // It returns it in a format suitable for the compute API. -func (ds *Datastore) GetInstanceLastStats(nodeID string) payloads.CiaoServersStats { - var serversStats payloads.CiaoServersStats +func (ds *Datastore) GetInstanceLastStats(nodeID string) types.CiaoServersStats { + var serversStats types.CiaoServersStats ds.instanceLastStatLock.RLock() for _, instance := range ds.instanceLastStat { @@ -1113,8 +1113,8 @@ func (ds *Datastore) GetInstanceLastStats(nodeID string) payloads.CiaoServersSta // GetNodeLastStats retrieves the last nodes stats received for this node. // It returns it in a format suitable for the compute API. -func (ds *Datastore) GetNodeLastStats() payloads.CiaoComputeNodes { - var computeNodes payloads.CiaoComputeNodes +func (ds *Datastore) GetNodeLastStats() types.CiaoComputeNodes { + var computeNodes types.CiaoComputeNodes ds.nodeLastStatLock.RLock() for _, node := range ds.nodeLastStat { @@ -1140,7 +1140,7 @@ func (ds *Datastore) addNodeStat(stat payloads.Stat) error { ds.nodesLock.Unlock() - cnStat := payloads.CiaoComputeNode{ + cnStat := types.CiaoComputeNode{ ID: stat.NodeUUID, Status: stat.Status, Load: stat.Load, @@ -1163,7 +1163,7 @@ func (ds *Datastore) addNodeStat(stat payloads.Stat) error { var tenantUsagePeriodMinutes float64 = 5 -func (ds *Datastore) updateTenantUsageNeeded(delta payloads.CiaoUsage, tenantID string) bool { +func (ds *Datastore) updateTenantUsageNeeded(delta types.CiaoUsage, tenantID string) bool { if delta.VCPU == 0 && delta.Memory == 0 && delta.Disk == 0 { @@ -1173,13 +1173,13 @@ func (ds *Datastore) updateTenantUsageNeeded(delta payloads.CiaoUsage, tenantID return true } -func (ds *Datastore) updateTenantUsage(delta payloads.CiaoUsage, tenantID string) { +func (ds *Datastore) updateTenantUsage(delta types.CiaoUsage, tenantID string) { if ds.updateTenantUsageNeeded(delta, tenantID) == false { return } createNewUsage := true - lastUsage := payloads.CiaoUsage{} + lastUsage := types.CiaoUsage{} ds.tenantUsageLock.Lock() @@ -1192,7 +1192,7 @@ func (ds *Datastore) updateTenantUsage(delta payloads.CiaoUsage, tenantID string } } - newUsage := payloads.CiaoUsage{ + newUsage := types.CiaoUsage{ VCPU: lastUsage.VCPU + delta.VCPU, Memory: lastUsage.Memory + delta.Memory, Disk: lastUsage.Disk + delta.Disk, @@ -1213,7 +1213,7 @@ func (ds *Datastore) updateTenantUsage(delta payloads.CiaoUsage, tenantID string // GetTenantUsage provides statistics on actual resource usage. // Usage is provided between a specified time period. -func (ds *Datastore) GetTenantUsage(tenantID string, start time.Time, end time.Time) ([]payloads.CiaoUsage, error) { +func (ds *Datastore) GetTenantUsage(tenantID string, start time.Time, end time.Time) ([]types.CiaoUsage, error) { ds.tenantUsageLock.RLock() defer ds.tenantUsageLock.RUnlock() @@ -1255,7 +1255,7 @@ func (ds *Datastore) addInstanceStats(stats []payloads.InstanceStat, nodeID stri for index := range stats { stat := stats[index] - instanceStat := payloads.CiaoServerStats{ + instanceStat := types.CiaoServerStats{ ID: stat.InstanceUUID, NodeID: nodeID, Timestamp: time.Now(), @@ -1269,7 +1269,7 @@ func (ds *Datastore) addInstanceStats(stats []payloads.InstanceStat, nodeID stri lastInstanceStat := ds.instanceLastStat[stat.InstanceUUID] - deltaUsage := payloads.CiaoUsage{ + deltaUsage := types.CiaoUsage{ VCPU: instanceStat.VCPUUsage - lastInstanceStat.VCPUUsage, Memory: instanceStat.MemUsage - lastInstanceStat.MemUsage, Disk: instanceStat.DiskUsage - lastInstanceStat.DiskUsage, diff --git a/ciao-controller/legacy_api.go b/ciao-controller/legacy_api.go new file mode 100644 index 000000000..79a667c07 --- /dev/null +++ b/ciao-controller/legacy_api.go @@ -0,0 +1,312 @@ +/* +// Copyright (c) 2016 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ +// @SubApi Servers API [/v2.1/{tenant}/servers] +// @SubApi Resources API [/v2.1/{tenant}/resources] +// @SubApi Quotas API [/v2.1/{tenant}/quotas] +// @SubApi Events API [/v2.1/{tenant}/events] +// @SubApi Nodes API [/v2.1/nodes] +// @SubApi Tenants API [/v2.1/tenants] +// @SubApi CNCIs API [/v2.1/cncis] +// @SubApi Traces API [/v2.1/traces] + +package main + +import ( + "encoding/json" + "net/http" + + "github.com/01org/ciao/openstack/compute" + "github.com/gorilla/mux" +) + +// APIHandler is a custom handler for the compute APIs. +// This custom handler allows us to more cleanly return an error and response, +// and pass some package level context into the handler. +type legacyAPIHandler struct { + *controller + Handler func(*controller, http.ResponseWriter, *http.Request) (APIResponse, error) +} + +func (h legacyAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + resp, err := h.Handler(h.controller, w, r) + if err != nil { + data := HTTPErrorData{ + Code: resp.status, + Name: http.StatusText(resp.status), + Message: err.Error(), + } + + code := HTTPReturnErrorCode{ + Error: data, + } + + b, err := json.Marshal(code) + if err != nil { + http.Error(w, http.StatusText(resp.status), resp.status) + } + + http.Error(w, string(b), resp.status) + } + + b, err := json.Marshal(resp.response) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), + http.StatusInternalServerError) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resp.status) + w.Write(b) +} + +// @Title listTenantQuotas +// @Description List the use of all resources used of a tenant from a start to end point of time. +// @Accept json +// @Success 200 {object} types.CiaoTenantResources "Returns the limits and usage of resources of a tenant." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/{tenant}/quotas [get] +// @Resource /v2.1/{tenant}/quotas +func listTenantQuotas(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return getResources(c, w, r) +} + +// @Title listTenantResources +// @Description List the use of all resources used of a tenant from a start to end point of time. +// @Accept json +// @Success 200 {object} types.CiaoUsageHistory "Returns the usage of resouces." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/{tenant}/resources [get] +// @Resource /v2.1/{tenant}/resources +func listTenantResources(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return getUsage(c, w, r) +} + +// @Title tenantServersAction +// @Description Runs the indicated action (os-start, os-stop, os-delete) in the servers. +// @Accept json +// @Success 202 {object} string "This operation does not return a response body, returns the 202 StatusAccepted code." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/{tenant}/servers/action [post] +// @Resource /v2.1/{tenant}/servers +// tenantServersAction will apply the operation sent in POST (as os-start, os-stop, os-delete) +// to all servers of a tenant or if ServersID size is greater than zero it will be applied +// only to the subset provided that also belongs to the tenant +func tenantServersAction(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return serversAction(c, w, r) +} + +// @Title legacyListTenants +// @Description List all tenants. +// @Accept json +// @Success 200 {array} interface "Marshalled format of types.CiaoComputeTenants representing the list of all tentants." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/tenants [get] +// @Resource /v2.1/tenants +func legacyListTenants(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return listTenants(c, w, r) +} + +// @Title legacyListNodes +// @Description Returns a list of all nodes. +// @Accept json +// @Success 200 {array} interface "Returns ciao-controller.nodePager with TotalInstances, TotalRunningInstances, TotalPendingInstances, TotalPausedInstances." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/nodes [get] +// @Resource /v2.1/nodes +func legacyListNodes(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return listNodes(c, w, r) +} + +// @Title legacyNodesSummary +// @Description A summary of all node stats. +// @Accept json +// @Success 200 {object} interface "Returns types.CiaoClusterStatus with TotalNodesReady, TotalNodesFull, TotalNodesOffline and TotalNodesMaintenance." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/nodes/summary [get] +// @Resource /v2.1/nodes +func legacyNodesSummary(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return nodesSummary(c, w, r) +} + +// @Title legacyListNodeServers +// @Description A list of servers by node id. +// @Accept json +// @Success 200 {object} interface "Returns types.CiaoServersStats" +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/nodes/{node}/servers/detail [get] +// @Resource /v2.1/nodes +func legacyListNodeServers(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return listNodeServers(c, w, r) +} + +// @Title legacyListCNCIs +// @Description Lists all CNCI agents. +// @Accept json +// @Success 200 {array} types.CiaoCNCIs "Returns all CNCI agents data as InstanceId, TenantID, IPv4 and subnets." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/cncis [get] +// @Resource /v2.1/cncis +func legacyListCNCIs(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return listCNCIs(c, w, r) +} + +// @Title legacyListCNCIDetails +// @Description List details of a CNCI agent. +// @Accept json +// @Success 200 {array} types.CiaoCNCIs "Returns details of a CNCI agent as InstanceId, TenantID, IPv4 and subnets." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/cncis/{cnci}/detail [get] +// @Resource /v2.1/cncis +func legacyListCNCIDetails(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return listCNCIDetails(c, w, r) +} + +// @Title legacyListTraces +// @Description List all Traces. +// @Accept json +// @Success 200 {array} types.CiaoTracesSummary "Returns a summary of each trace in the system." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/traces [get] +// @Resource /v2.1/traces +func legacyListTraces(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return listTraces(c, w, r) +} + +// @Title legacyListEvents +// @Description List all Events. +// @Accept json +// @Success 200 {array} types.CiaoEvent "Returns all events from the log system." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/events [get] +// @Resource /v2.1/events +func legacyListEvents(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return listEvents(c, w, r) +} + +// @Title legacyListTenantEvents +// @Description List Events. +// @Accept json +// @Success 200 {array} types.CiaoEvent "Returns the events of a tenant from the log system." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/{tenant}/events [get] +// @Resource /v2.1/events +// listTenantEvents is created with the only purpose of API documentation for method +// /v2.1/{tenant}/events +func legacyListTenantEvents(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return listEvents(c, w, r) +} + +// @Title legacyClearEvents +// @Description Clear Events Log. +// @Accept json +// @Success 202 {object} string "This operation does not return a response body, returns the 202 StatusAccepted code." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/events [delete] +// @Resource /v2.1/events +func legacyClearEvents(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return clearEvents(c, w, r) +} + +// @Title legacyTraceData +// @Description Trace data of a indicated trace. +// @Accept json +// @Success 200 {array} types.CiaoBatchFrameStat "Returns a summary of a trace in the system." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/traces/{label} [get] +// @Resource /v2.1/traces +func legacyTraceData(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + return traceData(c, w, r) +} + +// @Title listServerDetailsFlavors +// @Description Lists all servers with details for a particular flavor. +// @Accept json +// @Success 200 {array} compute.ServerDetails "Returns a list of all servers." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/flavors/{flavor}/servers/detail [get] +// @Resource /v2.1/flavors/{flavor}/servers +func listServerDetailsFlavors(c *controller, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + cxt := &compute.Context{ + Service: c, + } + + computeResp, err := compute.ListServersDetails(cxt, w, r) + + resp := APIResponse{ + status: computeResp.Status, + response: computeResp.Response, + } + + return resp, err +} + +func legacyComputeRoutes(context *controller, r *mux.Router) *mux.Router { + r.Handle("/v2.1/{tenant}/servers/action", + legacyAPIHandler{context, tenantServersAction}).Methods("POST") + + r.Handle("/v2.1/flavors/{flavor}/servers/detail", + legacyAPIHandler{context, listServerDetailsFlavors}).Methods("GET") + + r.Handle("/v2.1/{tenant}/resources", + legacyAPIHandler{context, listTenantResources}).Methods("GET") + + r.Handle("/v2.1/{tenant}/quotas", + legacyAPIHandler{context, listTenantQuotas}).Methods("GET") + + r.Handle("/v2.1/tenants", + legacyAPIHandler{context, legacyListTenants}).Methods("GET") + + r.Handle("/v2.1/nodes", + legacyAPIHandler{context, legacyListNodes}).Methods("GET") + r.Handle("/v2.1/nodes/summary", + legacyAPIHandler{context, legacyNodesSummary}).Methods("GET") + r.Handle("/v2.1/nodes/{node}/servers/detail", + legacyAPIHandler{context, legacyListNodeServers}).Methods("GET") + + r.Handle("/v2.1/cncis", + legacyAPIHandler{context, legacyListCNCIs}).Methods("GET") + r.Handle("/v2.1/cncis/{cnci}/detail", + legacyAPIHandler{context, legacyListCNCIDetails}).Methods("GET") + + r.Handle("/v2.1/events", + legacyAPIHandler{context, legacyListEvents}).Methods("GET") + r.Handle("/v2.1/events", + legacyAPIHandler{context, legacyClearEvents}).Methods("DELETE") + r.Handle("/v2.1/{tenant}/events", + legacyAPIHandler{context, legacyListTenantEvents}).Methods("GET") + + r.Handle("/v2.1/traces", + legacyAPIHandler{context, legacyListTraces}).Methods("GET") + r.Handle("/v2.1/traces/{label}", + legacyAPIHandler{context, legacyTraceData}).Methods("GET") + + return r +} diff --git a/ciao-controller/main.go b/ciao-controller/main.go index e01e0047a..66a6b0b92 100644 --- a/ciao-controller/main.go +++ b/ciao-controller/main.go @@ -187,7 +187,7 @@ func main() { } wg.Add(1) - go createComputeAPI(context) + go context.startComputeService() wg.Add(1) go context.startVolumeService() diff --git a/ciao-controller/openstack_compute.go b/ciao-controller/openstack_compute.go new file mode 100644 index 000000000..be473a1e8 --- /dev/null +++ b/ciao-controller/openstack_compute.go @@ -0,0 +1,350 @@ +// Copyright (c) 2016 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "errors" + "fmt" + "net/http" + "sort" + + "github.com/01org/ciao/ciao-controller/types" + "github.com/01org/ciao/openstack/compute" + osIdentity "github.com/01org/ciao/openstack/identity" + "github.com/01org/ciao/payloads" + "github.com/gorilla/mux" +) + +func instanceToServer(context *controller, instance *types.Instance) (compute.ServerDetails, error) { + workload, err := context.ds.GetWorkload(instance.WorkloadID) + if err != nil { + return compute.ServerDetails{}, err + } + + imageID := workload.ImageID + + server := compute.ServerDetails{ + HostID: instance.NodeID, + ID: instance.ID, + TenantID: instance.TenantID, + Flavor: compute.FlavorLinks{ + ID: instance.WorkloadID, + }, + Image: compute.Image{ + ID: imageID, + }, + Status: instance.State, + Addresses: compute.Addresses{ + Private: []compute.PrivateAddresses{ + { + Addr: instance.IPAddress, + OSEXTIPSMACMacAddr: instance.MACAddress, + }, + }, + }, + SSHIP: instance.SSHIP, + SSHPort: instance.SSHPort, + } + + return server, nil +} + +func (c *controller) CreateServer(tenant string, server compute.CreateServerRequest) (resp interface{}, err error) { + nInstances := 1 + + if server.Server.MaxInstances > 0 { + nInstances = server.Server.MaxInstances + } else if server.Server.MinInstances > 0 { + nInstances = server.Server.MinInstances + } + + // openstack doesn't allow us to use our traced start workload + // functionality. So we use the name field in our cli to indicate + // that we want to trace this workload. + trace := false + label := "" + if server.Server.Name != "" { + trace = true + label = server.Server.Name + } + + instances, err := c.startWorkload(server.Server.Flavor, tenant, nInstances, trace, label) + if err != nil { + return server, err + } + + var servers compute.Servers + + for _, instance := range instances { + server, err := instanceToServer(c, instance) + if err != nil { + return server, err + } + servers.Servers = append(servers.Servers, server) + } + + servers.TotalServers = len(instances) + + // set machine ID for OpenStack compatibility + server.Server.ID = instances[0].ID + + // builtServers is define to meet OpenStack compatibility on result + // format and keep CIAOs legacy behavior. + builtServers := struct { + compute.CreateServerRequest + compute.Servers + }{ + compute.CreateServerRequest{ + Server: server.Server, + }, + compute.Servers{ + TotalServers: servers.TotalServers, + Servers: servers.Servers, + }, + } + + return builtServers, nil +} + +func (c *controller) ListServersDetail(tenant string) ([]compute.ServerDetails, error) { + var servers []compute.ServerDetails + var err error + var instances []*types.Instance + + if tenant != "" { + instances, err = c.ds.GetAllInstancesFromTenant(tenant) + } else { + instances, err = c.ds.GetAllInstances() + } + + if err != nil { + return servers, err + } + + sort.Sort(types.SortedInstancesByID(instances)) + + for _, instance := range instances { + server, err := instanceToServer(c, instance) + if err != nil { + continue + } + + servers = append(servers, server) + } + + return servers, nil +} + +func (c *controller) ShowServerDetails(tenant string, server string) (compute.Server, error) { + var s compute.Server + + instance, err := c.ds.GetInstance(server) + if err != nil { + return s, err + } + + if instance.TenantID != tenant { + return s, compute.ErrServerOwner + } + + s.Server, err = instanceToServer(c, instance) + if err != nil { + return s, err + } + + return s, nil +} + +func (c *controller) DeleteServer(tenant string, server string) error { + /* First check that the instance belongs to this tenant */ + i, err := c.ds.GetInstance(server) + if err != nil { + return compute.ErrServerNotFound + } + + if i.TenantID != tenant { + return compute.ErrServerOwner + } + + err = c.deleteInstance(server) + if err != nil { + return err + } + + return nil +} + +func (c *controller) StartServer(tenant string, ID string) error { + i, err := c.ds.GetInstance(ID) + if err != nil { + return err + } + + if i.TenantID != tenant { + return compute.ErrServerOwner + } + + return c.restartInstance(ID) +} + +func (c *controller) StopServer(tenant string, ID string) error { + i, err := c.ds.GetInstance(ID) + if err != nil { + return err + } + + if i.TenantID != tenant { + return compute.ErrServerOwner + } + + return c.stopInstance(ID) +} + +func (c *controller) ListFlavors(tenant string) (compute.Flavors, error) { + flavors := compute.NewComputeFlavors() + + // we are ignoring tenant for now + workloads, err := c.ds.GetWorkloads() + if err != nil { + return flavors, err + } + + for _, workload := range workloads { + flavors.Flavors = append(flavors.Flavors, + struct { + ID string `json:"id"` + Links []compute.Link `json:"links"` + Name string `json:"name"` + }{ + ID: workload.ID, + Name: workload.Description, + }, + ) + } + + return flavors, nil +} + +func buildFlavorDetails(workload *types.Workload) (compute.FlavorDetails, error) { + var details compute.FlavorDetails + + defaults := workload.Defaults + if len(defaults) == 0 { + return details, fmt.Errorf("Workload resources not set") + } + + details.OsFlavorAccessIsPublic = true + details.ID = workload.ID + details.Disk = workload.ImageID + details.Name = workload.Description + + for r := range defaults { + switch defaults[r].Type { + case payloads.VCPUs: + details.Vcpus = defaults[r].Value + case payloads.MemMB: + details.RAM = defaults[r].Value + } + } + + return details, nil +} + +func (c *controller) ListFlavorsDetail(tenant string) (compute.FlavorsDetails, error) { + flavors := compute.NewComputeFlavorsDetails() + + // we ignore tenant for now + + workloads, err := c.ds.GetWorkloads() + if err != nil { + return flavors, err + } + + for _, workload := range workloads { + details, err := buildFlavorDetails(workload) + if err != nil { + continue + } + + flavors.Flavors = append(flavors.Flavors, details) + } + + return flavors, nil +} + +func (c *controller) ShowFlavorDetails(tenant string, flavorID string) (compute.Flavor, error) { + var flavor compute.Flavor + + workload, err := c.ds.GetWorkload(flavorID) + if err != nil { + return flavor, err + } + + flavor.Flavor, err = buildFlavorDetails(workload) + if err != nil { + return flavor, err + } + + return flavor, nil +} + +// Start will get the Compute API endpoints from the OpenStack compute api, +// then wrap them in keystone validation. It will then start the https +// service. +func (c *controller) startComputeService() error { + config := compute.APIConfig{Port: compute.APIPort, ComputeService: c} + + r := compute.Routes(config) + if r == nil { + return errors.New("Unable to start Compute Service") + } + + // we add on some ciao specific routes for legacy purposes + // using the openstack compute port. + r = legacyComputeRoutes(c, r) + + // setup identity for these routes. + validServices := []osIdentity.ValidService{ + {ServiceType: "compute", ServiceName: "ciao"}, + {ServiceType: "compute", ServiceName: "nova"}, + } + + validAdmins := []osIdentity.ValidAdmin{ + {Project: "service", Role: "admin"}, + {Project: "admin", Role: "admin"}, + } + + err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + h := osIdentity.Handler{ + Client: c.id.scV3, + Next: route.GetHandler(), + ValidServices: validServices, + ValidAdmins: validAdmins, + } + + route.Handler(h) + + return nil + }) + + if err != nil { + return err + } + + // start service. + service := fmt.Sprintf(":%d", compute.APIPort) + + return http.ListenAndServeTLS(service, httpsCAcert, httpsKey, r) +} diff --git a/ciao-controller/types/types.go b/ciao-controller/types/types.go index 3a1d69f4d..3a355ef86 100644 --- a/ciao-controller/types/types.go +++ b/ciao-controller/types/types.go @@ -17,6 +17,7 @@ package types import ( + "errors" "time" "github.com/01org/ciao/ciao-storage" @@ -96,7 +97,7 @@ func (s SortedInstancesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s SortedInstancesByID) Less(i, j int) bool { return s[i].ID < s[j].ID } // SortedComputeNodesByID implements sort.Interface for Node by ID string -type SortedComputeNodesByID []payloads.CiaoComputeNode +type SortedComputeNodesByID []CiaoComputeNode func (s SortedComputeNodesByID) Len() int { return len(s) } func (s SortedComputeNodesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } @@ -243,3 +244,260 @@ type StorageAttachment struct { InstanceID string // the instance this volume is attached to BlockID string // the ID of the block device } + +// CiaoComputeTenants represents the unmarshalled version of the contents of a +// /v2.1/tenants response. It contains information about the tenants in a ciao +// cluster. +type CiaoComputeTenants struct { + Tenants []struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"tenants"` +} + +// NewCiaoComputeTenants allocates a CiaoComputeTenants structure. +// It allocates the Tenants slice as well so that the marshalled +// JSON is an empty array and not a nil pointer, as specified by the +// OpenStack APIs. +func NewCiaoComputeTenants() (tenants CiaoComputeTenants) { + tenants.Tenants = []struct { + ID string `json:"id"` + Name string `json:"name"` + }{} + return +} + +// CiaoComputeNode contains status and statistic information for an individual +// node. +type CiaoComputeNode struct { + ID string `json:"id"` + Timestamp time.Time `json:"updated"` + Status string `json:"status"` + MemTotal int `json:"ram_total"` + MemAvailable int `json:"ram_available"` + DiskTotal int `json:"disk_total"` + DiskAvailable int `json:"disk_available"` + Load int `json:"load"` + OnlineCPUs int `json:"online_cpus"` + TotalInstances int `json:"total_instances"` + TotalRunningInstances int `json:"total_running_instances"` + TotalPendingInstances int `json:"total_pending_instances"` + TotalPausedInstances int `json:"total_paused_instances"` +} + +// CiaoComputeNodes represents the unmarshalled version of the contents of a +// /v2.1/nodes response. It contains status and statistics information +// for a set of nodes. +type CiaoComputeNodes struct { + Nodes []CiaoComputeNode `json:"nodes"` +} + +// NewCiaoComputeNodes allocates a CiaoComputeNodes structure. +// It allocates the Nodes slice as well so that the marshalled +// JSON is an empty array and not a nil pointer, as specified by the +// OpenStack APIs. +func NewCiaoComputeNodes() (nodes CiaoComputeNodes) { + nodes.Nodes = []CiaoComputeNode{} + return +} + +// CiaoTenantResources represents the unmarshalled version of the contents of a +// /v2.1/{tenant}/quotas response. It contains the current resource usage +// information for a tenant. +type CiaoTenantResources struct { + ID string `json:"id"` + Timestamp time.Time `json:"updated"` + InstanceLimit int `json:"instances_limit"` + InstanceUsage int `json:"instances_usage"` + VCPULimit int `json:"cpus_limit"` + VCPUUsage int `json:"cpus_usage"` + MemLimit int `json:"ram_limit"` + MemUsage int `json:"ram_usage"` + DiskLimit int `json:"disk_limit"` + DiskUsage int `json:"disk_usage"` +} + +// CiaoUsage contains a snapshot of resource consumption for a tenant. +type CiaoUsage struct { + VCPU int `json:"cpus_usage"` + Memory int `json:"ram_usage"` + Disk int `json:"disk_usage"` + Timestamp time.Time `json:"timestamp"` +} + +// CiaoUsageHistory represents the unmarshalled version of the contents of a +// /v2.1/{tenant}/resources response. It contains snapshots of usage information +// for a given tenant over a given period of time. +type CiaoUsageHistory struct { + Usages []CiaoUsage `json:"usage"` +} + +// CiaoCNCISubnet contains subnet information for a CNCI. +type CiaoCNCISubnet struct { + Subnet string `json:"subnet_cidr"` +} + +// CiaoCNCI contains information about an individual CNCI. +type CiaoCNCI struct { + ID string `json:"id"` + TenantID string `json:"tenant_id"` + IPv4 string `json:"IPv4"` + Geography string `json:"geography"` + Subnets []CiaoCNCISubnet `json:"subnets"` +} + +// CiaoCNCIDetail represents the unmarshalled version of the contents of a +// v2.1/cncis/{cnci}/detail response. It contains information about a CNCI. +type CiaoCNCIDetail struct { + CiaoCNCI `json:"cnci"` +} + +// CiaoCNCIs represents the unmarshalled version of the contents of a +// v2.1/cncis response. It contains information about all the CNCIs +// in the ciao cluster. +type CiaoCNCIs struct { + CNCIs []CiaoCNCI `json:"cncis"` +} + +// NewCiaoCNCIs allocates a CiaoCNCIs structure. +// It allocates the CNCIs slice as well so that the marshalled +// JSON is an empty array and not a nil pointer, as specified by the +// OpenStack APIs. +func NewCiaoCNCIs() (cncis CiaoCNCIs) { + cncis.CNCIs = []CiaoCNCI{} + return +} + +// CiaoServerStats contains status information about a CN or a NN. +type CiaoServerStats struct { + ID string `json:"id"` + NodeID string `json:"node_id"` + Timestamp time.Time `json:"updated"` + Status string `json:"status"` + TenantID string `json:"tenant_id"` + IPv4 string `json:"IPv4"` + VCPUUsage int `json:"cpus_usage"` + MemUsage int `json:"ram_usage"` + DiskUsage int `json:"disk_usage"` +} + +// CiaoServersStats represents the unmarshalled version of the contents of a +// v2.1/nodes/{node}/servers/detail response. It contains general information +// about a group of instances. +type CiaoServersStats struct { + TotalServers int `json:"total_servers"` + Servers []CiaoServerStats `json:"servers"` +} + +// NewCiaoServersStats allocates a CiaoServersStats structure. +// It allocates the Servers slice as well so that the marshalled +// JSON is an empty array and not a nil pointer, as specified by the +// OpenStack APIs. +func NewCiaoServersStats() (servers CiaoServersStats) { + servers.Servers = []CiaoServerStats{} + return +} + +// CiaoClusterStatus represents the unmarshalled version of the contents of a +// v2.1/nodes/summary response. It contains information about the nodes that +// make up a ciao cluster. +type CiaoClusterStatus struct { + Status struct { + TotalNodes int `json:"total_nodes"` + TotalNodesReady int `json:"total_nodes_ready"` + TotalNodesFull int `json:"total_nodes_full"` + TotalNodesOffline int `json:"total_nodes_offline"` + TotalNodesMaintenance int `json:"total_nodes_maintenance"` + } `json:"cluster"` +} + +// CNCIDetail stores the IPv4 for a CNCI Agent. +type CNCIDetail struct { + IPv4 string `json:"IPv4"` +} + +// CiaoServersAction represents the unmarshalled version of the contents of a +// v2.1/servers/action request. It contains an action to be performed on +// one or more instances. +type CiaoServersAction struct { + Action string `json:"action"` + ServerIDs []string `json:"servers"` +} + +// CiaoTraceSummary contains information about a specific SSNTP Trace label. +type CiaoTraceSummary struct { + Label string `json:"label"` + Instances int `json:"instances"` +} + +// CiaoTracesSummary represents the unmarshalled version of the response to a +// v2.1/traces request. It contains a list of all trace labels and the +// number of instances associated with them. +type CiaoTracesSummary struct { + Summaries []CiaoTraceSummary `json:"summaries"` +} + +// CiaoFrameStat contains the elapsed time statistics for a frame. +type CiaoFrameStat struct { + ID string `json:"node_id"` + TotalElapsedTime float64 `json:"total_elapsed_time"` + ControllerTime float64 `json:"total_controller_time"` + LauncherTime float64 `json:"total_launcher_time"` + SchedulerTime float64 `json:"total_scheduler_time"` +} + +// CiaoBatchFrameStat contains frame statisitics for a ciao cluster. +type CiaoBatchFrameStat struct { + NumInstances int `json:"num_instances"` + TotalElapsed float64 `json:"total_elapsed"` + AverageElapsed float64 `json:"average_elapsed"` + AverageControllerElapsed float64 `json:"average_controller_elapsed"` + AverageLauncherElapsed float64 `json:"average_launcher_elapsed"` + AverageSchedulerElapsed float64 `json:"average_scheduler_elapsed"` + VarianceController float64 `json:"controller_variance"` + VarianceLauncher float64 `json:"launcher_variance"` + VarianceScheduler float64 `json:"scheduler_variance"` +} + +// CiaoTraceData represents the unmarshalled version of the response to a +// v2.1/traces/{label} request. It contains statistics computed from the trace +// information of SSNTP commands sent within a ciao cluster. +type CiaoTraceData struct { + Summary CiaoBatchFrameStat `json:"summary"` + FramesStat []CiaoFrameStat `json:"frames"` +} + +// CiaoEvent contains information about an individual event generated +// in a ciao cluster. +type CiaoEvent struct { + Timestamp time.Time `json:"time_stamp"` + TenantID string `json:"tenant_id"` + EventType string `json:"type"` + Message string `json:"message"` +} + +// CiaoEvents represents the unmarshalled version of the response to a +// v2.1/{tenant}/event or v2.1/event request. +type CiaoEvents struct { + Events []CiaoEvent `json:"events"` +} + +// NewCiaoEvents allocates a CiaoEvents structure. +// It allocates the Events slice as well so that the marshalled +// JSON is an empty array and not a nil pointer, as specified by the +// OpenStack APIs. +func NewCiaoEvents() (events CiaoEvents) { + events.Events = []CiaoEvent{} + return +} + +var ( + // ErrQuota is returned when a resource limit is exceeded. + ErrQuota = errors.New("Over Quota") + + // ErrTenantNotFound is returned when a tenant ID is unknown. + ErrTenantNotFound = errors.New("Tenant not found") + + // ErrInstanceNotFound is returned when an instance is not found. + ErrInstanceNotFound = errors.New("Instance not found") +) diff --git a/openstack/compute/api.go b/openstack/compute/api.go new file mode 100644 index 000000000..eac7818a6 --- /dev/null +++ b/openstack/compute/api.go @@ -0,0 +1,741 @@ +// Copyright (c) 2016 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compute + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/http/httputil" + "strconv" + "strings" + "time" + + "github.com/golang/glog" + "github.com/gorilla/mux" +) + +// APIPort is the OpenStack compute port +const APIPort = 8774 + +// PrivateAddresses contains information about a single instance network +// interface. +type PrivateAddresses struct { + Addr string `json:"addr"` + OSEXTIPSMACMacAddr string `json:"OS-EXT-IPS-MAC:mac_addr"` + OSEXTIPSType string `json:"OS-EXT-IPS:type"` + Version int `json:"version"` +} + +// Addresses contains information about an instance's networks. +type Addresses struct { + Private []PrivateAddresses `json:"private"` +} + +// Link contains the address to a compute resource, like e.g. a Flavor or an +// Image. +type Link struct { + Href string `json:"href"` + Rel string `json:"rel"` +} + +// FlavorLinks provides links to a specific flavor ID. +type FlavorLinks struct { + ID string `json:"id"` + Links []Link `json:"links"` +} + +// Image identifies the base image of the instance. +type Image struct { + ID string `json:"id"` + Links []Link `json:"links"` +} + +// SecurityGroup represents the security group of an instance. +type SecurityGroup struct { + Name string `json:"name"` +} + +// These errors can be returned by the Service interface +var ( + ErrQuota = errors.New("Tenant over quota") + ErrTenantNotFound = errors.New("Tenant not found") + ErrServerNotFound = errors.New("Server not found") + ErrServerOwner = errors.New("You are not server owner") +) + +// errorResponse maps service error responses to http responses. +// this helper function can help functions avoid having to switch +// on return values all the time. +func errorResponse(err error) APIResponse { + switch err { + case ErrQuota: + return APIResponse{http.StatusForbidden, nil} + case ErrTenantNotFound: + return APIResponse{http.StatusNotFound, nil} + case ErrServerNotFound: + return APIResponse{http.StatusNotFound, nil} + case ErrServerOwner: + return APIResponse{http.StatusForbidden, nil} + default: + return APIResponse{http.StatusInternalServerError, nil} + } +} + +// ServerDetails contains information about a specific instance. +type ServerDetails struct { + Addresses Addresses `json:"addresses"` + Created time.Time `json:"created"` + Flavor FlavorLinks `json:"flavor"` + HostID string `json:"hostId"` + ID string `json:"id"` + Image Image `json:"image"` + KeyName string `json:"key_name"` + Links []Link `json:"links"` + Name string `json:"name"` + AccessIPv4 string `json:"accessIPv4"` + AccessIPv6 string `json:"accessIPv6"` + ConfigDrive string `json:"config_drive"` + OSDCFDiskConfig string `json:"OS-DCF:diskConfig"` + OSEXTAZAvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` + OSEXTSRVATTRHost string `json:"OS-EXT-SRV-ATTR:host"` + OSEXTSRVATTRHypervisorHostname string `json:"OS-EXT-SRV-ATTR:hypervisor_hostname"` + OSEXTSRVATTRInstanceName string `json:"OS-EXT-SRV-ATTR:instance_name"` + OSEXTSTSPowerState int `json:"OS-EXT-STS:power_state"` + OSEXTSTSTaskState string `json:"OS-EXT-STS:task_state"` + OSEXTSTSVMState string `json:"OS-EXT-STS:vm_state"` + OsExtendedVolumesVolumesAttached []string `json:"os-extended-volumes:volumes_attached"` + OSSRVUSGLaunchedAt time.Time `json:"OS-SRV-USG:launched_at"` + OSSRVUSGTerminatedAt time.Time `json:"OS-SRV-USG:terminated_at"` + Progress int `json:"progress"` + SecurityGroups []SecurityGroup `json:"security_groups"` + Status string `json:"status"` + HostStatus string `json:"host_status"` + TenantID string `json:"tenant_id"` + Updated time.Time `json:"updated"` + UserID string `json:"user_id"` + SSHIP string `json:"ssh_ip"` + SSHPort int `json:"ssh_port"` +} + +// Servers represents the unmarshalled version of the contents of a +// /v2.1/{tenant}/servers/detail response. It contains information about a +// set of instances within a ciao cluster. +// http://developer.openstack.org/api-ref-compute-v2.1.html#listServersDetailed +// BUG - TotalServers is not specified by the openstack api. We are going +// to pretend it is for now. +type Servers struct { + TotalServers int `json:"total_servers"` + Servers []ServerDetails `json:"servers"` +} + +// NewServers allocates a Servers structure. +// It allocates the Servers slice as well so that the marshalled +// JSON is an empty array and not a nil pointer for, as +// specified by the OpenStack APIs. +func NewServers() (servers Servers) { + servers.Servers = []ServerDetails{} + return +} + +// Server represents the unmarshalled version of the contents of a +// /v2.1/{tenant}/servers/{server} response. It contains information about a +// specific instance within a ciao cluster. +type Server struct { + Server ServerDetails `json:"server"` +} + +// Flavors represents the unmarshalled version of the contents of a +// /v2.1/{tenant}/flavors response. It contains information about all the +// flavors in a cluster. +type Flavors struct { + Flavors []struct { + ID string `json:"id"` + Links []Link `json:"links"` + Name string `json:"name"` + } `json:"flavors"` +} + +// NewComputeFlavors allocates a ComputeFlavors structure. +// It allocates the Flavors slice as well so that the marshalled +// JSON is an empty array and not a nil pointer, as specified +// by the OpenStack APIs. +func NewComputeFlavors() (flavors Flavors) { + flavors.Flavors = []struct { + ID string `json:"id"` + Links []Link `json:"links"` + Name string `json:"name"` + }{} + return +} + +// FlavorDetails contains information about a specific flavor. +type FlavorDetails struct { + OSFLVDISABLEDDisabled bool `json:"OS-FLV-DISABLED:disabled"` + Disk string `json:"disk"` /* OpenStack API says this is an int */ + OSFLVEXTDATAEphemeral int `json:"OS-FLV-EXT-DATA:ephemeral"` + OsFlavorAccessIsPublic bool `json:"os-flavor-access:is_public"` + ID string `json:"id"` + Links []Link `json:"links"` + Name string `json:"name"` + RAM int `json:"ram"` + Swap string `json:"swap"` + Vcpus int `json:"vcpus"` +} + +// Flavor represents the unmarshalled version of the contents of a +// /v2.1/{tenant}/flavors/{flavor} response. It contains information about a +// specific flavour. +type Flavor struct { + Flavor FlavorDetails `json:"flavor"` +} + +// FlavorsDetails represents the unmarshalled version of the contents of a +// /v2.1/{tenant}/flavors/detail response. It contains detailed information about +// all flavour for a given tenant. +type FlavorsDetails struct { + Flavors []FlavorDetails `json:"flavors"` +} + +// NewComputeFlavorsDetails allocates a ComputeFlavorsDetails structure. +// It allocates the Flavors slice as well so that the marshalled +// JSON is an empty array and not a nil pointer, as specified by the +// OpenStack APIs. +func NewComputeFlavorsDetails() (flavors FlavorsDetails) { + flavors.Flavors = []FlavorDetails{} + return +} + +// CreateServerRequest represents the unmarshalled version of the contents of a +// /v2.1/{tenant}/servers request. It contains the information needed to start +// one or more instances. +type CreateServerRequest struct { + Server struct { + ID string `json:"id"` + Name string `json:"name"` + Image string `json:"imageRef"` + Flavor string `json:"flavorRef"` + MaxInstances int `json:"max_count"` + MinInstances int `json:"min_count"` + } `json:"server"` +} + +// APIConfig contains information needed to start the compute api service. +type APIConfig struct { + Port int // the https port of the compute api service + ComputeService Service // the service interface +} + +// Service defines the interface required by the compute service. +type Service interface { + // server interfaces + CreateServer(string, CreateServerRequest) (interface{}, error) + ListServersDetail(tenant string) ([]ServerDetails, error) + ShowServerDetails(tenant string, server string) (Server, error) + DeleteServer(tenant string, server string) error + StartServer(tenant string, server string) error + StopServer(tenant string, server string) error + + //flavor interfaces + ListFlavors(string) (Flavors, error) + ListFlavorsDetail(string) (FlavorsDetails, error) + ShowFlavorDetails(string, string) (Flavor, error) +} + +type pagerFilterType uint8 + +const ( + none pagerFilterType = iota + changesSinceFilter + imageFilter + flavorFilter + nameFilter + statusFilter + hostFilter + limit + marker +) + +type pager interface { + filter(filterType pagerFilterType, filter string, item interface{}) bool + nextPage(filterType pagerFilterType, filter string, r *http.Request) ([]byte, error) +} + +type serverPager struct { + servers []ServerDetails +} + +func pagerQueryParse(r *http.Request) (int, int, string) { + values := r.URL.Query() + limit := 0 + offset := 0 + marker := "" + + // we only support marker and offset for now. + if values["marker"] != nil { + marker = values["marker"][0] + } else if values["offset"] != nil { + o, err := strconv.ParseInt(values["offset"][0], 10, 32) + if err != nil { + offset = 0 + } else { + offset = (int)(o) + } + } + + return limit, offset, marker +} + +func (pager *serverPager) getServers(filterType pagerFilterType, filter string, servers []ServerDetails, limit int, offset int) (Servers, error) { + newServers := NewServers() + + newServers.TotalServers = len(servers) + pageLength := 0 + + glog.V(2).Infof("Get servers limit [%d] offset [%d]", limit, offset) + + if servers == nil || offset >= len(servers) { + return newServers, nil + } + + for _, server := range servers[offset:] { + if filterType != none && + pager.filter(filterType, filter, server) { + continue + } + + newServers.Servers = append(newServers.Servers, server) + pageLength++ + if limit > 0 && pageLength >= limit { + break + } + } + + return newServers, nil +} + +func (pager *serverPager) filter(filterType pagerFilterType, filter string, server ServerDetails) bool { + // we only support filtering by flavor right now + switch filterType { + case flavorFilter: + if server.Flavor.ID != filter { + return true + } + } + + return false +} + +func (pager *serverPager) nextPage(filterType pagerFilterType, filter string, r *http.Request) (Servers, error) { + limit, offset, lastSeen := pagerQueryParse(r) + + glog.V(2).Infof("Next page marker [%s] limit [%d] offset [%d]", + lastSeen, limit, offset) + + if lastSeen == "" { + if limit != 0 { + return pager.getServers(filterType, filter, + pager.servers, limit, offset) + } + + return pager.getServers(filterType, filter, pager.servers, + 0, offset) + } + + for i, server := range pager.servers { + if server.ID == lastSeen { + if i >= len(pager.servers)-1 { + return pager.getServers(filterType, filter, + nil, limit, 0) + } + + return pager.getServers(filterType, filter, + pager.servers[i+1:], limit, 0) + } + } + + return Servers{}, fmt.Errorf("Item %s not found", lastSeen) +} + +type action uint8 + +const ( + computeActionStart action = iota + computeActionStop + computeActionDelete +) + +func dumpRequestBody(r *http.Request, body bool) { + if glog.V(2) { + dump, err := httputil.DumpRequest(r, body) + if err != nil { + glog.Errorf("HTTP request dump error %s", err) + } + + glog.Infof("HTTP request [%q]", dump) + } +} + +// DumpRequest will dump an http request if log level is 2 +func DumpRequest(r *http.Request) { + dumpRequestBody(r, false) +} + +// HTTPErrorData represents the HTTP response body for +// a compute API request error. +type HTTPErrorData struct { + Code int `json:"code"` + Name string `json:"name"` + Message string `json:"message"` +} + +// HTTPReturnErrorCode represents the unmarshalled version for Return codes +// when a API call is made and you need to return explicit data of +// the call as OpenStack format +// http://developer.openstack.org/api-guide/compute/faults.html +type HTTPReturnErrorCode struct { + Error HTTPErrorData `json:"error"` +} + +// Context contains information needed by the compute API service +type Context struct { + port int + Service +} + +// APIResponse is returned from all compute API functions. +// It contains the http status and response to be marshalled if needed. +type APIResponse struct { + Status int + Response interface{} +} + +// APIHandler is a custom handler for the compute APIs. +// This custom handler allows us to more cleanly return an error and response, +// and pass some package level context into the handler. +type APIHandler struct { + *Context + Handler func(*Context, http.ResponseWriter, *http.Request) (APIResponse, error) +} + +// ServeHTTP satisfies the interface for the http Handler. +// If the individual handler returns an error, then it will marshal +// an error response. +func (h APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + resp, err := h.Handler(h.Context, w, r) + if err != nil { + data := HTTPErrorData{ + Code: resp.Status, + Name: http.StatusText(resp.Status), + Message: err.Error(), + } + + code := HTTPReturnErrorCode{ + Error: data, + } + + b, err := json.Marshal(code) + if err != nil { + http.Error(w, http.StatusText(resp.Status), resp.Status) + } + + http.Error(w, string(b), resp.Status) + } + + b, err := json.Marshal(resp.Response) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), + http.StatusInternalServerError) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resp.Status) + w.Write(b) +} + +// @Title createServer +// @Description Creates a server. +// @Accept json +// @Success 202 {object} Servers "Returns Servers and CreateServerRequest with data of the created server." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/{tenant}/servers [post] +// @Resource /v2.1/{tenant}/servers +func createServer(c *Context, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + + vars := mux.Vars(r) + tenant := vars["tenant"] + + DumpRequest(r) + + defer r.Body.Close() + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return APIResponse{http.StatusBadRequest, nil}, err + } + + var req CreateServerRequest + + err = json.Unmarshal(body, &req) + if err != nil { + return APIResponse{http.StatusBadRequest, nil}, err + } + + resp, err := c.CreateServer(tenant, req) + if err != nil { + return errorResponse(err), err + } + + return APIResponse{http.StatusAccepted, resp}, nil +} + +// ListServersDetails provides server details by tenant or by flavor. +// This function is exported for use by ciao-controller due to legacy +// endpoint using the "flavor" option. It is simpler to just overload +// this function than to reimplement the legacy code. +// +// @Title ListServerDetails +// @Description Lists all servers with details. +// @Accept json +// @Success 200 {array} ServerDetails "Returns details of all servers." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/{tenant}/servers/detail [get] +// @Resource /v2.1/{tenant}/servers +func ListServersDetails(c *Context, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + tenant := vars["tenant"] + + // flavor will never have a valid value. This is left over from + // when this function could be used by a different endpoint. + // the function needs to be rewritten with no pager. + flavor := vars["flavor"] + + DumpRequest(r) + + servers, err := c.ListServersDetail(tenant) + if err != nil { + return errorResponse(err), err + } + + pager := serverPager{servers: servers} + filterType := none + filter := "" + if flavor != "" { + filterType = flavorFilter + filter = flavor + } + + resp, err := pager.nextPage(filterType, filter, r) + if err != nil { + return errorResponse(err), err + } + + return APIResponse{http.StatusOK, resp}, nil +} + +// @Title showServerDetails +// @Description Shows details for a server. +// @Accept json +// @Success 200 {object} Server "Returns details for a server." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/{tenant}/servers/{server} [get] +// @Resource /v2.1/{tenant}/servers +func showServerDetails(c *Context, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + tenant := vars["tenant"] + server := vars["server"] + + DumpRequest(r) + + resp, err := c.ShowServerDetails(tenant, server) + if err != nil { + return errorResponse(err), err + } + + return APIResponse{http.StatusOK, resp}, nil +} + +// @Title deleteServer +// @Description Deletes a server. +// @Accept json +// @Success 204 {object} string "This operation does not return a response body, returns the 204 StatusNoContent code." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/{tenant}/servers/{server} [delete] +// @Resource /v2.1/{tenant}/servers +func deleteServer(c *Context, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + tenant := vars["tenant"] + server := vars["server"] + + DumpRequest(r) + + err := c.DeleteServer(tenant, server) + if err != nil { + return errorResponse(err), err + } + + return APIResponse{http.StatusNoContent, nil}, nil +} + +// @Title serverAction +// @Description Runs the indicated action (os-start, os-stop) in the a server. +// @Accept json +// @Success 202 {object} string "This operation does not return a response body, returns the 202 StatusAccepted code." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/{tenant}/servers/{server}/action [post] +// @Resource /v2.1/{tenant}/servers +func serverAction(c *Context, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + tenant := vars["tenant"] + server := vars["server"] + + DumpRequest(r) + + defer r.Body.Close() + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return APIResponse{http.StatusBadRequest, nil}, err + } + + bodyString := string(body) + + var action action + + if strings.Contains(bodyString, "os-start") { + action = computeActionStart + } else if strings.Contains(bodyString, "os-stop") { + action = computeActionStop + } else { + return APIResponse{http.StatusServiceUnavailable, nil}, + errors.New("Unsupported Action") + } + + switch action { + case computeActionStart: + err = c.StartServer(tenant, server) + case computeActionStop: + err = c.StopServer(tenant, server) + } + + if err != nil { + return errorResponse(err), err + } + + return APIResponse{http.StatusAccepted, nil}, nil +} + +// @Title listFlavors +// @Description Lists flavors. +// @Accept json +// @Success 200 {object} Flavors "Returns Flavors with the corresponding available flavors for the tenant." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/{tenant}/flavors [get] +// @Resource /v2.1/{tenant}/flavors +func listFlavors(c *Context, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + tenant := vars["tenant"] + + DumpRequest(r) + + resp, err := c.ListFlavors(tenant) + if err != nil { + return errorResponse(err), err + } + + return APIResponse{http.StatusOK, resp}, nil +} + +// @Title listFlavorsDetails +// @Description Lists flavors with details. +// @Accept json +// @Success 200 {object} FlavorsDetails "Returns FlavorsDetails with flavor details." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/{tenant}/flavors/detail [get] +// @Resource /v2.1/{tenant}/flavors +func listFlavorsDetails(c *Context, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + tenant := vars["tenant"] + + DumpRequest(r) + + resp, err := c.ListFlavorsDetail(tenant) + if err != nil { + return errorResponse(err), err + } + + return APIResponse{http.StatusOK, resp}, nil +} + +// @Title showFlavorDetails +// @Description Shows details for a flavor. +// @Accept json +// @Success 200 {object} Flavor "Returns details for a flavor." +// @Failure 400 {object} HTTPReturnErrorCode "The response contains the corresponding message and 40x corresponding code." +// @Failure 500 {object} HTTPReturnErrorCode "The response contains the corresponding message and 50x corresponding code." +// @Router /v2.1/{tenant}/flavors/{flavor} [get] +// @Resource /v2.1/{tenant}/flavors +func showFlavorDetails(c *Context, w http.ResponseWriter, r *http.Request) (APIResponse, error) { + vars := mux.Vars(r) + tenant := vars["tenant"] + flavor := vars["flavor"] + + DumpRequest(r) + + resp, err := c.ShowFlavorDetails(tenant, flavor) + if err != nil { + return errorResponse(err), err + } + + return APIResponse{http.StatusOK, resp}, nil +} + +// Routes returns a gorilla mux router for the compute endpoints. +func Routes(config APIConfig) *mux.Router { + context := &Context{config.Port, config.ComputeService} + + r := mux.NewRouter() + + // servers endpoints + r.Handle("/v2.1/{tenant}/servers", + APIHandler{context, createServer}).Methods("POST") + r.Handle("/v2.1/{tenant}/servers/detail", + APIHandler{context, ListServersDetails}).Methods("GET") + r.Handle("/v2.1/{tenant}/servers/{server}", + APIHandler{context, showServerDetails}).Methods("GET") + r.Handle("/v2.1/{tenant}/servers/{server}", + APIHandler{context, deleteServer}).Methods("DELETE") + r.Handle("/v2.1/{tenant}/servers/{server}/action", + APIHandler{context, serverAction}).Methods("POST") + + // flavor related endpoints + r.Handle("/v2.1/{tenant}/flavors", + APIHandler{context, listFlavors}).Methods("GET") + r.Handle("/v2.1/{tenant}/flavors/detail", + APIHandler{context, listFlavorsDetails}).Methods("GET") + r.Handle("/v2.1/{tenant}/flavors/{flavor}", + APIHandler{context, showFlavorDetails}).Methods("GET") + + return r +} diff --git a/openstack/compute/api_test.go b/openstack/compute/api_test.go new file mode 100644 index 000000000..8ecb873ed --- /dev/null +++ b/openstack/compute/api_test.go @@ -0,0 +1,276 @@ +// Copyright (c) 2016 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compute + +import ( + "bytes" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +type test struct { + method string + pattern string + handler func(*Context, http.ResponseWriter, *http.Request) (APIResponse, error) + request string + expectedStatus int + expectedResponse string +} + +func myHostname() string { + host, _ := os.Hostname() + return host +} + +var tests = []test{ + { + "POST", + "/v2.1/{tenant}/servers/", + createServer, + `{"server":{"name":"new-server-test","imageRef": "http://glance.openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b","flavorRef":"http://openstack.example.com/flavors/1","metadata":{"My Server Name":"Apache1"}}}`, + http.StatusAccepted, + `{"server":{"id":"validServerID","name":"new-server-test","imageRef":"http://glance.openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b","flavorRef":"http://openstack.example.com/flavors/1","max_count":0,"min_count":0}}`, + }, + { + "GET", + "/v2.1/{tenant}/servers/detail", + ListServersDetails, + "", + http.StatusOK, + `{"total_servers":1,"servers":[{"addresses":{"private":[{"addr":"192.169.0.1","OS-EXT-IPS-MAC:mac_addr":"00:02:00:01:02:03","OS-EXT-IPS:type":"","version":0}]},"created":"0001-01-01T00:00:00Z","flavor":{"id":"testFlavorUUID","links":null},"hostId":"hostUUID","id":"testUUID","image":{"id":"testImageUUID","links":null},"key_name":"","links":null,"name":"","accessIPv4":"","accessIPv6":"","config_drive":"","OS-DCF:diskConfig":"","OS-EXT-AZ:availability_zone":"","OS-EXT-SRV-ATTR:host":"","OS-EXT-SRV-ATTR:hypervisor_hostname":"","OS-EXT-SRV-ATTR:instance_name":"","OS-EXT-STS:power_state":0,"OS-EXT-STS:task_state":"","OS-EXT-STS:vm_state":"","os-extended-volumes:volumes_attached":null,"OS-SRV-USG:launched_at":"0001-01-01T00:00:00Z","OS-SRV-USG:terminated_at":"0001-01-01T00:00:00Z","progress":0,"security_groups":null,"status":"active","host_status":"","tenant_id":"","updated":"0001-01-01T00:00:00Z","user_id":"","ssh_ip":"","ssh_port":0}]}`, + }, + { + "GET", + "/v2.1/{tenant}/servers/{server}", + showServerDetails, + "", + http.StatusOK, + `{"server":{"addresses":{"private":[{"addr":"192.169.0.1","OS-EXT-IPS-MAC:mac_addr":"00:02:00:01:02:03","OS-EXT-IPS:type":"","version":0}]},"created":"0001-01-01T00:00:00Z","flavor":{"id":"testFlavorUUID","links":null},"hostId":"hostUUID","id":"","image":{"id":"testImageUUID","links":null},"key_name":"","links":null,"name":"","accessIPv4":"","accessIPv6":"","config_drive":"","OS-DCF:diskConfig":"","OS-EXT-AZ:availability_zone":"","OS-EXT-SRV-ATTR:host":"","OS-EXT-SRV-ATTR:hypervisor_hostname":"","OS-EXT-SRV-ATTR:instance_name":"","OS-EXT-STS:power_state":0,"OS-EXT-STS:task_state":"","OS-EXT-STS:vm_state":"","os-extended-volumes:volumes_attached":null,"OS-SRV-USG:launched_at":"0001-01-01T00:00:00Z","OS-SRV-USG:terminated_at":"0001-01-01T00:00:00Z","progress":0,"security_groups":null,"status":"active","host_status":"","tenant_id":"","updated":"0001-01-01T00:00:00Z","user_id":"","ssh_ip":"","ssh_port":0}}`, + }, + { + "DELETE", + "/v2.1/{tenant}/servers/{server}", + deleteServer, + "", + http.StatusNoContent, + "null", + }, + { + "POST", + "/v2.1/{tenant}/servers/{server}/action", + serverAction, + `{"os-start":null}`, + http.StatusAccepted, + "null", + }, + { + "POST", + "/v2.1/{tenant}/servers/{server}/action", + serverAction, + `{"os-stop":null}`, + http.StatusAccepted, + "null", + }, + { + "GET", + "/v2.1/{tenant}/flavors/", + listFlavors, + "", + http.StatusOK, + `{"flavors":[{"id":"flavorUUID","links":null,"name":"testflavor"}]}`, + }, + { + "GET", + "/v2.1/{tenant}/flavors/", + listFlavorsDetails, + "", + http.StatusOK, + `{"flavors":[{"OS-FLV-DISABLED:disabled":false,"disk":"imageUUID","OS-FLV-EXT-DATA:ephemeral":0,"os-flavor-access:is_public":true,"id":"workloadUUID","links":null,"name":"testflavor","ram":256,"swap":"","vcpus":2}]}`, + }, + { + "GET", + "/v2.1/{tenant}/flavors/", + showFlavorDetails, + "", + http.StatusOK, + `{"flavor":{"OS-FLV-DISABLED:disabled":false,"disk":"imageUUID","OS-FLV-EXT-DATA:ephemeral":0,"os-flavor-access:is_public":true,"id":"workloadUUID","links":null,"name":"testflavor","ram":256,"swap":"","vcpus":2}}`, + }, +} + +type testComputeService struct{} + +// server interfaces +func (cs testComputeService) CreateServer(tenant string, req CreateServerRequest) (interface{}, error) { + req.Server.ID = "validServerID" + return req, nil +} + +func (cs testComputeService) ListServersDetail(tenant string) ([]ServerDetails, error) { + var servers []ServerDetails + + server := ServerDetails{ + HostID: "hostUUID", + ID: "testUUID", + TenantID: tenant, + Flavor: FlavorLinks{ + ID: "testFlavorUUID", + }, + Image: Image{ + ID: "testImageUUID", + }, + Status: "active", + Addresses: Addresses{ + Private: []PrivateAddresses{ + { + Addr: "192.169.0.1", + OSEXTIPSMACMacAddr: "00:02:00:01:02:03", + }, + }, + }, + } + + servers = append(servers, server) + + return servers, nil +} + +func (cs testComputeService) ShowServerDetails(tenant string, server string) (Server, error) { + s := ServerDetails{ + HostID: "hostUUID", + ID: server, + TenantID: tenant, + Flavor: FlavorLinks{ + ID: "testFlavorUUID", + }, + Image: Image{ + ID: "testImageUUID", + }, + Status: "active", + Addresses: Addresses{ + Private: []PrivateAddresses{ + { + Addr: "192.169.0.1", + OSEXTIPSMACMacAddr: "00:02:00:01:02:03", + }, + }, + }, + } + + return Server{Server: s}, nil +} + +func (cs testComputeService) DeleteServer(tenant string, server string) error { + return nil +} + +func (cs testComputeService) StartServer(tenant string, server string) error { + return nil +} + +func (cs testComputeService) StopServer(tenant string, server string) error { + return nil +} + +//flavor interfaces +func (cs testComputeService) ListFlavors(string) (Flavors, error) { + flavors := NewComputeFlavors() + + flavors.Flavors = append(flavors.Flavors, + struct { + ID string `json:"id"` + Links []Link `json:"links"` + Name string `json:"name"` + }{ + ID: "flavorUUID", + Name: "testflavor", + }, + ) + + return flavors, nil +} + +func (cs testComputeService) ListFlavorsDetail(string) (FlavorsDetails, error) { + flavors := NewComputeFlavorsDetails() + var details FlavorDetails + + details.OsFlavorAccessIsPublic = true + details.ID = "workloadUUID" + details.Disk = "imageUUID" + details.Name = "testflavor" + details.Vcpus = 2 + details.RAM = 256 + + flavors.Flavors = append(flavors.Flavors, details) + + return flavors, nil +} + +func (cs testComputeService) ShowFlavorDetails(string, string) (Flavor, error) { + var details FlavorDetails + var flavor Flavor + + details.OsFlavorAccessIsPublic = true + details.ID = "workloadUUID" + details.Disk = "imageUUID" + details.Name = "testflavor" + details.Vcpus = 2 + details.RAM = 256 + + flavor.Flavor = details + + return flavor, nil +} + +func TestAPIResponse(t *testing.T) { + var cs testComputeService + + // TBD: add context to test definition so it can be created per + // endpoint with either a pass testComputeService or a failure + // one. + context := &Context{8774, cs} + + for _, tt := range tests { + req, err := http.NewRequest(tt.method, tt.pattern, bytes.NewBuffer([]byte(tt.request))) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + handler := APIHandler{context, tt.handler} + + handler.ServeHTTP(rr, req) + + status := rr.Code + if status != tt.expectedStatus { + t.Errorf("got %v, expected %v", status, tt.expectedStatus) + } + + if rr.Body.String() != tt.expectedResponse { + t.Errorf("%s: failed\ngot: %v\nexp: %v", tt.pattern, rr.Body.String(), tt.expectedResponse) + } + } +} + +func TestRoutes(t *testing.T) { + var cs testComputeService + config := APIConfig{8774, cs} + + r := Routes(config) + if r == nil { + t.Fatalf("No routes returned") + } +} diff --git a/payloads/compute.go b/payloads/compute.go deleted file mode 100644 index b7b5192a6..000000000 --- a/payloads/compute.go +++ /dev/null @@ -1,470 +0,0 @@ -/* -// Copyright (c) 2016 Intel Corporation -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -*/ - -package payloads - -import ( - "time" -) - -// PrivateAddresses contains information about a single instance network -// interface. -type PrivateAddresses struct { - Addr string `json:"addr"` - OSEXTIPSMACMacAddr string `json:"OS-EXT-IPS-MAC:mac_addr"` - OSEXTIPSType string `json:"OS-EXT-IPS:type"` - Version int `json:"version"` -} - -// Addresses contains information about an instance's networks. -type Addresses struct { - Private []PrivateAddresses `json:"private"` -} - -// Link contains the address to a compute resource, like e.g. a Flavor or an -// Image. -type Link struct { - Href string `json:"href"` - Rel string `json:"rel"` -} - -// Flavor identifies the flavour (workload) of an instance. -type Flavor struct { - ID string `json:"id"` - Links []Link `json:"links"` -} - -// Image identifies the base image of the instance. -type Image struct { - ID string `json:"id"` - Links []Link `json:"links"` -} - -// SecurityGroup represents the security group of an instance. -type SecurityGroup struct { - Name string `json:"name"` -} - -const ( - // ComputeStatusPending is a filter that used to select pending - // instances in requests to the controller. - ComputeStatusPending = "pending" - - // ComputeStatusRunning is a filter that used to select running - // instances in requests to the controller. - ComputeStatusRunning = "active" - - // ComputeStatusStopped is a filter that used to select exited - // instances in requests to the controller. - ComputeStatusStopped = "exited" -) - -// Server contains information about a specific instance within a ciao cluster. -type Server struct { - Addresses Addresses `json:"addresses"` - Created time.Time `json:"created"` - Flavor Flavor `json:"flavor"` - HostID string `json:"hostId"` - ID string `json:"id"` - Image Image `json:"image"` - KeyName string `json:"key_name"` - Links []Link `json:"links"` - Name string `json:"name"` - AccessIPv4 string `json:"accessIPv4"` - AccessIPv6 string `json:"accessIPv6"` - ConfigDrive string `json:"config_drive"` - OSDCFDiskConfig string `json:"OS-DCF:diskConfig"` - OSEXTAZAvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` - OSEXTSRVATTRHost string `json:"OS-EXT-SRV-ATTR:host"` - OSEXTSRVATTRHypervisorHostname string `json:"OS-EXT-SRV-ATTR:hypervisor_hostname"` - OSEXTSRVATTRInstanceName string `json:"OS-EXT-SRV-ATTR:instance_name"` - OSEXTSTSPowerState int `json:"OS-EXT-STS:power_state"` - OSEXTSTSTaskState string `json:"OS-EXT-STS:task_state"` - OSEXTSTSVMState string `json:"OS-EXT-STS:vm_state"` - OsExtendedVolumesVolumesAttached []string `json:"os-extended-volumes:volumes_attached"` - OSSRVUSGLaunchedAt time.Time `json:"OS-SRV-USG:launched_at"` - OSSRVUSGTerminatedAt time.Time `json:"OS-SRV-USG:terminated_at"` - Progress int `json:"progress"` - SecurityGroups []SecurityGroup `json:"security_groups"` - Status string `json:"status"` - HostStatus string `json:"host_status"` - TenantID string `json:"tenant_id"` - Updated time.Time `json:"updated"` - UserID string `json:"user_id"` - SSHIP string `json:"ssh_ip"` - SSHPort int `json:"ssh_port"` -} - -// ComputeServers represents the unmarshalled version of the contents of a -// /v2.1/{tenant}/servers/detail response. It contains information about a -// set of instances within a ciao cluster. -type ComputeServers struct { - TotalServers int `json:"total_servers"` - Servers []Server `json:"servers"` -} - -// NewComputeServers allocates a ComputeServers structure. -// It allocates the Servers slice as well so that the marshalled -// JSON is an empty array and not a nil pointer for, as -// specified by the OpenStack APIs. -func NewComputeServers() (servers ComputeServers) { - servers.Servers = []Server{} - return -} - -// ComputeServer represents the unmarshalled version of the contents of a -// /v2.1/{tenant}/servers/{server} response. It contains information about a -// specific instance within a ciao cluster. -type ComputeServer struct { - Server Server `json:"server"` -} - -// ComputeFlavors represents the unmarshalled version of the contents of a -// /v2.1/{tenant}/flavors response. It contains information about all the -// flavors in a cluster. -type ComputeFlavors struct { - Flavors []struct { - ID string `json:"id"` - Links []Link `json:"links"` - Name string `json:"name"` - } `json:"flavors"` -} - -// NewComputeFlavors allocates a ComputeFlavors structure. -// It allocates the Flavors slice as well so that the marshalled -// JSON is an empty array and not a nil pointer, as specified -// by the OpenStack APIs. -func NewComputeFlavors() (flavors ComputeFlavors) { - flavors.Flavors = []struct { - ID string `json:"id"` - Links []Link `json:"links"` - Name string `json:"name"` - }{} - return -} - -// FlavorDetails contains information about a specific flavor. -type FlavorDetails struct { - OSFLVDISABLEDDisabled bool `json:"OS-FLV-DISABLED:disabled"` - Disk string `json:"disk"` /* OpenStack API says this is an int */ - OSFLVEXTDATAEphemeral int `json:"OS-FLV-EXT-DATA:ephemeral"` - OsFlavorAccessIsPublic bool `json:"os-flavor-access:is_public"` - ID string `json:"id"` - Links []Link `json:"links"` - Name string `json:"name"` - RAM int `json:"ram"` - Swap string `json:"swap"` - Vcpus int `json:"vcpus"` -} - -// ComputeFlavorDetails represents the unmarshalled version of the contents of a -// /v2.1/{tenant}/flavors/{flavor} response. It contains information about a -// specific flavour. -type ComputeFlavorDetails struct { - Flavor FlavorDetails `json:"flavor"` -} - -// ComputeFlavorsDetails represents the unmarshalled version of the contents of a -// /v2.1/{tenant}/flavors/detail response. It contains detailed information about -// all flavour for a given tenant. -type ComputeFlavorsDetails struct { - Flavors []FlavorDetails `json:"flavors"` -} - -// NewComputeFlavorsDetails allocates a ComputeFlavorsDetails structure. -// It allocates the Flavors slice as well so that the marshalled -// JSON is an empty array and not a nil pointer, as specified by the -// OpenStack APIs. -func NewComputeFlavorsDetails() (flavors ComputeFlavorsDetails) { - flavors.Flavors = []FlavorDetails{} - return -} - -// ComputeCreateServer represents the unmarshalled version of the contents of a -// /v2.1/{tenant}/servers request. It contains the information needed to start -// one or more instances. -type ComputeCreateServer struct { - Server struct { - ID string `json:"id"` - Name string `json:"name"` - Image string `json:"imageRef"` - Workload string `json:"flavorRef"` - MaxInstances int `json:"max_count"` - MinInstances int `json:"min_count"` - } `json:"server"` -} - -// CiaoComputeTenants represents the unmarshalled version of the contents of a -// /v2.1/tenants response. It contains information about the tenants in a ciao -// cluster. -type CiaoComputeTenants struct { - Tenants []struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"tenants"` -} - -// NewCiaoComputeTenants allocates a CiaoComputeTenants structure. -// It allocates the Tenants slice as well so that the marshalled -// JSON is an empty array and not a nil pointer, as specified by the -// OpenStack APIs. -func NewCiaoComputeTenants() (tenants CiaoComputeTenants) { - tenants.Tenants = []struct { - ID string `json:"id"` - Name string `json:"name"` - }{} - return -} - -// CiaoComputeNode contains status and statistic information for an individual -// node. -type CiaoComputeNode struct { - ID string `json:"id"` - Timestamp time.Time `json:"updated"` - Status string `json:"status"` - MemTotal int `json:"ram_total"` - MemAvailable int `json:"ram_available"` - DiskTotal int `json:"disk_total"` - DiskAvailable int `json:"disk_available"` - Load int `json:"load"` - OnlineCPUs int `json:"online_cpus"` - TotalInstances int `json:"total_instances"` - TotalRunningInstances int `json:"total_running_instances"` - TotalPendingInstances int `json:"total_pending_instances"` - TotalPausedInstances int `json:"total_paused_instances"` -} - -// CiaoComputeNodes represents the unmarshalled version of the contents of a -// /v2.1/nodes response. It contains status and statistics information -// for a set of nodes. -type CiaoComputeNodes struct { - Nodes []CiaoComputeNode `json:"nodes"` -} - -// NewCiaoComputeNodes allocates a CiaoComputeNodes structure. -// It allocates the Nodes slice as well so that the marshalled -// JSON is an empty array and not a nil pointer, as specified by the -// OpenStack APIs. -func NewCiaoComputeNodes() (nodes CiaoComputeNodes) { - nodes.Nodes = []CiaoComputeNode{} - return -} - -// CiaoTenantResources represents the unmarshalled version of the contents of a -// /v2.1/{tenant}/quotas response. It contains the current resource usage -// information for a tenant. -type CiaoTenantResources struct { - ID string `json:"id"` - Timestamp time.Time `json:"updated"` - InstanceLimit int `json:"instances_limit"` - InstanceUsage int `json:"instances_usage"` - VCPULimit int `json:"cpus_limit"` - VCPUUsage int `json:"cpus_usage"` - MemLimit int `json:"ram_limit"` - MemUsage int `json:"ram_usage"` - DiskLimit int `json:"disk_limit"` - DiskUsage int `json:"disk_usage"` -} - -// CiaoUsage contains a snapshot of resource consumption for a tenant. -type CiaoUsage struct { - VCPU int `json:"cpus_usage"` - Memory int `json:"ram_usage"` - Disk int `json:"disk_usage"` - Timestamp time.Time `json:"timestamp"` -} - -// CiaoUsageHistory represents the unmarshalled version of the contents of a -// /v2.1/{tenant}/resources response. It contains snapshots of usage information -// for a given tenant over a given period of time. -type CiaoUsageHistory struct { - Usages []CiaoUsage `json:"usage"` -} - -// CiaoCNCISubnet contains subnet information for a CNCI. -type CiaoCNCISubnet struct { - Subnet string `json:"subnet_cidr"` -} - -// CiaoCNCI contains information about an individual CNCI. -type CiaoCNCI struct { - ID string `json:"id"` - TenantID string `json:"tenant_id"` - IPv4 string `json:"IPv4"` - Geography string `json:"geography"` - Subnets []CiaoCNCISubnet `json:"subnets"` -} - -// CiaoCNCIDetail represents the unmarshalled version of the contents of a -// v2.1/cncis/{cnci}/detail response. It contains information about a CNCI. -type CiaoCNCIDetail struct { - CiaoCNCI `json:"cnci"` -} - -// CiaoCNCIs represents the unmarshalled version of the contents of a -// v2.1/cncis response. It contains information about all the CNCIs -// in the ciao cluster. -type CiaoCNCIs struct { - CNCIs []CiaoCNCI `json:"cncis"` -} - -// NewCiaoCNCIs allocates a CiaoCNCIs structure. -// It allocates the CNCIs slice as well so that the marshalled -// JSON is an empty array and not a nil pointer, as specified by the -// OpenStack APIs. -func NewCiaoCNCIs() (cncis CiaoCNCIs) { - cncis.CNCIs = []CiaoCNCI{} - return -} - -// CiaoServerStats contains status information about a CN or a NN. -type CiaoServerStats struct { - ID string `json:"id"` - NodeID string `json:"node_id"` - Timestamp time.Time `json:"updated"` - Status string `json:"status"` - TenantID string `json:"tenant_id"` - IPv4 string `json:"IPv4"` - VCPUUsage int `json:"cpus_usage"` - MemUsage int `json:"ram_usage"` - DiskUsage int `json:"disk_usage"` -} - -// CiaoServersStats represents the unmarshalled version of the contents of a -// v2.1/nodes/{node}/servers/detail response. It contains general information -// about a group of instances. -type CiaoServersStats struct { - TotalServers int `json:"total_servers"` - Servers []CiaoServerStats `json:"servers"` -} - -// NewCiaoServersStats allocates a CiaoServersStats structure. -// It allocates the Servers slice as well so that the marshalled -// JSON is an empty array and not a nil pointer, as specified by the -// OpenStack APIs. -func NewCiaoServersStats() (servers CiaoServersStats) { - servers.Servers = []CiaoServerStats{} - return -} - -// CiaoClusterStatus represents the unmarshalled version of the contents of a -// v2.1/nodes/summary response. It contains information about the nodes that -// make up a ciao cluster. -type CiaoClusterStatus struct { - Status struct { - TotalNodes int `json:"total_nodes"` - TotalNodesReady int `json:"total_nodes_ready"` - TotalNodesFull int `json:"total_nodes_full"` - TotalNodesOffline int `json:"total_nodes_offline"` - TotalNodesMaintenance int `json:"total_nodes_maintenance"` - } `json:"cluster"` -} - -// CNCIDetail stores the IPv4 for a CNCI Agent. -type CNCIDetail struct { - IPv4 string `json:"IPv4"` -} - -// CiaoServersAction represents the unmarshalled version of the contents of a -// v2.1/servers/action request. It contains an action to be performed on -// one or more instances. -type CiaoServersAction struct { - Action string `json:"action"` - ServerIDs []string `json:"servers"` -} - -// CiaoTraceSummary contains information about a specific SSNTP Trace label. -type CiaoTraceSummary struct { - Label string `json:"label"` - Instances int `json:"instances"` -} - -// CiaoTracesSummary represents the unmarshalled version of the response to a -// v2.1/traces request. It contains a list of all trace labels and the -// number of instances associated with them. -type CiaoTracesSummary struct { - Summaries []CiaoTraceSummary `json:"summaries"` -} - -// CiaoFrameStat contains the elapsed time statistics for a frame. -type CiaoFrameStat struct { - ID string `json:"node_id"` - TotalElapsedTime float64 `json:"total_elapsed_time"` - ControllerTime float64 `json:"total_controller_time"` - LauncherTime float64 `json:"total_launcher_time"` - SchedulerTime float64 `json:"total_scheduler_time"` -} - -// CiaoBatchFrameStat contains frame statisitics for a ciao cluster. -type CiaoBatchFrameStat struct { - NumInstances int `json:"num_instances"` - TotalElapsed float64 `json:"total_elapsed"` - AverageElapsed float64 `json:"average_elapsed"` - AverageControllerElapsed float64 `json:"average_controller_elapsed"` - AverageLauncherElapsed float64 `json:"average_launcher_elapsed"` - AverageSchedulerElapsed float64 `json:"average_scheduler_elapsed"` - VarianceController float64 `json:"controller_variance"` - VarianceLauncher float64 `json:"launcher_variance"` - VarianceScheduler float64 `json:"scheduler_variance"` -} - -// CiaoTraceData represents the unmarshalled version of the response to a -// v2.1/traces/{label} request. It contains statistics computed from the trace -// information of SSNTP commands sent within a ciao cluster. -type CiaoTraceData struct { - Summary CiaoBatchFrameStat `json:"summary"` - FramesStat []CiaoFrameStat `json:"frames"` -} - -// CiaoEvent contains information about an individual event generated -// in a ciao cluster. -type CiaoEvent struct { - Timestamp time.Time `json:"time_stamp"` - TenantID string `json:"tenant_id"` - EventType string `json:"type"` - Message string `json:"message"` -} - -// CiaoEvents represents the unmarshalled version of the response to a -// v2.1/{tenant}/event or v2.1/event request. -type CiaoEvents struct { - Events []CiaoEvent `json:"events"` -} - -// NewCiaoEvents allocates a CiaoEvents structure. -// It allocates the Events slice as well so that the marshalled -// JSON is an empty array and not a nil pointer, as specified by the -// OpenStack APIs. -func NewCiaoEvents() (events CiaoEvents) { - events.Events = []CiaoEvent{} - return -} - -// HTTPErrorData represents the HTTP response body for -// a compute API request error. -type HTTPErrorData struct { - Code int `json:"code"` - Name string `json:"name"` - Message string `json:"message"` -} - -// HTTPReturnErrorCode represents the unmarshalled version for Return codes -// when a API call is made and you need to return explicit data of -// the call as OpenStack format -// http://developer.openstack.org/api-guide/compute/faults.html -type HTTPReturnErrorCode struct { - Error HTTPErrorData `json:"error"` -} diff --git a/payloads/stats.go b/payloads/stats.go index 501e21b2b..1b055bb7f 100644 --- a/payloads/stats.go +++ b/payloads/stats.go @@ -104,6 +104,20 @@ type Stat struct { Instances []InstanceStat } +const ( + // ComputeStatusPending is a filter that used to select pending + // instances in requests to the controller. + ComputeStatusPending = "pending" + + // ComputeStatusRunning is a filter that used to select running + // instances in requests to the controller. + ComputeStatusRunning = "active" + + // ComputeStatusStopped is a filter that used to select exited + // instances in requests to the controller. + ComputeStatusStopped = "exited" +) + const ( // Pending indicates that ciao-launcher has not yet ascertained the // state of a given instance. This can happen, either because the