From b8552b72a12f940e29d9df833a3ad27d503fed8f Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Wed, 22 May 2019 08:39:46 +0000 Subject: [PATCH 1/8] guests: add guest.GetPrivateIPs() --- pkg/compute/models/guests.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/compute/models/guests.go b/pkg/compute/models/guests.go index 40a55a2948..db872d1d4d 100644 --- a/pkg/compute/models/guests.go +++ b/pkg/compute/models/guests.go @@ -1692,6 +1692,21 @@ func (self *SGuest) getVirtualIPs() []string { return ips } +func (self *SGuest) GetPrivateIPs() []string { + ips := self.GetRealIPs() + for i := len(ips) - 1; i >= 0; i-- { + ipAddr, err := netutils.NewIPV4Addr(ips[i]) + if err != nil { + log.Errorf("guest %s(%s) has bad ipv4 address (%s): %v", self.Name, self.Id, ips[i], err) + continue + } + if !netutils.IsPrivate(ipAddr) { + ips = append(ips[:i], ips[i+1:]...) + } + } + return ips +} + func (self *SGuest) getIPs() []string { ips := self.GetRealIPs() vips := self.getVirtualIPs() From 5fb930c3a82ffafba2c6ac63dd3f0b16ef8e8895 Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Thu, 23 May 2019 08:26:10 +0000 Subject: [PATCH 2/8] validators: add SetParent() --- pkg/cloudcommon/validators/validators.go | 27 +++++++++++-------- pkg/cloudcommon/validators/validators_cert.go | 6 ++--- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/pkg/cloudcommon/validators/validators.go b/pkg/cloudcommon/validators/validators.go index 5e71428c61..a86d6ceb71 100644 --- a/pkg/cloudcommon/validators/validators.go +++ b/pkg/cloudcommon/validators/validators.go @@ -60,6 +60,11 @@ type Validator struct { value jsonutils.JSONObject } +func (v *Validator) SetParent(parent IValidator) IValidator { + v.parent = parent + return parent +} + func (v *Validator) Optional(optional bool) IValidator { v.optional = optional return v.parent @@ -132,7 +137,7 @@ func NewIPv4PrefixValidator(key string) *ValidatorIPv4Prefix { v := &ValidatorIPv4Prefix{ Validator: Validator{Key: key}, } - v.parent = v + v.SetParent(v) return v } @@ -164,7 +169,7 @@ func NewStringChoicesValidator(key string, choices choices.Choices) *ValidatorSt Validator: Validator{Key: key}, Choices: choices, } - v.parent = v + v.SetParent(v) return v } @@ -210,7 +215,7 @@ func NewStringMultiChoicesValidator(key string, choices choices.Choices) *Valida Validator: Validator{Key: key}, Choices: choices, } - v.parent = v + v.SetParent(v) return v } @@ -311,7 +316,7 @@ func NewBoolValidator(key string) *ValidatorBool { v := &ValidatorBool{ Validator: Validator{Key: key}, } - v.parent = v + v.SetParent(v) return v } @@ -354,7 +359,7 @@ func NewRangeValidator(key string, lower int64, upper int64) *ValidatorRange { Lower: lower, Upper: upper, } - v.parent = v + v.SetParent(v) return v } @@ -434,7 +439,7 @@ func NewModelIdOrNameValidator(key string, modelKeyword string, ownerId mcclient ModelKeyword: modelKeyword, modelIdKey: key + "_id", } - v.parent = v + v.SetParent(v) return v } @@ -535,7 +540,7 @@ func NewRegexpValidator(key string, regexp *regexp.Regexp) *ValidatorRegexp { Validator: Validator{Key: key}, Regexp: regexp, } - v.parent = v + v.SetParent(v) return v } @@ -547,7 +552,7 @@ func NewDomainNameValidator(key string) *ValidatorDomainName { v := &ValidatorDomainName{ ValidatorRegexp: *NewRegexpValidator(key, regutils.DOMAINNAME_REG), } - v.parent = v + v.SetParent(v) return v } @@ -562,7 +567,7 @@ func NewURLPathValidator(key string) *ValidatorURLPath { v := &ValidatorURLPath{ ValidatorRegexp: *NewRegexpValidator(key, regexpURLPath), } - v.parent = v + v.SetParent(v) return v } @@ -598,7 +603,7 @@ func NewStructValidator(key string, value interface{}) *ValidatorStruct { Validator: Validator{Key: key}, Value: value, } - v.parent = v + v.SetParent(v) return v } @@ -645,6 +650,6 @@ func NewIPv4AddrValidator(key string) *ValidatorIPv4Addr { v := &ValidatorIPv4Addr{ Validator: Validator{Key: key}, } - v.parent = v + v.SetParent(v) return v } diff --git a/pkg/cloudcommon/validators/validators_cert.go b/pkg/cloudcommon/validators/validators_cert.go index 7c18122371..2c097b96dc 100644 --- a/pkg/cloudcommon/validators/validators_cert.go +++ b/pkg/cloudcommon/validators/validators_cert.go @@ -37,7 +37,7 @@ func NewPEMValidator(key string) *ValidatorPEM { v := &ValidatorPEM{ Validator: Validator{Key: key}, } - v.parent = v + v.SetParent(v) return v } @@ -101,7 +101,7 @@ func NewCertificateValidator(key string) *ValidatorCertificate { v := &ValidatorCertificate{ ValidatorPEM: *NewPEMValidator(key), } - v.parent = v + v.SetParent(v) return v } @@ -232,7 +232,7 @@ func NewPrivateKeyValidator(key string) *ValidatorPrivateKey { v := &ValidatorPrivateKey{ ValidatorPEM: *NewPEMValidator(key), } - v.parent = v + v.SetParent(v) return v } From b60d62a85ffb3be1245ddd0d56b5eb12354654a5 Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Mon, 20 May 2019 13:06:16 +0000 Subject: [PATCH 3/8] climc: ansible-hosts: print newline at the end --- cmd/climc/shell/ansible.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/climc/shell/ansible.go b/cmd/climc/shell/ansible.go index ca227325c3..577686757b 100644 --- a/cmd/climc/shell/ansible.go +++ b/cmd/climc/shell/ansible.go @@ -94,7 +94,7 @@ func doList(s *mcclient.ClientSession, args *AnsibleHostsOptions) error { output.Add(children, "all", "children") output.Add(hosts, "ungrouped", "hosts") - fmt.Printf("%s", output.PrettyString()) + fmt.Printf("%s\n", output.PrettyString()) return nil } @@ -123,7 +123,7 @@ func doHost(s *mcclient.ClientSession, host string, args *AnsibleHostsOptions) e hostVar.Add(jsonutils.NewString(args.UserBecome), "ansible_become_user") } - fmt.Printf("%s", hostVar.PrettyString()) + fmt.Printf("%s\n", hostVar.PrettyString()) return nil } From 669ae4712f3aabda9ddb08ceaae6da867d3995ba Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Wed, 15 May 2019 08:43:17 +0000 Subject: [PATCH 4/8] vendor: update github.com/pkg/errors Add WithMessagef() --- Gopkg.lock | 6 +-- vendor/github.com/pkg/errors/.travis.yml | 12 ++++-- vendor/github.com/pkg/errors/README.md | 4 +- vendor/github.com/pkg/errors/errors.go | 43 +++++++++++++------- vendor/github.com/pkg/errors/stack.go | 51 +++++------------------- 5 files changed, 51 insertions(+), 65 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 91123c91f4..2037785355 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1060,12 +1060,12 @@ version = "v2.0.7" [[projects]] - digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747" + digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b" name = "github.com/pkg/errors" packages = ["."] pruneopts = "UT" - revision = "645ef00459ed84a119197bfb8d8205042c6df63d" - version = "v0.8.0" + revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4" + version = "v0.8.1" [[projects]] branch = "master" diff --git a/vendor/github.com/pkg/errors/.travis.yml b/vendor/github.com/pkg/errors/.travis.yml index 588ceca183..d4b92663ba 100644 --- a/vendor/github.com/pkg/errors/.travis.yml +++ b/vendor/github.com/pkg/errors/.travis.yml @@ -1,10 +1,14 @@ language: go go_import_path: github.com/pkg/errors go: - - 1.4.3 - - 1.5.4 - - 1.6.2 - - 1.7.1 + - 1.4.x + - 1.5.x + - 1.6.x + - 1.7.x + - 1.8.x + - 1.9.x + - 1.10.x + - 1.11.x - tip script: diff --git a/vendor/github.com/pkg/errors/README.md b/vendor/github.com/pkg/errors/README.md index 273db3c98a..6483ba2afb 100644 --- a/vendor/github.com/pkg/errors/README.md +++ b/vendor/github.com/pkg/errors/README.md @@ -1,4 +1,4 @@ -# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) +# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) [![Sourcegraph](https://sourcegraph.com/github.com/pkg/errors/-/badge.svg)](https://sourcegraph.com/github.com/pkg/errors?badge) Package errors provides simple error handling primitives. @@ -47,6 +47,6 @@ We welcome pull requests, bug fixes and issue reports. With that said, the bar f Before proposing a change, please discuss your change by raising an issue. -## Licence +## License BSD-2-Clause diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go index 842ee80456..7421f326ff 100644 --- a/vendor/github.com/pkg/errors/errors.go +++ b/vendor/github.com/pkg/errors/errors.go @@ -6,7 +6,7 @@ // return err // } // -// which applied recursively up the call stack results in error reports +// which when applied recursively up the call stack results in error reports // without context or debugging information. The errors package allows // programmers to add context to the failure path in their code in a way // that does not destroy the original value of the error. @@ -15,16 +15,17 @@ // // The errors.Wrap function returns a new error that adds context to the // original error by recording a stack trace at the point Wrap is called, -// and the supplied message. For example +// together with the supplied message. For example // // _, err := ioutil.ReadAll(r) // if err != nil { // return errors.Wrap(err, "read failed") // } // -// If additional control is required the errors.WithStack and errors.WithMessage -// functions destructure errors.Wrap into its component operations of annotating -// an error with a stack trace and an a message, respectively. +// If additional control is required, the errors.WithStack and +// errors.WithMessage functions destructure errors.Wrap into its component +// operations: annotating an error with a stack trace and with a message, +// respectively. // // Retrieving the cause of an error // @@ -38,7 +39,7 @@ // } // // can be inspected by errors.Cause. errors.Cause will recursively retrieve -// the topmost error which does not implement causer, which is assumed to be +// the topmost error that does not implement causer, which is assumed to be // the original cause. For example: // // switch err := errors.Cause(err).(type) { @@ -48,16 +49,16 @@ // // unknown error // } // -// causer interface is not exported by this package, but is considered a part -// of stable public API. +// Although the causer interface is not exported by this package, it is +// considered a part of its stable public interface. // // Formatted printing of errors // // All error values returned from this package implement fmt.Formatter and can -// be formatted by the fmt package. The following verbs are supported +// be formatted by the fmt package. The following verbs are supported: // // %s print the error. If the error has a Cause it will be -// printed recursively +// printed recursively. // %v see %s // %+v extended format. Each Frame of the error's StackTrace will // be printed in detail. @@ -65,13 +66,13 @@ // Retrieving the stack trace of an error or wrapper // // New, Errorf, Wrap, and Wrapf record a stack trace at the point they are -// invoked. This information can be retrieved with the following interface. +// invoked. This information can be retrieved with the following interface: // // type stackTracer interface { // StackTrace() errors.StackTrace // } // -// Where errors.StackTrace is defined as +// The returned errors.StackTrace type is defined as // // type StackTrace []Frame // @@ -85,8 +86,8 @@ // } // } // -// stackTracer interface is not exported by this package, but is considered a part -// of stable public API. +// Although the stackTracer interface is not exported by this package, it is +// considered a part of its stable public interface. // // See the documentation for Frame.Format for more details. package errors @@ -192,7 +193,7 @@ func Wrap(err error, message string) error { } // Wrapf returns an error annotating err with a stack trace -// at the point Wrapf is call, and the format specifier. +// at the point Wrapf is called, and the format specifier. // If err is nil, Wrapf returns nil. func Wrapf(err error, format string, args ...interface{}) error { if err == nil { @@ -220,6 +221,18 @@ func WithMessage(err error, message string) error { } } +// WithMessagef annotates err with the format specifier. +// If err is nil, WithMessagef returns nil. +func WithMessagef(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } +} + type withMessage struct { cause error msg string diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go index 6b1f2891a5..2874a048cf 100644 --- a/vendor/github.com/pkg/errors/stack.go +++ b/vendor/github.com/pkg/errors/stack.go @@ -46,7 +46,8 @@ func (f Frame) line() int { // // Format accepts flags that alter the printing of some verbs, as follows: // -// %+s path of source file relative to the compile time GOPATH +// %+s function name and path of source file relative to the compile time +// GOPATH separated by \n\t (\n\t) // %+v equivalent to %+s:%d func (f Frame) Format(s fmt.State, verb rune) { switch verb { @@ -79,6 +80,14 @@ func (f Frame) Format(s fmt.State, verb rune) { // StackTrace is stack of Frames from innermost (newest) to outermost (oldest). type StackTrace []Frame +// Format formats the stack of Frames according to the fmt.Formatter interface. +// +// %s lists source files for each Frame in the stack +// %v lists the source file and line number for each Frame in the stack +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+v Prints filename, function, and line number for each Frame in the stack. func (st StackTrace) Format(s fmt.State, verb rune) { switch verb { case 'v': @@ -136,43 +145,3 @@ func funcname(name string) string { i = strings.Index(name, ".") return name[i+1:] } - -func trimGOPATH(name, file string) string { - // Here we want to get the source file path relative to the compile time - // GOPATH. As of Go 1.6.x there is no direct way to know the compiled - // GOPATH at runtime, but we can infer the number of path segments in the - // GOPATH. We note that fn.Name() returns the function name qualified by - // the import path, which does not include the GOPATH. Thus we can trim - // segments from the beginning of the file path until the number of path - // separators remaining is one more than the number of path separators in - // the function name. For example, given: - // - // GOPATH /home/user - // file /home/user/src/pkg/sub/file.go - // fn.Name() pkg/sub.Type.Method - // - // We want to produce: - // - // pkg/sub/file.go - // - // From this we can easily see that fn.Name() has one less path separator - // than our desired output. We count separators from the end of the file - // path until it finds two more than in the function name and then move - // one character forward to preserve the initial path segment without a - // leading separator. - const sep = "/" - goal := strings.Count(name, sep) + 2 - i := len(file) - for n := 0; n < goal; n++ { - i = strings.LastIndex(file[:i], sep) - if i == -1 { - // not enough separators found, set i so that the slice expression - // below leaves file unmodified - i = -len(sep) - break - } - } - // get back to 0 or trim the leading separator - file = file[i+len(sep):] - return file -} From a92f550d50f9cd506109e35c98644f7b1870c249 Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Tue, 14 May 2019 12:19:46 +0000 Subject: [PATCH 5/8] pkg: ansible: add inventory, module, playbook --- pkg/util/ansible/module.go | 68 ++++++++++ pkg/util/ansible/parser.go | 69 ++++++++++ pkg/util/ansible/playbook.go | 211 ++++++++++++++++++++++++++++++ pkg/util/ansible/playbook_test.go | 40 ++++++ pkg/util/ansible/serializable.go | 29 ++++ pkg/util/ansible/session.go | 71 ++++++++++ 6 files changed, 488 insertions(+) create mode 100644 pkg/util/ansible/module.go create mode 100644 pkg/util/ansible/parser.go create mode 100644 pkg/util/ansible/playbook.go create mode 100644 pkg/util/ansible/playbook_test.go create mode 100644 pkg/util/ansible/serializable.go create mode 100644 pkg/util/ansible/session.go diff --git a/pkg/util/ansible/module.go b/pkg/util/ansible/module.go new file mode 100644 index 0000000000..fb71cea695 --- /dev/null +++ b/pkg/util/ansible/module.go @@ -0,0 +1,68 @@ +package ansible + +import ( + "bytes" +) + +// Module represents name and args of ansible module to execute +type Module struct { + // Name is ansible module name + Name string + // Args is a list of module arguments in form of key=value + Args []string +} + +// Host represents an ansible host +type Host struct { + // Name denotes the host to operate on + Name string + // Vars is a map recording host vars + Vars map[string]string +} + +// GetVar returns variable value. Second return value will be false if the +// variable does not exist +func (h *Host) GetVar(k string) (v string, exist bool) { + if len(h.Vars) == 0 { + return + } + v, exist = h.Vars[k] + return +} + +// SetVar sets variable k to value v +func (h *Host) SetVar(k, v string) { + if h.Vars == nil { + h.Vars = map[string]string{ + k: v, + } + return + } + h.Vars[k] = v +} + +// Inventory contains a list of ansible hosts +type Inventory struct { + Hosts []Host +} + +// IsEmpty returns true if the inventory is empty +func (i *Inventory) IsEmpty() bool { + return len(i.Hosts) == 0 +} + +// Data returns serialized form of the inventory as represented on disk +func (i *Inventory) Data() []byte { + b := &bytes.Buffer{} + for _, h := range i.Hosts { + b.WriteString(h.Name) + for k, v := range h.Vars { + b.WriteRune(' ') + b.WriteString(k) + b.WriteRune('=') + b.WriteString(v) + } + b.WriteRune('\n') + } + return b.Bytes() +} diff --git a/pkg/util/ansible/parser.go b/pkg/util/ansible/parser.go new file mode 100644 index 0000000000..d76867387b --- /dev/null +++ b/pkg/util/ansible/parser.go @@ -0,0 +1,69 @@ +package ansible + +import ( + "fmt" + "strings" +) + +// TODO +// +// quote parser + +// ParseHostLine parse string representation of an ansible host +// +// Input should be in format "name key=value" +func ParseHostLine(s string) (host Host, err error) { + s = strings.TrimSpace(s) + parts := strings.Split(s, " ") + if len(parts) == 0 { + err = fmt.Errorf("Host name must not be empty") + return + } + host.Name = parts[0] + vars := map[string]string{} + for _, kv := range parts[1:] { + s := strings.SplitN(kv, "=", 2) + if len(s) != 2 { + err = fmt.Errorf("host var not in the form name=val: %s", kv) + return + } + vars[s[0]] = s[1] + } + host.Vars = vars + return +} + +// ParseModuleLine parse string representation of an ansible module task +// +// The input should be in format "name key=value arg0 arg1". The argN form is +// For module "command" and "shell" +func ParseModuleLine(s string) (mod Module, err error) { + s = strings.TrimSpace(s) + parts := strings.Split(s, " ") + if len(parts) == 0 { + err = fmt.Errorf("Module name must not be empty") + return + } + mod.Name = parts[0] + + args := []string{} + command := "" + freeForm := false // command and shell module take free form arguments + for _, part := range parts[1:] { + if freeForm { + command = command + " " + part + continue + } + if strings.Contains(part, "=") { + args = append(args, part) + } else { + freeForm = true + command = command + " " + part + } + } + if command != "" { + args = append(args, command) + } + mod.Args = args + return +} diff --git a/pkg/util/ansible/playbook.go b/pkg/util/ansible/playbook.go new file mode 100644 index 0000000000..c7a6652de6 --- /dev/null +++ b/pkg/util/ansible/playbook.go @@ -0,0 +1,211 @@ +package ansible + +import ( + "bytes" + "context" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + + "github.com/pkg/errors" + + "yunion.io/x/pkg/gotypes" + yerrors "yunion.io/x/pkg/util/errors" +) + +type pbState int + +const ( + pbStateInit pbState = iota + pbStateRunning + pbStateStopped +) + +func (pbs pbState) String() string { + switch pbs { + case pbStateInit: + return "init" + case pbStateRunning: + return "running" + case pbStateStopped: + return "stopped" + } + return "unknown" +} + +type Playbook struct { + Inventory Inventory + Modules []Module + PrivateKey []byte + + tmpdir string + noCleanOnExit bool + stdio *bytes.Buffer + state pbState + stateMux *sync.Mutex +} + +func NewPlaybook() *Playbook { + pb := &Playbook{ + state: pbStateInit, + stateMux: &sync.Mutex{}, + stdio: &bytes.Buffer{}, + } + return pb +} + +func (pb *Playbook) Copy() *Playbook { + pb1 := NewPlaybook() + pb1.Inventory = gotypes.DeepCopy(pb.Inventory).(Inventory) + pb1.Modules = gotypes.DeepCopy(pb.Modules).([]Module) + pb1.PrivateKey = gotypes.DeepCopy(pb.PrivateKey).([]byte) + return pb1 +} + +// CleanOnExit decide whether temporary workdir will be cleaned up after Run +func (pb *Playbook) CleanOnExit(b bool) { + pb.noCleanOnExit = !b +} + +// State returns current state of the playbook +func (pb *Playbook) State() pbState { + pb.stateMux.Lock() + defer pb.stateMux.Unlock() + return pb.state +} + +// Runnable returns whether the playbook is in a state feasible to be run +func (pb *Playbook) Runnable() bool { + return pb.state == pbStateInit +} + +// Running returns whether the playbook is currently running +func (pb *Playbook) Running() bool { + return pb.state == pbStateRunning +} + +// Run runs the playbook +func (pb *Playbook) Run(ctx context.Context) (err error) { + var ( + tmpdir string + ) + + pb.stateMux.Lock() + if pb.state != pbStateInit { + return errors.Errorf("playbook state %s, want %s", + pb.state, pbStateInit) + } + pb.state = pbStateRunning + pb.stateMux.Unlock() + defer func() { + pb.stateMux.Lock() + pb.state = pbStateStopped + pb.stateMux.Unlock() + }() + + if pb.Inventory.IsEmpty() { + return errors.New("empty inventory") + } + + // make tmpdir + tmpdir, err = ioutil.TempDir("", "onecloud-ansible") + if err != nil { + err = errors.WithMessage(err, "making tmp dir") + return + } + pb.tmpdir = tmpdir + defer func() { + if pb.noCleanOnExit { + return + } + if err1 := os.RemoveAll(tmpdir); err1 != nil { + err = errors.WithMessagef(err1, "removing %q", tmpdir) + } + }() + + // write out inventory + inventory := filepath.Join(tmpdir, "inventory") + err = ioutil.WriteFile(inventory, pb.Inventory.Data(), os.FileMode(0600)) + if err != nil { + err = errors.WithMessagef(err, "writing inventory %s", inventory) + return + } + + // write out private key + var privateKey string + if len(pb.PrivateKey) > 0 { + privateKey = filepath.Join(tmpdir, "private_key") + err = ioutil.WriteFile(privateKey, pb.PrivateKey, os.FileMode(0600)) + if err != nil { + err = errors.WithMessagef(err, "writing private key %s", privateKey) + return + } + } + + // run modules one by one + var errs []error + defer func() { + if len(errs) > 0 { + err = yerrors.NewAggregate(errs) + } + }() + for _, m := range pb.Modules { + select { + case <-ctx.Done(): + err = ctx.Err() + return + default: + } + modArgs := strings.Join(m.Args, " ") + args := []string{ + "--inventory", inventory, + "--module-name", m.Name, + "--args", modArgs, + "all", + } + if privateKey != "" { + args = append(args, "--private-key", privateKey) + } + cmd := exec.CommandContext(ctx, "ansible", args...) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "ANSIBLE_HOST_KEY_CHECKING=False") + stdout, _ := cmd.StdoutPipe() + stderr, _ := cmd.StderrPipe() + if err1 := cmd.Start(); err1 != nil { + errs = append(errs, errors.WithMessagef(err1, "run module %q, args %q", m.Name, modArgs)) + return + } + f := func(r io.Reader) { + b := make([]byte, 4096) + for { + n, err := r.Read(b) + if n > 0 { + // Mix stdout, stderr + pb.stdio.Write(b[:n]) + } + if err != nil { + return + } + } + } + go f(stdout) + go f(stderr) + if err1 := cmd.Wait(); err1 != nil { + errs = append(errs, errors.WithMessagef(err1, "wait module %q, args %q", m.Name, modArgs)) + // continue to next + } + } + return nil +} + +// Output returns the stdio output of the playbook +func (pb *Playbook) Output() []byte { + if pb.stdio != nil { + return pb.stdio.Bytes() + } + return nil +} diff --git a/pkg/util/ansible/playbook_test.go b/pkg/util/ansible/playbook_test.go new file mode 100644 index 0000000000..22f6103b99 --- /dev/null +++ b/pkg/util/ansible/playbook_test.go @@ -0,0 +1,40 @@ +package ansible + +import ( + "context" + "os/exec" + "testing" +) + +func skipIfNoAnsible(t *testing.T) { + _, err := exec.LookPath("ansible") + if err != nil { + t.Skipf("looking for ansible: %v", err) + } +} + +func TestPlaybook(t *testing.T) { + skipIfNoAnsible(t) + + pb := NewPlaybook() + pb.Inventory = Inventory{ + Hosts: []Host{ + { + Name: "127.0.0.1", + Vars: map[string]string{ + "ansible_connection": "local", + }, + }, + }, + } + pb.Modules = []Module{ + { + Name: "ping", + }, + } + err := pb.Run(context.TODO()) + if err != nil { + t.Fatalf("not expecting err: %v", err) + } + t.Logf("%s", pb.Output()) +} diff --git a/pkg/util/ansible/serializable.go b/pkg/util/ansible/serializable.go new file mode 100644 index 0000000000..118e1e2a55 --- /dev/null +++ b/pkg/util/ansible/serializable.go @@ -0,0 +1,29 @@ +package ansible + +import ( + "reflect" + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/gotypes" +) + +// String implements gotypes.ISerializable +func (pb *Playbook) String() string { + return jsonutils.Marshal(pb).String() +} + +// IsZero implements gotypes.ISerializable +func (pb *Playbook) IsZero() bool { + if len(pb.Inventory.Hosts) == 0 { + return true + } + if len(pb.Modules) == 0 { + return true + } + return false +} + +func init() { + gotypes.RegisterSerializable(reflect.TypeOf(&Playbook{}), func() gotypes.ISerializable { + return NewPlaybook() + }) +} diff --git a/pkg/util/ansible/session.go b/pkg/util/ansible/session.go new file mode 100644 index 0000000000..b3d7e3c3b4 --- /dev/null +++ b/pkg/util/ansible/session.go @@ -0,0 +1,71 @@ +package ansible + +import ( + "context" +) + +// Session is a container for execution of playbook +type Session struct { + // Ctx is the context under which the playbook will run + Ctx context.Context + // Playbook is the ansible playbook to be run + Playbook *Playbook + + // cancelFunc can be called to cancel the running playbook + cancelFunc context.CancelFunc +} + +// SessionManager manages a collection of keyed sessions +type SessionManager map[string]*Session + +// Has returns true if a session with the specified id exists in the manager +func (sm SessionManager) Has(id string) bool { + _, ok := sm[id] + return ok +} + +// Add adds a Playbook to the manager keyed with the specified id +func (sm SessionManager) Add(id string, pb *Playbook) *Session { + ctx, cancelFunc := context.WithCancel(context.Background()) + session := &Session{ + Playbook: pb, + Ctx: ctx, + cancelFunc: cancelFunc, + } + sm[id] = session + return session +} + +// Remove stops (if appliable) and removes the playbook keyed with the +// specified id from the manager +func (sm SessionManager) Remove(id string) { + sm.Stop(id) + delete(sm, id) +} + +// Run runs the playbook keyed with specified id +func (sm SessionManager) Run(id string) error { + s, ok := sm[id] + if !ok { + return nil + } + return s.Playbook.Run(s.Ctx) +} + +// Stop stops the playbook keyed with specified id +func (sm SessionManager) Stop(id string) { + s, ok := sm[id] + if !ok { + return + } + s.cancelFunc() +} + +// Err returns possible error from sm.Ctx.Err() +func (sm SessionManager) Err(id string) error { + s, ok := sm[id] + if !ok { + return nil + } + return s.Ctx.Err() +} From 881c20e6ae542b0f5442884a7dab8be25f6294b2 Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Mon, 20 May 2019 03:35:42 +0000 Subject: [PATCH 6/8] region: add ansibleplaybooks --- pkg/compute/models/ansibleplaybooks.go | 257 ++++++++++++++++++ .../models/ansibleplaybooks_validator.go | 95 +++++++ pkg/compute/models/initdb.go | 1 + pkg/compute/service/handlers.go | 1 + 4 files changed, 354 insertions(+) create mode 100644 pkg/compute/models/ansibleplaybooks.go create mode 100644 pkg/compute/models/ansibleplaybooks_validator.go diff --git a/pkg/compute/models/ansibleplaybooks.go b/pkg/compute/models/ansibleplaybooks.go new file mode 100644 index 0000000000..b64eecc544 --- /dev/null +++ b/pkg/compute/models/ansibleplaybooks.go @@ -0,0 +1,257 @@ +// Copyright 2019 Yunion +// +// 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 models + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/pkg/errors" + + "yunion.io/x/jsonutils" + "yunion.io/x/log" + "yunion.io/x/sqlchemy" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/compute/sshkeys" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/ansible" +) + +const ( + AnsiblePlaybookStatusInit = "init" + AnsiblePlaybookStatusRunning = "running" + AnsiblePlaybookStatusSucceeded = "succeeded" + AnsiblePlaybookStatusFailed = "failed" + AnsiblePlaybookStatusCanceled = "canceled" + AnsiblePlaybookStatusUnknown = "unknown" +) + +// low priority +// +// - retry times and interval +// - timeout, +// - copied playbook has no added value + +type SAnsiblePlaybook struct { + db.SVirtualResourceBase + + Playbook *ansible.Playbook `nullable:"false" create:"required" get:"user" update:"user"` + Output string `get:"user"` + StartTime time.Time `list:"user"` + EndTime time.Time `list:"user"` +} + +type SAnsiblePlaybookManager struct { + db.SVirtualResourceBaseManager + + sessions ansible.SessionManager + sessionsMux *sync.Mutex +} + +var AnsiblePlaybookManager *SAnsiblePlaybookManager + +func init() { + AnsiblePlaybookManager = &SAnsiblePlaybookManager{ + SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager( + SAnsiblePlaybook{}, + "ansibleplaybooks_tbl", + "ansibleplaybook", + "ansibleplaybooks", + ), + sessions: ansible.SessionManager{}, + sessionsMux: &sync.Mutex{}, + } +} + +func (man *SAnsiblePlaybookManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerProjId string, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { + v := NewAnsiblePlaybookValidator("playbook", userCred) + if err := v.Validate(data); err != nil { + return nil, err + } + data.Set("status", jsonutils.NewString(AnsiblePlaybookStatusInit)) + return man.SVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerProjId, query, data) +} + +func (apb *SAnsiblePlaybook) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerProjId string, query jsonutils.JSONObject, data jsonutils.JSONObject) { + apb.SVirtualResourceBase.PostCreate(ctx, userCred, ownerProjId, query, data) + err := apb.runPlaybook(ctx, userCred) + if err != nil { + log.Errorf("postCreate: runPlaybook: %v", err) + } +} + +func (man *SAnsiblePlaybookManager) InitializeData() error { + pbs := []SAnsiblePlaybook{} + q := AnsiblePlaybookManager.Query() + q = q.Filter(sqlchemy.Equals(q.Field("status"), AnsiblePlaybookStatusRunning)) + if err := db.FetchModelObjects(AnsiblePlaybookManager, q, &pbs); err != nil { + return errors.WithMessage(err, "fetch running playbooks") + } + for i := 0; i < len(pbs); i++ { + pb := &pbs[i] + _, err := db.Update(pb, func() error { + pb.Status = AnsiblePlaybookStatusUnknown + return nil + }) + if err != nil { + log.Errorf("set playbook %s(%s) to unknown state: %v", pb.Name, pb.Id, err) + } + } + return nil +} + +func (apb *SAnsiblePlaybook) ValidateDeleteCondition(ctx context.Context) error { + if apb.Status == AnsiblePlaybookStatusRunning { + return httperrors.NewConflictError("playbook is in running state") + } + return nil +} + +func (apb *SAnsiblePlaybook) ValidateUpdateCondition(ctx context.Context) error { + if apb.Status == AnsiblePlaybookStatusRunning { + return httperrors.NewConflictError("playbook is in running state") + } + return nil +} + +func (apb *SAnsiblePlaybook) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { + pbV := NewAnsiblePlaybookValidator("playbook", userCred) + if err := pbV.Validate(data); err != nil { + return nil, err + } + apb.Playbook = pbV.Playbook // Update as a whole + data.Set("status", jsonutils.NewString(AnsiblePlaybookStatusInit)) + return data, nil +} + +func (apb *SAnsiblePlaybook) PostUpdate(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) { + apb.SVirtualResourceBase.PostUpdate(ctx, userCred, query, data) + err := apb.runPlaybook(ctx, userCred) + if err != nil { + log.Errorf("postUpdate: runPlaybook: %v", err) + } +} + +func (apb *SAnsiblePlaybook) AllowPerformRun(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { + return apb.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, apb, "run") +} + +func (apb *SAnsiblePlaybook) PerformRun(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + err := apb.runPlaybook(ctx, userCred) + if err != nil { + return nil, httperrors.NewConflictError("%s", err.Error()) + } + return nil, nil +} + +func (apb *SAnsiblePlaybook) AllowPerformStop(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool { + return apb.IsOwner(userCred) || db.IsAdminAllowPerform(userCred, apb, "stop") +} + +func (apb *SAnsiblePlaybook) PerformStop(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + err := apb.stopPlaybook(ctx, userCred) + if err != nil { + return nil, httperrors.NewConflictError("%s", err.Error()) + } + return nil, nil +} + +func (apb *SAnsiblePlaybook) runPlaybook(ctx context.Context, userCred mcclient.TokenCredential) error { + man := AnsiblePlaybookManager + man.sessionsMux.Lock() + if man.sessions.Has(apb.Id) { + man.sessionsMux.Unlock() + return fmt.Errorf("playbook is already running") + } + + // init private key + pb := apb.Playbook.Copy() + if userCred.HasSystemAdminPrivilege() { + k, _, err := sshkeys.GetSshAdminKeypair(ctx) + if err != nil { + return errors.WithMessage(err, "get admin ssh key") + } + pb.PrivateKey = []byte(k) + } else { + k, _, err := sshkeys.GetSshProjectKeypair(ctx, userCred.GetTenantId()) + if err != nil { + return errors.WithMessage(err, "get project ssh key") + } + pb.PrivateKey = []byte(k) + } + + man.sessions.Add(apb.Id, pb) + man.sessionsMux.Unlock() + + _, err := db.Update(apb, func() error { + apb.StartTime = time.Now() + apb.EndTime = time.Time{} + apb.Output = "" + apb.Status = AnsiblePlaybookStatusRunning + return nil + }) + if err != nil { + log.Errorf("run playbook: update db failed before run: %v", err) + } + // NOTE host state check? run only on online hosts and running guests, skip others + go func() { + defer func() { + man.sessionsMux.Lock() + defer man.sessionsMux.Unlock() + man.sessions.Remove(apb.Id) + }() + runErr := man.sessions.Run(apb.Id) + + _, err := db.Update(apb, func() error { + err := man.sessions.Err(apb.Id) + if err != nil { + apb.Status = AnsiblePlaybookStatusCanceled + } else if runErr != nil { + apb.Status = AnsiblePlaybookStatusFailed + } else { + apb.Status = AnsiblePlaybookStatusSucceeded + } + apb.EndTime = time.Now() + // truncate to preserve the tail + output := pb.Output() + textMax := 64*1024 - 1 + if len(output) > textMax { + output = output[len(output)-textMax:] + } + apb.Output = string(output) + return nil + }) + if err != nil { + log.Errorf("updating ansible playbook failed: %v", err) + } + }() + return nil +} + +func (apb *SAnsiblePlaybook) stopPlaybook(ctx context.Context, userCred mcclient.TokenCredential) error { + man := AnsiblePlaybookManager + man.sessionsMux.Lock() + defer man.sessionsMux.Unlock() + if !man.sessions.Has(apb.Id) { + return fmt.Errorf("playbook is not running") + } + // the playbook will be removed from session map in runPlaybook() on return from run + man.sessions.Stop(apb.Id) + return nil +} diff --git a/pkg/compute/models/ansibleplaybooks_validator.go b/pkg/compute/models/ansibleplaybooks_validator.go new file mode 100644 index 0000000000..a965ab5515 --- /dev/null +++ b/pkg/compute/models/ansibleplaybooks_validator.go @@ -0,0 +1,95 @@ +// Copyright 2019 Yunion +// +// 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 models + +import ( + "strings" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/util/regutils" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/validators" + "yunion.io/x/onecloud/pkg/httperrors" + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/util/ansible" +) + +type ValidatorAnsiblePlaybook struct { + validators.Validator + Playbook *ansible.Playbook + + userCred mcclient.TokenCredential +} + +func NewAnsiblePlaybookValidator(key string, userCred mcclient.TokenCredential) *ValidatorAnsiblePlaybook { + v := &ValidatorAnsiblePlaybook{ + Validator: validators.Validator{Key: key}, + userCred: userCred, + } + v.SetParent(v) + return v +} + +func (v *ValidatorAnsiblePlaybook) Validate(data *jsonutils.JSONDict) error { + pb := ansible.NewPlaybook() + err := data.Unmarshal(pb, "playbook") + if err != nil { + return httperrors.NewBadRequestError("unmarshaling json: %v", err) + } + hosts := pb.Inventory.Hosts + for i := range hosts { + name := strings.TrimSpace(hosts[i].Name) + if len(name) == 0 { + return httperrors.NewBadRequestError("empty host name") + } + switch { + case regutils.MatchIP4Addr(name): + continue + case strings.HasPrefix(name, "host:"): + name = strings.TrimSpace(name[len("host:"):]) + m, err := db.FetchByIdOrName(HostManager, v.userCred, name) + if err != nil { + return httperrors.NewBadRequestError("cannot find host %s", name) + } + if m.(*SHost).AccessIp == "" { + return httperrors.NewBadRequestError("host %s has no access ip", name) + } + name = m.(*SHost).AccessIp + case strings.HasPrefix(name, "server:"): + name = name[len("server:"):] + fallthrough + default: + name = strings.TrimSpace(name) + m, err := db.FetchByIdOrName(GuestManager, v.userCred, name) + if err != nil { + return httperrors.NewBadRequestError("cannot find guest %s", name) + } + ips := m.(*SGuest).GetPrivateIPs() + if len(ips) == 0 { + return httperrors.NewBadRequestError("guest %s has no private ips", name) + } + name = ips[0] + } + hosts[i].Name = name + if username, _ := hosts[i].GetVar("ansible_user"); username == "" { + hosts[i].SetVar("ansible_user", ansible.PUBLIC_CLOUD_ANSIBLE_USER) + } + } + v.Playbook = pb + pbJson := jsonutils.Marshal(pb) + data.Set("playbook", pbJson) + return nil +} diff --git a/pkg/compute/models/initdb.go b/pkg/compute/models/initdb.go index 29bde0ab4c..63066ff0c7 100644 --- a/pkg/compute/models/initdb.go +++ b/pkg/compute/models/initdb.go @@ -42,6 +42,7 @@ func InitDB() error { LoadbalancerListenerRuleManager, LoadbalancerBackendGroupManager, LoadbalancerBackendManager, + AnsiblePlaybookManager, SchedtagManager, DynamicschedtagManager, ServerSkuManager, diff --git a/pkg/compute/service/handlers.go b/pkg/compute/service/handlers.go index 769215a941..0bec7f7c6f 100644 --- a/pkg/compute/service/handlers.go +++ b/pkg/compute/service/handlers.go @@ -93,6 +93,7 @@ func InitHandlers(app *appsrv.Application) { models.LoadbalancerAclManager, models.LoadbalancerAgentManager, models.RouteTableManager, + models.AnsiblePlaybookManager, models.SchedpolicyManager, models.DynamicschedtagManager, From ac3b9bc8495a9cf5b075234f23aa58e7a6488291 Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Tue, 21 May 2019 16:00:16 +0000 Subject: [PATCH 7/8] climc: add ansibleplaybook-xxx commands --- cmd/climc/shell/ansibleplaybooks.go | 108 +++++++++++++++++++ pkg/mcclient/modules/mod_ansibleplaybooks.go | 41 +++++++ pkg/mcclient/options/ansibleplaybooks.go | 95 ++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 cmd/climc/shell/ansibleplaybooks.go create mode 100644 pkg/mcclient/modules/mod_ansibleplaybooks.go create mode 100644 pkg/mcclient/options/ansibleplaybooks.go diff --git a/cmd/climc/shell/ansibleplaybooks.go b/cmd/climc/shell/ansibleplaybooks.go new file mode 100644 index 0000000000..392a16b123 --- /dev/null +++ b/cmd/climc/shell/ansibleplaybooks.go @@ -0,0 +1,108 @@ +// Copyright 2019 Yunion +// +// 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 shell + +import ( + //"fmt" + //"strings" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/modules" + "yunion.io/x/onecloud/pkg/mcclient/options" +) + +func init() { + printAnsiblePlaybookObject := func(obj jsonutils.JSONObject) { + dict := obj.(*jsonutils.JSONDict) + pbJson, err := dict.Get("playbook") + if err != nil { + printObject(obj) + return + } + pbStr := pbJson.YAMLString() + dict.Set("playbook", jsonutils.NewString(pbStr)) + printObject(obj) + } + R(&options.AnsiblePlaybookCreateOptions{}, "ansibleplaybook-create", "Create ansible playbook", func(s *mcclient.ClientSession, opts *options.AnsiblePlaybookCreateOptions) error { + params, err := opts.Params() + if err != nil { + return err + } + apb, err := modules.AnsiblePlaybooks.Create(s, params) + if err != nil { + return err + } + printAnsiblePlaybookObject(apb) + return nil + }) + R(&options.AnsiblePlaybookIdOptions{}, "ansibleplaybook-show", "Show ansible playbook", func(s *mcclient.ClientSession, opts *options.AnsiblePlaybookIdOptions) error { + apb, err := modules.AnsiblePlaybooks.Get(s, opts.ID, nil) + if err != nil { + return err + } + printAnsiblePlaybookObject(apb) + return nil + }) + R(&options.AnsiblePlaybookListOptions{}, "ansibleplaybook-list", "List ansible playbooks", func(s *mcclient.ClientSession, opts *options.AnsiblePlaybookListOptions) error { + params, err := opts.Params() + if err != nil { + return err + } + apbs, err := modules.AnsiblePlaybooks.List(s, params) + if err != nil { + return err + } + printList(apbs, modules.AnsiblePlaybooks.GetColumns(s)) + return nil + }) + R(&options.AnsiblePlaybookIdOptions{}, "ansibleplaybook-delete", "Delete ansible playbook", func(s *mcclient.ClientSession, opts *options.AnsiblePlaybookIdOptions) error { + apb, err := modules.AnsiblePlaybooks.Delete(s, opts.ID, nil) + if err != nil { + return err + } + printAnsiblePlaybookObject(apb) + return nil + }) + R(&options.AnsiblePlaybookUpdateOptions{}, "ansibleplaybook-update", "Update ansible playbook", func(s *mcclient.ClientSession, opts *options.AnsiblePlaybookUpdateOptions) error { + params, err := opts.Params() + if err != nil { + return err + } + apb, err := modules.AnsiblePlaybooks.Update(s, opts.ID, params) + if err != nil { + return err + } + printAnsiblePlaybookObject(apb) + return nil + }) + R(&options.AnsiblePlaybookIdOptions{}, "ansibleplaybook-run", "Run ansible playbook", func(s *mcclient.ClientSession, opts *options.AnsiblePlaybookIdOptions) error { + apb, err := modules.AnsiblePlaybooks.PerformAction(s, opts.ID, "run", nil) + if err != nil { + return err + } + printAnsiblePlaybookObject(apb) + return nil + }) + R(&options.AnsiblePlaybookIdOptions{}, "ansibleplaybook-stop", "Stop ansible playbook", func(s *mcclient.ClientSession, opts *options.AnsiblePlaybookIdOptions) error { + apb, err := modules.AnsiblePlaybooks.PerformAction(s, opts.ID, "stop", nil) + if err != nil { + return err + } + printAnsiblePlaybookObject(apb) + return nil + }) +} diff --git a/pkg/mcclient/modules/mod_ansibleplaybooks.go b/pkg/mcclient/modules/mod_ansibleplaybooks.go new file mode 100644 index 0000000000..80604a846c --- /dev/null +++ b/pkg/mcclient/modules/mod_ansibleplaybooks.go @@ -0,0 +1,41 @@ +// Copyright 2019 Yunion +// +// 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 modules + +type AnsiblePlaybookManager struct { + ResourceManager +} + +var ( + AnsiblePlaybooks AnsiblePlaybookManager +) + +func init() { + AnsiblePlaybooks = AnsiblePlaybookManager{ + NewComputeManager( + "ansibleplaybook", + "ansibleplaybooks", + []string{ + "id", + "name", + "status", + "start_time", + "end_time", + }, + []string{}, + ), + } + registerCompute(&AnsiblePlaybooks) +} diff --git a/pkg/mcclient/options/ansibleplaybooks.go b/pkg/mcclient/options/ansibleplaybooks.go new file mode 100644 index 0000000000..e5cb7b35f7 --- /dev/null +++ b/pkg/mcclient/options/ansibleplaybooks.go @@ -0,0 +1,95 @@ +// Copyright 2019 Yunion +// +// 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 options + +import ( + "fmt" + + "yunion.io/x/jsonutils" + + "yunion.io/x/onecloud/pkg/util/ansible" +) + +type AnsiblePlaybookIdOptions struct { + ID string `help:"name/id of the playbook"` +} + +type AnsiblePlaybookListOptions struct { + BaseListOptions +} + +type AnsiblePlaybookCommonOptions struct { + Host []string `help:"name or id of server or host in format '<[server:]id|host:id>|ipaddr var=val'"` + Mod []string `help:"ansible modules and their arguments in format 'name k1=v1 k2=v2'"` +} + +func (opts *AnsiblePlaybookCommonOptions) params() (jsonutils.JSONObject, error) { + if len(opts.Mod) == 0 { + return nil, fmt.Errorf("Requires at least one --mod argument") + } + if len(opts.Host) == 0 { + return nil, fmt.Errorf("Requires at least one server/host to operate on") + } + pb := ansible.NewPlaybook() + hosts := []ansible.Host{} + for _, s := range opts.Host { + host, err := ansible.ParseHostLine(s) + if err != nil { + return nil, err + } + hosts = append(hosts, host) + } + pb.Inventory = ansible.Inventory{Hosts: hosts} + for _, s := range opts.Mod { + module, err := ansible.ParseModuleLine(s) + if err != nil { + return nil, err + } + pb.Modules = append(pb.Modules, module) + } + pbJson := jsonutils.Marshal(pb) + return pbJson, nil +} + +type AnsiblePlaybookCreateOptions struct { + NAME string `help:"name of the playbook"` + AnsiblePlaybookCommonOptions +} + +func (opts *AnsiblePlaybookCreateOptions) Params() (*jsonutils.JSONDict, error) { + pbJson, err := opts.AnsiblePlaybookCommonOptions.params() + if err != nil { + return nil, err + } + params := jsonutils.NewDict() + params.Set("playbook", pbJson) + params.Set("name", jsonutils.NewString(opts.NAME)) + return params, nil +} + +type AnsiblePlaybookUpdateOptions struct { + ID string `json:"-" help:"name/id of the playbook"` + AnsiblePlaybookCommonOptions +} + +func (opts *AnsiblePlaybookUpdateOptions) Params() (*jsonutils.JSONDict, error) { + pbJson, err := opts.AnsiblePlaybookCommonOptions.params() + if err != nil { + return nil, err + } + params := jsonutils.NewDict() + params.Set("playbook", pbJson) + return params, nil +} From 966694389cd183e641ec6ea8ab034b17ca045d4d Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Sun, 9 Jun 2019 14:08:13 +0000 Subject: [PATCH 8/8] ansibleserver: as a separate service --- cmd/ansibleserver/main.go | 9 + .../models/ansibleplaybooks.go | 8 +- .../models/ansibleplaybooks_validator.go | 64 +++++-- pkg/ansibleserver/models/initdb.go | 38 +++++ pkg/ansibleserver/options/options.go | 26 +++ pkg/ansibleserver/service/handlers.go | 34 ++++ pkg/ansibleserver/service/service.go | 59 +++++++ pkg/compute/models/initdb.go | 1 - pkg/compute/service/handlers.go | 1 - pkg/mcclient/models/base.go | 6 + pkg/mcclient/models/hosts.go | 60 +++++++ pkg/mcclient/models/servers.go | 158 ++++++++++++++++++ pkg/mcclient/modules/managers.go | 8 + pkg/mcclient/modules/mod_ansibleplaybooks.go | 4 +- 14 files changed, 453 insertions(+), 23 deletions(-) create mode 100644 cmd/ansibleserver/main.go rename pkg/{compute => ansibleserver}/models/ansibleplaybooks.go (95%) rename pkg/{compute => ansibleserver}/models/ansibleplaybooks_validator.go (54%) create mode 100644 pkg/ansibleserver/models/initdb.go create mode 100644 pkg/ansibleserver/options/options.go create mode 100644 pkg/ansibleserver/service/handlers.go create mode 100644 pkg/ansibleserver/service/service.go create mode 100644 pkg/mcclient/models/hosts.go create mode 100644 pkg/mcclient/models/servers.go diff --git a/cmd/ansibleserver/main.go b/cmd/ansibleserver/main.go new file mode 100644 index 0000000000..0d033302ab --- /dev/null +++ b/cmd/ansibleserver/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "yunion.io/x/onecloud/pkg/ansibleserver/service" +) + +func main() { + service.StartService() +} diff --git a/pkg/compute/models/ansibleplaybooks.go b/pkg/ansibleserver/models/ansibleplaybooks.go similarity index 95% rename from pkg/compute/models/ansibleplaybooks.go rename to pkg/ansibleserver/models/ansibleplaybooks.go index b64eecc544..271f2844fd 100644 --- a/pkg/compute/models/ansibleplaybooks.go +++ b/pkg/ansibleserver/models/ansibleplaybooks.go @@ -79,17 +79,17 @@ func init() { } } -func (man *SAnsiblePlaybookManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerProjId string, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { +func (man *SAnsiblePlaybookManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { v := NewAnsiblePlaybookValidator("playbook", userCred) if err := v.Validate(data); err != nil { return nil, err } data.Set("status", jsonutils.NewString(AnsiblePlaybookStatusInit)) - return man.SVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerProjId, query, data) + return man.SVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, data) } -func (apb *SAnsiblePlaybook) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerProjId string, query jsonutils.JSONObject, data jsonutils.JSONObject) { - apb.SVirtualResourceBase.PostCreate(ctx, userCred, ownerProjId, query, data) +func (apb *SAnsiblePlaybook) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) { + apb.SVirtualResourceBase.PostCreate(ctx, userCred, ownerId, query, data) err := apb.runPlaybook(ctx, userCred) if err != nil { log.Errorf("postCreate: runPlaybook: %v", err) diff --git a/pkg/compute/models/ansibleplaybooks_validator.go b/pkg/ansibleserver/models/ansibleplaybooks_validator.go similarity index 54% rename from pkg/compute/models/ansibleplaybooks_validator.go rename to pkg/ansibleserver/models/ansibleplaybooks_validator.go index a965ab5515..c7e7b9c7fc 100644 --- a/pkg/compute/models/ansibleplaybooks_validator.go +++ b/pkg/ansibleserver/models/ansibleplaybooks_validator.go @@ -15,15 +15,18 @@ package models import ( + "context" "strings" "yunion.io/x/jsonutils" "yunion.io/x/pkg/util/regutils" - "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/validators" "yunion.io/x/onecloud/pkg/httperrors" - "yunion.io/x/onecloud/pkg/mcclient" + mcclient "yunion.io/x/onecloud/pkg/mcclient" + mcclient_auth "yunion.io/x/onecloud/pkg/mcclient/auth" + mcclient_models "yunion.io/x/onecloud/pkg/mcclient/models" + mcclient_modules "yunion.io/x/onecloud/pkg/mcclient/modules" "yunion.io/x/onecloud/pkg/util/ansible" ) @@ -59,29 +62,22 @@ func (v *ValidatorAnsiblePlaybook) Validate(data *jsonutils.JSONDict) error { case regutils.MatchIP4Addr(name): continue case strings.HasPrefix(name, "host:"): + var err error name = strings.TrimSpace(name[len("host:"):]) - m, err := db.FetchByIdOrName(HostManager, v.userCred, name) + name, err = v.getHostAccessIp(name) if err != nil { - return httperrors.NewBadRequestError("cannot find host %s", name) + return err } - if m.(*SHost).AccessIp == "" { - return httperrors.NewBadRequestError("host %s has no access ip", name) - } - name = m.(*SHost).AccessIp case strings.HasPrefix(name, "server:"): name = name[len("server:"):] fallthrough default: + var err error name = strings.TrimSpace(name) - m, err := db.FetchByIdOrName(GuestManager, v.userCred, name) + name, err = v.getServerIp(name) if err != nil { - return httperrors.NewBadRequestError("cannot find guest %s", name) + return err } - ips := m.(*SGuest).GetPrivateIPs() - if len(ips) == 0 { - return httperrors.NewBadRequestError("guest %s has no private ips", name) - } - name = ips[0] } hosts[i].Name = name if username, _ := hosts[i].GetVar("ansible_user"); username == "" { @@ -93,3 +89,41 @@ func (v *ValidatorAnsiblePlaybook) Validate(data *jsonutils.JSONDict) error { data.Set("playbook", pbJson) return nil } + +func (v *ValidatorAnsiblePlaybook) getHostAccessIp(name string) (string, error) { + sess := mcclient_auth.GetSession(context.Background(), v.userCred, "", "") + hostJson, err := mcclient_modules.Hosts.Get(sess, name, nil) + if err != nil { + return "", httperrors.NewBadRequestError("cannot find host %s", name) + } + host := &mcclient_models.Host{} + if err := hostJson.Unmarshal(host); err != nil { + return "", httperrors.NewBadRequestError("unmarshal host %s: %v", name, err) + } + if host.AccessIp == "" { + return "", httperrors.NewBadRequestError("host %s has no access ip", name) + } + return host.AccessIp, nil +} + +func (v *ValidatorAnsiblePlaybook) getServerIp(name string) (string, error) { + sess := mcclient_auth.GetSession(context.Background(), v.userCred, "", "") + serverJson, err := mcclient_modules.Servers.Get(sess, name, nil) + if err != nil { + return "", httperrors.NewBadRequestError("find server %s: %v", name, err) + } + server := &mcclient_models.Server{} + if err := serverJson.Unmarshal(server); err != nil { + return "", httperrors.NewBadRequestError("unmarshal server %s: %v", name, err) + } + serverNetworks, err := mcclient_models.ParseServerNetworkDetailedString(server.Networks) + if err != nil { + return "", httperrors.NewConflictError("parse networks of %s: %v", name, err) + } + ips := serverNetworks.GetPrivateIPs() + if len(ips) == 0 { + return "", httperrors.NewBadRequestError("server %s has no private ips", name) + } + name = ips[0].String() + return name, nil +} diff --git a/pkg/ansibleserver/models/initdb.go b/pkg/ansibleserver/models/initdb.go new file mode 100644 index 0000000000..f937600d8e --- /dev/null +++ b/pkg/ansibleserver/models/initdb.go @@ -0,0 +1,38 @@ +// Copyright 2019 Yunion +// +// 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 models + +import ( + "yunion.io/x/log" + + "yunion.io/x/onecloud/pkg/cloudcommon/db" +) + +func InitDB() error { + for _, manager := range []db.IModelManager{ + /* + * Important!!! + * initialization order matters, do not change the order + */ + AnsiblePlaybookManager, + } { + err := manager.InitializeData() + if err != nil { + log.Errorf("Manager %s initializeData fail %s", manager.Keyword(), err) + // return err skip error table + } + } + return nil +} diff --git a/pkg/ansibleserver/options/options.go b/pkg/ansibleserver/options/options.go new file mode 100644 index 0000000000..3e6d59e6f1 --- /dev/null +++ b/pkg/ansibleserver/options/options.go @@ -0,0 +1,26 @@ +// Copyright 2019 Yunion +// +// 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 options + +import common_options "yunion.io/x/onecloud/pkg/cloudcommon/options" + +type AnsibleServerOptions struct { + common_options.CommonOptions + common_options.DBOptions +} + +var ( + Options AnsibleServerOptions +) diff --git a/pkg/ansibleserver/service/handlers.go b/pkg/ansibleserver/service/handlers.go new file mode 100644 index 0000000000..c163eb7676 --- /dev/null +++ b/pkg/ansibleserver/service/handlers.go @@ -0,0 +1,34 @@ +package service + +// Copyright 2019 Yunion +// +// 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. + +import ( + "yunion.io/x/onecloud/pkg/ansibleserver/models" + "yunion.io/x/onecloud/pkg/appsrv" + "yunion.io/x/onecloud/pkg/appsrv/dispatcher" + "yunion.io/x/onecloud/pkg/cloudcommon/db" +) + +func InitHandlers(app *appsrv.Application) { + db.InitAllManagers() + + for _, manager := range []db.IModelManager{ + models.AnsiblePlaybookManager, + } { + db.RegisterModelManager(manager) + handler := db.NewModelHandler(manager) + dispatcher.AddModelDispatcher("", app, handler) + } +} diff --git a/pkg/ansibleserver/service/service.go b/pkg/ansibleserver/service/service.go new file mode 100644 index 0000000000..e0c3a287c3 --- /dev/null +++ b/pkg/ansibleserver/service/service.go @@ -0,0 +1,59 @@ +// Copyright 2019 Yunion +// +// 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 service + +import ( + "os" + + _ "github.com/go-sql-driver/mysql" + + "yunion.io/x/log" + + "yunion.io/x/onecloud/pkg/ansibleserver/models" + "yunion.io/x/onecloud/pkg/ansibleserver/options" + "yunion.io/x/onecloud/pkg/cloudcommon" + common_app "yunion.io/x/onecloud/pkg/cloudcommon/app" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + common_options "yunion.io/x/onecloud/pkg/cloudcommon/options" +) + +func StartService() { + opts := &options.Options + common_options.ParseOptions(opts, os.Args, "ansibleserver.conf", "ansibleserver") + + commonOpts := &opts.CommonOptions + common_app.InitAuth(commonOpts, func() { + log.Infof("Auth complete") + }) + + dbOpts := &opts.DBOptions + cloudcommon.InitDB(dbOpts) + defer cloudcommon.CloseDB() + + baseOpts := &opts.BaseOptions + app := common_app.InitApp(baseOpts, false) + InitHandlers(app) + + if !db.CheckSync(opts.AutoSyncTable) { + log.Fatalf("database schema not in sync!") + } + + err := models.InitDB() + if err != nil { + log.Errorf("InitDB fail: %s", err) + } + + common_app.ServeForever(app, baseOpts) +} diff --git a/pkg/compute/models/initdb.go b/pkg/compute/models/initdb.go index 63066ff0c7..29bde0ab4c 100644 --- a/pkg/compute/models/initdb.go +++ b/pkg/compute/models/initdb.go @@ -42,7 +42,6 @@ func InitDB() error { LoadbalancerListenerRuleManager, LoadbalancerBackendGroupManager, LoadbalancerBackendManager, - AnsiblePlaybookManager, SchedtagManager, DynamicschedtagManager, ServerSkuManager, diff --git a/pkg/compute/service/handlers.go b/pkg/compute/service/handlers.go index 0bec7f7c6f..769215a941 100644 --- a/pkg/compute/service/handlers.go +++ b/pkg/compute/service/handlers.go @@ -93,7 +93,6 @@ func InitHandlers(app *appsrv.Application) { models.LoadbalancerAclManager, models.LoadbalancerAgentManager, models.RouteTableManager, - models.AnsiblePlaybookManager, models.SchedpolicyManager, models.DynamicschedtagManager, diff --git a/pkg/mcclient/models/base.go b/pkg/mcclient/models/base.go index c88807f4f5..8af73396f7 100644 --- a/pkg/mcclient/models/base.go +++ b/pkg/mcclient/models/base.go @@ -69,6 +69,12 @@ type StatusStandaloneResource struct { Status string } +type EnabledStatusStandaloneResourceBase struct { + StatusStandaloneResource + + Enabled bool +} + type VirtualResource struct { StatusStandaloneResource diff --git a/pkg/mcclient/models/hosts.go b/pkg/mcclient/models/hosts.go new file mode 100644 index 0000000000..03351f60f1 --- /dev/null +++ b/pkg/mcclient/models/hosts.go @@ -0,0 +1,60 @@ +package models + +import ( + "time" + + "yunion.io/x/jsonutils" +) + +type Host struct { + EnabledStatusStandaloneResourceBase + + Rack string + Slots string + + AccessMac string + AccessIp string + ManagerUri string + + SysInfo jsonutils.JSONObject + SN string + + CpuCount int + NodeCount int8 + CpuDesc string + CpuMhz int + CpuCache int + CpuReserved int + CpuCmtbound float32 + + MemSize int + MemReserved int + MemCmtbound float32 + + StorageSize int + StorageType string + StorageDriver string + StorageInfo jsonutils.JSONObject + + IpmiInfo jsonutils.JSONObject + + HostStatus string + + ZoneId string + + HostType string + + Version string + + IsBaremetal bool + + IsMaintenance bool + + LastPingAt time.Time + + ResourceType string + + RealExternalId string + + IsImport bool +} diff --git a/pkg/mcclient/models/servers.go b/pkg/mcclient/models/servers.go new file mode 100644 index 0000000000..b7dff52401 --- /dev/null +++ b/pkg/mcclient/models/servers.go @@ -0,0 +1,158 @@ +package models + +import ( + "fmt" + "net" + "strconv" + "strings" + + "github.com/pkg/errors" + + "yunion.io/x/pkg/tristate" + "yunion.io/x/pkg/util/netutils" +) + +type Server struct { + VirtualResource + + VcpuCount int + VmemSize int + + BootOrder string + + DisableDelete tristate.TriState + ShutdownBehavior string + + KeypairId string + + HostId string + BackupHostId string + + Vga string + Vdi string + Machine string + Bios string + OsType string + + FlavorId string + + SecgrpId string + AdminSecgrpId string + + Hypervisor string + + InstanceType string + + // Derived attributes + Networks string +} + +type ServerNetworks []ServerNetwork +type ServerNetwork struct { + Index int + Ip net.IP + IpMask int + MacAddr net.HardwareAddr + VlanId int + + Name string + Driver string + Bw int +} + +func (sns ServerNetworks) GetPrivateIPs() []net.IP { + ips := []net.IP{} + for _, sn := range sns { + ipStr := sn.Ip.String() + ipAddr, err := netutils.NewIPV4Addr(ipStr) + if err != nil { + continue + } + if netutils.IsPrivate(ipAddr) { + ips = append(ips, sn.Ip) + } + } + return ips +} + +func ParseServerNetworkDetailedString(s string) (ServerNetworks, error) { + sns := ServerNetworks{} + lines := strings.Split(s, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + if !strings.HasPrefix(line, "eth") { + return nil, errors.New("should be prefixed with 'eth'") + } + parts := strings.Split(line, "/") + if len(parts) < 7 { + return nil, errors.New("less than 7 parts separated by '/'") + } + sn := ServerNetwork{ + Name: parts[4], + Driver: parts[5], + } + { // index, ipaddr + idip := parts[0][3:] + i := strings.IndexByte(idip, ':') + if i <= 0 { + return nil, errors.New("no sep ':'") + } + idStr, ipStr := idip[:i], idip[i+1:] + { + id, err := strconv.ParseUint(idStr, 10, 8) + if err != nil { + return nil, errors.WithMessagef(err, "bad index %s", idStr) + } + sn.Index = int(id) + } + { + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, fmt.Errorf("bad ip %s", ipStr) + } + ip = ip.To4() + if ip == nil { + return nil, fmt.Errorf("not ipv4 addr: %s", ipStr) + } + sn.Ip = ip + } + } + { // ip mask + masklenStr := parts[1] + masklen, err := strconv.ParseUint(masklenStr, 10, 8) + if err != nil { + return nil, errors.WithMessagef(err, "bad network mask len: %s", masklenStr) + } + sn.IpMask = int(masklen) + } + { // macaddr + macaddrStr := parts[2] + macaddr, err := net.ParseMAC(macaddrStr) + if err != nil { + return nil, errors.WithMessagef(err, "bad macaddr: %s", macaddrStr) + } + sn.MacAddr = macaddr + } + { // vlan + vlanStr := parts[3] + vlan, err := strconv.ParseUint(vlanStr, 10, 16) + if err != nil { + return nil, errors.WithMessagef(err, "bad vlan id: %s", vlanStr) + } + sn.VlanId = int(vlan) + } + { // bw + bwStr := parts[6] + bw, err := strconv.ParseUint(bwStr, 10, 32) + if err != nil { + return nil, errors.WithMessagef(err, "bad bw: %s", bwStr) + } + sn.Bw = int(bw) + } + sns = append(sns, sn) + } + return sns, nil +} diff --git a/pkg/mcclient/modules/managers.go b/pkg/mcclient/modules/managers.go index 0885886bed..364763d6b2 100644 --- a/pkg/mcclient/modules/managers.go +++ b/pkg/mcclient/modules/managers.go @@ -192,3 +192,11 @@ func NewCloudmetaManager(keyword, keywordPlural string, columns, adminColumns [] serviceType: "cloudmeta"}, Keyword: keyword, KeywordPlural: keywordPlural} } + +func NewAnsibleManager(keyword, keywordPlural string, columns, adminColumns []string) ResourceManager { + return ResourceManager{ + BaseManager: BaseManager{columns: columns, + adminColumns: adminColumns, + serviceType: "ansible"}, + Keyword: keyword, KeywordPlural: keywordPlural} +} diff --git a/pkg/mcclient/modules/mod_ansibleplaybooks.go b/pkg/mcclient/modules/mod_ansibleplaybooks.go index 80604a846c..d9272144f9 100644 --- a/pkg/mcclient/modules/mod_ansibleplaybooks.go +++ b/pkg/mcclient/modules/mod_ansibleplaybooks.go @@ -24,7 +24,7 @@ var ( func init() { AnsiblePlaybooks = AnsiblePlaybookManager{ - NewComputeManager( + NewAnsibleManager( "ansibleplaybook", "ansibleplaybooks", []string{ @@ -37,5 +37,5 @@ func init() { []string{}, ), } - registerCompute(&AnsiblePlaybooks) + register(&AnsiblePlaybooks) }