1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-16 22:50:10 +03:00

F #4298: Support for OneFlow services in GOCA

Co-authored-by: Daniel Clavijo Coca <dclavijo@opennebula.io>
This commit is contained in:
Daniel Clavijo Coca 2020-05-21 03:44:56 -05:00 committed by GitHub
parent 5448adcd6c
commit fdac6fa6c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 830 additions and 1 deletions

View File

@ -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("============")
}

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -0,0 +1,7 @@
package service
// Service schema
type Service struct {
Template
State int
}

View File

@ -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"`
}

View File

@ -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 <id>
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 <id>
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()
}

View File

@ -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 <id>
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 <id>
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 <id>
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))
}

View File

@ -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{}{},
}
}