diff --git a/src/oca/go/share/examples/service_example.go b/src/oca/go/share/examples/service_example.go new file mode 100644 index 0000000000..670f641eaf --- /dev/null +++ b/src/oca/go/share/examples/service_example.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "log" + + "github.com/OpenNebula/one/src/oca/go/src/goca" +) + +var rclient *goca.RESTClient +var controller *goca.Controller + +func init() { + rclient = goca.NewRESTClient( + goca.NewFlowConfig("", "", ""), + ) + xclient := goca.NewDefaultClient( + goca.NewConfig("", "", ""), + ) + + controller = goca.NewController(xclient, rclient) +} + +func main() { + testClient() + testGoca() +} + +// Shows oneflow server up and running +func testClient() { + response, e := rclient.Get("service") + + if e == nil { + body := response.BodyMap() + + fmt.Println(body) + + } else { + fmt.Println(e) + } +} + +func testGoca() { + id := 4 + + serviceCtrl := controller.Service(id) + serv, e := serviceCtrl.Show(id) + + if e != nil { + log.Fatalln(e) + } + + fmt.Println(serv.ID) + fmt.Println(serv.Name) + + fmt.Println("============") + + var status bool + var body string + + status, body = serviceCtrl.Shutdown(id) + + fmt.Println(status) + fmt.Println(body) + + fmt.Println("============") + + status, body = serviceCtrl.Delete(id) + fmt.Println(status) + fmt.Println(body) + + fmt.Println("============") +} diff --git a/src/oca/go/src/goca/controller.go b/src/oca/go/src/goca/controller.go index 6f074adbcb..73dc019906 100644 --- a/src/oca/go/src/goca/controller.go +++ b/src/oca/go/src/goca/controller.go @@ -21,9 +21,15 @@ type RPCCaller interface { Call(method string, args ...interface{}) (*Response, error) } +// HTTPCaller is the analogous to RPCCaller but for http endpoints +type HTTPCaller interface { + HTTPMethod(method string, url string, args ...interface{}) (*Response, error) +} + // Controller is the controller used to make requets on various entities type Controller struct { - Client RPCCaller + Client RPCCaller + ClientREST HTTPCaller } // entitiesController is a controller for entitites diff --git a/src/oca/go/src/goca/flow_client.go b/src/oca/go/src/goca/flow_client.go new file mode 100644 index 0000000000..2e65341fb2 --- /dev/null +++ b/src/oca/go/src/goca/flow_client.go @@ -0,0 +1,222 @@ +package goca + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "log" + "net/http" + "os" + "strings" +) + +// RESTClient for communicating with oneflow server +type RESTClient struct { + user string + pass string + address string // oneflow server address, ie: http://localhost:2474 + + httpClient *http.Client +} + +// NewRESTClient Constructor +func NewRESTClient(conf HTTPAuth) *RESTClient { + return &RESTClient{ + user: conf.user, + pass: conf.pass, + address: conf.address, + + httpClient: http.DefaultClient, + } +} + +// HTTPAuth holds credentials for a server address +type HTTPAuth struct { + user string + pass string + address string // oneflow server address, ie: http://localhost:2474 +} + +// NewFlowConfig considering environment variables and such +func NewFlowConfig(fuser, fpass, fURL string) HTTPAuth { + // 1 - ONEFLOW_URL, ONEFLOW_USER and ONEFLOW_PASSWORD + // 2 - ONE_AUTH + // 3 - ~/.one/one_auth + + var conf HTTPAuth + + if fURL == "" { + conf.address = os.Getenv("ONEFLOW_URL") + + if conf.address == "" { + conf.address = "http://localhost:2474" + } + } else { + conf.address = fURL + } + + if fuser == "" && fpass == "" { + oneAuthPath := os.Getenv("ONE_AUTH") + if oneAuthPath == "" { + oneAuthPath = os.Getenv("HOME") + "/.one/one_auth" + } + + oneAuth, err := ioutil.ReadFile(oneAuthPath) + var auth string + + if err == nil { + auth = string(oneAuth) + } else { + log.Fatalln(err) + } + + credentials := strings.Split(auth, ":") + + conf.user = credentials[0] + conf.pass = credentials[1] + + } else { + conf.user = fuser + conf.pass = fpass + } + + return conf +} + +// NewHTTPResponse Creates Response from flow http response +func NewHTTPResponse(r *http.Response, e error) (*Response, error) { + if e != nil { + return &Response{}, e + } + + status := true + + // HTTP 2XX + if r.StatusCode/100 != 2 { + status = false + } + + return &Response{ + status: status, + body: bodyToStr(r), + }, nil +} + +// HTTPMethod interface to client internals +func (c *RESTClient) HTTPMethod(method string, url string, args ...interface{}) (*Response, error) { + var e error + var response Response + r := &response + + switch method { + case "GET": + r, e = c.Get(string(url)) + case "DELETE": + r, e = c.Delete(string(url)) + case "POST": + r, e = c.Post(string(url), args[1].(map[string]interface{})) + case "PUT": + r, e = c.Put(string(url), args[1].(map[string]interface{})) + case "": + return &Response{}, e + } + + return r, e +} + +// HTTP METHODS +// The url passed to the methods is the follow up to the endpoint +// ex. use service instead of http://localhost:2474/service + +// Get http +func (c *RESTClient) Get(eurl string) (*Response, error) { + url := genurl(c.address, eurl) + + return NewHTTPResponse(httpReq(c, "GET", url, nil)) +} + +// Delete http +func (c *RESTClient) Delete(eurl string) (*Response, error) { + url := genurl(c.address, eurl) + + return NewHTTPResponse(httpReq(c, "DELETE", url, nil)) +} + +// Post http +func (c *RESTClient) Post(eurl string, message map[string]interface{}) (*Response, error) { + url := genurl(c.address, eurl) + + return NewHTTPResponse(httpReq(c, "POST", url, message)) + +} + +// Put http +func (c *RESTClient) Put(eurl string, message map[string]interface{}) (*Response, error) { + url := genurl(c.address, eurl) + + return NewHTTPResponse(httpReq(c, "PUT", url, message)) + +} + +// BodyMap accesses the body of the response and returns it as a map +func (r *Response) BodyMap() map[string]interface{} { + var bodyMap map[string]interface{} + + if err := json.Unmarshal([]byte(r.body), &bodyMap); err != nil { + panic(err) + } + + return bodyMap +} + +// Btomap returns http body as map +func bodyToMap(response *http.Response) map[string]interface{} { + var result map[string]interface{} + + json.NewDecoder(response.Body).Decode(&result) + + return result +} + +// Btostr returns http body as string +func bodyToStr(response *http.Response) string { + body, err := ioutil.ReadAll(response.Body) + + if err != nil { + log.Fatalln(err) + } + + return string(body) +} + +// HELPERS + +// General http request method for the c. +func httpReq(c *RESTClient, method string, eurl string, message map[string]interface{}) (*http.Response, error) { + req, err := http.NewRequest(method, eurl, bodyContent(message)) + + if err != nil { + log.Fatalln(err) + } + + req.SetBasicAuth(c.user, c.pass) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + return c.httpClient.Do(req) +} + +// concatenates flow endpoint with flow server address in a string +func genurl(address, endpoint string) string { + return strings.Join([]string{address, endpoint}, "/") +} + +// BodyContent prepares map for put/post http requests +func bodyContent(message map[string]interface{}) *bytes.Buffer { + represent, err := json.Marshal(message) + + if err != nil { + log.Fatalln(err) + } + + return bytes.NewBuffer(represent) +} diff --git a/src/oca/go/src/goca/flow_client_test.go b/src/oca/go/src/goca/flow_client_test.go new file mode 100644 index 0000000000..41ad2c93ca --- /dev/null +++ b/src/oca/go/src/goca/flow_client_test.go @@ -0,0 +1,24 @@ +package goca + +import ( + "testing" +) + +func TestFlowClient(t *testing.T) { + client := createRESTClient() + + response, e := client.HTTPMethod("GET", "service") + + if e != nil { + t.Fatal(e) + } + + if response.status == false { + t.Error(response.Body()) + } +} + +func createRESTClient() *RESTClient { + config := NewFlowConfig("", "", "") + return NewRESTClient(config) +} diff --git a/src/oca/go/src/goca/schemas/service/service.go b/src/oca/go/src/goca/schemas/service/service.go new file mode 100644 index 0000000000..0d2e230a69 --- /dev/null +++ b/src/oca/go/src/goca/schemas/service/service.go @@ -0,0 +1,7 @@ +package service + +// Service schema +type Service struct { + Template + State int +} diff --git a/src/oca/go/src/goca/schemas/service/template.go b/src/oca/go/src/goca/schemas/service/template.go new file mode 100644 index 0000000000..4bb3d2a10f --- /dev/null +++ b/src/oca/go/src/goca/schemas/service/template.go @@ -0,0 +1,12 @@ +package service + +// Template schema +type Template struct { + Name string + Roles []map[string]interface{} + ID int `json:",omitempty"` + Deployment string `json:",omitempty"` + ShutdownAction string `json:",omitempty"` + ReadyStatusGate bool `json:",omitempty"` + JSON map[string]interface{} `json:",omitempty"` +} diff --git a/src/oca/go/src/goca/service.go b/src/oca/go/src/goca/service.go new file mode 100644 index 0000000000..0d9775175a --- /dev/null +++ b/src/oca/go/src/goca/service.go @@ -0,0 +1,233 @@ +package goca + +import ( + "fmt" + "strconv" + + "github.com/OpenNebula/one/src/oca/go/src/goca/schemas/service" +) + +var endpointFService string + +func init() { + endpointFService = "service" +} + +// ServiceController interacts with oneflow service. Uses REST Client. +type ServiceController entityController + +// ServicesController interacts with oneflow services. Uses REST Client. +type ServicesController entitiesController + +// Service Controller constructor +func (c *Controller) Service(id int) *ServiceController { + return &ServiceController{c, id} +} + +// Services Controller constructor +func (c *Controller) Services() *ServicesController { + return &ServicesController{c} +} + +// NewService constructor +func NewService(docJSON map[string]interface{}) *service.Service { + var serv service.Service + + template := NewTemplate(docJSON) + + serv.Template = *template + serv.State = template.JSON["state"].(int) + + return &serv +} + +// OpenNebula Actions + +// Show the SERVICE resource identified by +func (sc *ServiceController) Show() (*service.Service, error) { + url := urlService(sc.ID) + + response, e := sc.c.ClientREST.HTTPMethod("GET", url) + + if e != nil { + return &service.Service{}, e + } + + return NewService(documentJSON(response)), nil +} + +// Delete the SERVICE resource identified by +func (sc *ServiceController) Delete() (bool, string) { + url := urlService(sc.ID) + + return sc.c.boolResponse("DELETE", url, nil) +} + +// Shutdown running services +func (sc *ServiceController) Shutdown() (bool, string) { + action := make(map[string]interface{}) + + action["action"] = map[string]interface{}{ + "perform": "shutdown", + } + + return sc.Action(action) +} + +// Recover existing service +func (sc *ServiceController) Recover() (bool, string) { + action := make(map[string]interface{}) + + action["action"] = map[string]interface{}{ + "perform": "recover", + } + + return sc.Action(action) +} + +// List the contents of the SERVICE collection. +func (ssc *ServicesController) List() (*[]*service.Service, error) { + var services []*service.Service + + response, e := ssc.c.ClientREST.HTTPMethod("GET", endpointFService) + + if e != nil { + services = append(services, &service.Service{}) + return &services, e + } + + documents := response.BodyMap()["DOCUMENT_POOL"].(map[string]interface{}) + + for _, v := range documents { + service := NewService(v.(map[string]interface{})) + services = append(services, service) + } + + return &services, e +} + +// Role operations + +// Scale the cardinality of a service role +func (sc *ServiceController) Scale(role string, cardinal int) (bool, string) { + + roleBody := make(map[string]interface{}) + + roleBody["cardinality"] = 2 + roleBody["force"] = true + + return sc.UpdateRole(role, roleBody) +} + +// VMAction performs the action on every VM belonging to role. Available actions: +// shutdown, shutdown-hard, undeploy, undeploy-hard, hold, release, stop, suspend, resume, boot, delete, delete-recreate, reboot, reboot-hard, poweroff, poweroff-hard, snapshot-create. +// Example params. Read the flow API docu. +// map[string]interface{}{ +// "period": 60, +// "number": 2, +// }, +// TODO: enforce only available actions +func (sc *ServiceController) VMAction(role, name string, params map[string]interface{}) (bool, string) { + url := fmt.Sprintf("%s/action", urlRole(sc.ID, role)) + + action := make(map[string]interface{}) + + action["action"] = map[string]interface{}{ + "perform": name, + "params": params, + } + + return sc.c.boolResponse("POST", url, action) +} + +// UpdateRole of a given Service +func (sc *ServiceController) UpdateRole(name string, body map[string]interface{}) (bool, string) { + url := urlRole(sc.ID, name) + + return sc.c.boolResponse("PUT", url, body) +} + +// Permissions operations + +// Chgrp service +func (sc *ServiceController) Chgrp(gid int) (bool, string) { + action := make(map[string]interface{}) + + action["action"] = map[string]interface{}{ + "perform": "chgrp", + "params": map[string]interface{}{ + "group_id": gid, + }, + } + + return sc.Action(action) +} + +// Chown service +func (sc *ServiceController) Chown(uid, gid int) (bool, string) { + action := make(map[string]interface{}) + + action["action"] = map[string]interface{}{ + "perform": "chgrp", + "params": map[string]interface{}{ + "group_id": gid, + "user_id": uid, + }, + } + + return sc.Action(action) +} + +// Chmod service +func (sc *ServiceController) Chmod(owner, group, other int) (bool, string) { + action := make(map[string]interface{}) + + action["action"] = map[string]interface{}{ + "perform": "chgrp", + "params": map[string]interface{}{ + "owner": owner, + "group": group, + "other": other, + }, + } + + return sc.Action(action) +} + +// Helpers + +func documentJSON(response *Response) map[string]interface{} { + responseJSON := response.BodyMap() + + return responseJSON["DOCUMENT"].(map[string]interface{}) +} + +func urlServiceAction(id int) string { + return fmt.Sprintf("%s/action", urlService(id)) +} + +func urlRole(id int, name string) string { + return fmt.Sprintf("%s/role/%s", urlService(id), name) + +} + +func urlService(id int) string { + return fmt.Sprintf("%s/%s", endpointFService, strconv.Itoa(id)) +} + +// Action handler for existing flow services. Requires the action body. +func (sc *ServiceController) Action(action map[string]interface{}) (bool, string) { + url := urlServiceAction(sc.ID) + + return sc.c.boolResponse("POST", url, action) +} + +func (c *Controller) boolResponse(method string, url string, body map[string]interface{}) (bool, string) { + response, e := c.ClientREST.HTTPMethod(method, url, body) + + if e != nil { + return false, e.Error() + } + + return response.status, response.Body() +} diff --git a/src/oca/go/src/goca/service_template.go b/src/oca/go/src/goca/service_template.go new file mode 100644 index 0000000000..ee9134d0a2 --- /dev/null +++ b/src/oca/go/src/goca/service_template.go @@ -0,0 +1,212 @@ +package goca + +import ( + "fmt" + "strconv" + + "github.com/OpenNebula/one/src/oca/go/src/goca/schemas/service" +) + +var endpointFTemplate string + +func init() { + endpointFTemplate = "service_template" +} + +// STemplateController interacts with oneflow service. Uses REST Client. +type STemplateController entityController + +// STemplatesController interacts with oneflow services. Uses REST Client. +type STemplatesController entitiesController + +// STemplate Controller constructor +func (c *Controller) STemplate(id int) *STemplateController { + return &STemplateController{c, id} +} + +// STemplates Controller constructor +func (c *Controller) STemplates() *STemplatesController { + return &STemplatesController{c} +} + +// NewTemplate constructor +func NewTemplate(docJSON map[string]interface{}) *service.Template { + var template service.Template + + template.JSON = docJSON + + body := docJSON["TEMPLATE"].(map[string]interface{})["BODY"].(map[string]interface{}) + + id, err := strconv.Atoi(docJSON["ID"].(string)) + + if err == nil { + template.ID = id + } + + template.Name = body["name"].(string) + template.Deployment = body["deployment"].(string) + + ready, err := strconv.ParseBool(body["ready_status_gate"].(string)) + + if err == nil { + template.ReadyStatusGate = ready + } + + template.Roles = body["roles"].([]map[string]interface{}) + + return &template +} + +// Map Template to map +func (tc *STemplateController) Map(st *service.Template) map[string]interface{} { + body := map[string]interface{}{ + "name": st.Name, + "roles": st.Roles, + "ready_status_gate": st.ReadyStatusGate, + } + + if st.Deployment != "" { + body["deployment"] = st.Deployment + } + + return body +} + +// OpenNebula Actions + +// Create service template +func (tc *STemplateController) Create(st *service.Template) (*service.Template, error) { + body := tc.Map(st) + + response, e := tc.c.ClientREST.HTTPMethod("POST", endpointFTemplate, body) + + if e != nil { + return &service.Template{}, e + } + + return NewTemplate(documentJSON(response)), nil +} + +// Delete the SERVICE resource identified by +func (tc *STemplateController) Delete() (bool, string) { + url := urlTemplate(tc.ID) + + return tc.c.boolResponse("DELETE", url, nil) +} + +// Update service template +func (tc *STemplateController) Update(st *service.Template) (bool, string) { + url := urlTemplate(tc.ID) + body := tc.Map(st) + + return tc.c.boolResponse("PUT", url, body) +} + +// Show the service template +func (tc *STemplateController) Show() (*service.Template, error) { + url := urlTemplate(tc.ID) + + response, e := tc.c.ClientREST.HTTPMethod("GET", url) + + if e != nil { + return &service.Template{}, e + } + + return NewTemplate(documentJSON(response)), nil +} + +// List service templates +func (tsc *STemplatesController) List() (*[]*service.Template, error) { + var templates []*service.Template + + response, e := tsc.c.ClientREST.HTTPMethod("GET", endpointFTemplate) + + if e != nil { + templates = append(templates, &service.Template{}) + return &templates, e + } + + documents := response.BodyMap()["DOCUMENT_POOL"].(map[string]interface{}) + + for _, v := range documents { + template := NewTemplate(v.(map[string]interface{})) + templates = append(templates, template) + } + + return &templates, e +} + +// Instantiate the service_template resource identified by +func (tc *STemplateController) Instantiate() (bool, string) { + action := make(map[string]interface{}) + + action["action"] = map[string]interface{}{ + "perform": "instantiate", + } + + return tc.Action(action) +} + +// Action handler for service_templates identified by +func (tc *STemplateController) Action(action map[string]interface{}) (bool, string) { + url := urlTemplateAction(tc.ID) + + return tc.c.boolResponse("POST", url, action) +} + +// Permissions operations + +// Chgrp template +func (tc *STemplateController) Chgrp(gid int) (bool, string) { + action := make(map[string]interface{}) + + action["action"] = map[string]interface{}{ + "perform": "chgrp", + "params": map[string]interface{}{ + "group_id": gid, + }, + } + + return tc.Action(action) +} + +// Chown template +func (tc *STemplateController) Chown(uid, gid int) (bool, string) { + action := make(map[string]interface{}) + + action["action"] = map[string]interface{}{ + "perform": "chgrp", + "params": map[string]interface{}{ + "group_id": gid, + "user_id": uid, + }, + } + + return tc.Action(action) +} + +// Chmod template +func (tc *STemplateController) Chmod(owner, group, other int) (bool, string) { + action := make(map[string]interface{}) + + action["action"] = map[string]interface{}{ + "perform": "chgrp", + "params": map[string]interface{}{ + "owner": owner, + "group": group, + "other": other, + }, + } + + return tc.Action(action) +} + +// Helpers + +func urlTemplateAction(id int) string { + return fmt.Sprintf("%s/action", urlTemplate(id)) +} + +func urlTemplate(id int) string { + return fmt.Sprintf("%s/%s", endpointFTemplate, strconv.Itoa(id)) +} diff --git a/src/oca/go/src/goca/service_test.go b/src/oca/go/src/goca/service_test.go new file mode 100644 index 0000000000..2ea9f42913 --- /dev/null +++ b/src/oca/go/src/goca/service_test.go @@ -0,0 +1,40 @@ +package goca + +import ( + "fmt" + "testing" +) + +func TestService(t *testing.T) { + c := createController() + services := c.Services() + + response, e := services.List() + + if e != nil { + t.Fatal(e) + } + + fmt.Println(response) +} + +func createController() *Controller { + config := NewFlowConfig("", "", "") + client := NewRESTClient(config) + + controller := NewController(nil) + + controller.ClientREST = client + + return controller +} + +func createRole(name string) map[string]interface{} { + return map[string]interface{}{ + "name": name, + "cardiniality": 1, + "vm_template": 0, + "elasticity_policies": []map[string]interface{}{}, + "scheduled_policies": []map[string]interface{}{}, + } +}