diff --git a/pkg/ansibleserver/models/ansibleplaybooks.go b/pkg/ansibleserver/models/ansibleplaybooks.go index 5a89490491..ed08156557 100644 --- a/pkg/ansibleserver/models/ansibleplaybooks.go +++ b/pkg/ansibleserver/models/ansibleplaybooks.go @@ -82,8 +82,8 @@ func init() { } 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 { + pbV := NewAnsiblePlaybookValidator("playbook", userCred) + if err := pbV.Validate(data); err != nil { return nil, err } data.Set("status", jsonutils.NewString(AnsiblePlaybookStatusInit)) @@ -244,6 +244,7 @@ func (apb *SAnsiblePlaybook) runPlaybook(ctx context.Context, userCred mcclient. if err != nil { apb.Status = AnsiblePlaybookStatusCanceled } else if runErr != nil { + log.Warningf("playbook %s(%s) failed: %v", apb.Name, apb.Id, runErr) apb.Status = AnsiblePlaybookStatusFailed } else { apb.Status = AnsiblePlaybookStatusSucceeded diff --git a/pkg/apis/ansible/ansible.go b/pkg/apis/ansible/ansible.go new file mode 100644 index 0000000000..d4090361c4 --- /dev/null +++ b/pkg/apis/ansible/ansible.go @@ -0,0 +1,29 @@ +// 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 ansible + +import ( + "yunion.io/x/onecloud/pkg/apis" + "yunion.io/x/onecloud/pkg/util/ansible" +) + +type AnsiblePlaybookCreateInput struct { + apis.Meta + + Name string + Playbook ansible.Playbook +} + +type AnsiblePlaybookUpdateInput AnsiblePlaybookCreateInput diff --git a/pkg/mcclient/models/ansibleplaybooks.go b/pkg/mcclient/models/ansibleplaybooks.go new file mode 100644 index 0000000000..a2a3a9460f --- /dev/null +++ b/pkg/mcclient/models/ansibleplaybooks.go @@ -0,0 +1,16 @@ +package models + +import ( + "time" + + "yunion.io/x/onecloud/pkg/util/ansible" +) + +type AnsiblePlaybook struct { + VirtualResource + + Playbook *ansible.Playbook + Output string + StartTime time.Time + EndTime time.Time +} diff --git a/pkg/mcclient/options/ansibleplaybooks.go b/pkg/mcclient/options/ansibleplaybooks.go index e5cb7b35f7..642bedd0d4 100644 --- a/pkg/mcclient/options/ansibleplaybooks.go +++ b/pkg/mcclient/options/ansibleplaybooks.go @@ -16,9 +16,12 @@ package options import ( "fmt" + "io/ioutil" + "strings" "yunion.io/x/jsonutils" + apis "yunion.io/x/onecloud/pkg/apis/ansible" "yunion.io/x/onecloud/pkg/util/ansible" ) @@ -33,9 +36,10 @@ type AnsiblePlaybookListOptions struct { 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'"` + File []string `help:"files for use by modules, e.g. name=content, name=@file"` } -func (opts *AnsiblePlaybookCommonOptions) params() (jsonutils.JSONObject, error) { +func (opts *AnsiblePlaybookCommonOptions) ToPlaybook() (*ansible.Playbook, error) { if len(opts.Mod) == 0 { return nil, fmt.Errorf("Requires at least one --mod argument") } @@ -59,8 +63,30 @@ func (opts *AnsiblePlaybookCommonOptions) params() (jsonutils.JSONObject, error) } pb.Modules = append(pb.Modules, module) } - pbJson := jsonutils.Marshal(pb) - return pbJson, nil + files := map[string][]byte{} + for _, s := range opts.File { + i := strings.IndexByte(s, '=') + if i < 0 { + return nil, fmt.Errorf("missing '=' in argument for --file. Read command help") + } + name := strings.TrimSpace(s[:i]) + if name == "" { + return nil, fmt.Errorf("empty file name: %s", s) + } + v := s[i+1:] + if len(v) > 0 && v[0] == '@' { + path := v[1:] + d, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read file %s: %v", path, err) + } + files[name] = d + } else { + files[name] = []byte(v) + } + } + pb.Files = files + return pb, nil } type AnsiblePlaybookCreateOptions struct { @@ -69,27 +95,33 @@ type AnsiblePlaybookCreateOptions struct { } func (opts *AnsiblePlaybookCreateOptions) Params() (*jsonutils.JSONDict, error) { - pbJson, err := opts.AnsiblePlaybookCommonOptions.params() + pb, err := opts.AnsiblePlaybookCommonOptions.ToPlaybook() if err != nil { return nil, err } - params := jsonutils.NewDict() - params.Set("playbook", pbJson) - params.Set("name", jsonutils.NewString(opts.NAME)) + input := &apis.AnsiblePlaybookCreateInput{ + Name: opts.NAME, + Playbook: *pb, + } + params := input.JSON(input) return params, nil } type AnsiblePlaybookUpdateOptions struct { - ID string `json:"-" help:"name/id of the playbook"` + ID string `json:"-" help:"name/id of the playbook"` + Name string AnsiblePlaybookCommonOptions } func (opts *AnsiblePlaybookUpdateOptions) Params() (*jsonutils.JSONDict, error) { - pbJson, err := opts.AnsiblePlaybookCommonOptions.params() + pb, err := opts.AnsiblePlaybookCommonOptions.ToPlaybook() if err != nil { return nil, err } - params := jsonutils.NewDict() - params.Set("playbook", pbJson) + input := &apis.AnsiblePlaybookUpdateInput{ + Name: opts.Name, + Playbook: *pb, + } + params := input.JSON(input) return params, nil } diff --git a/pkg/util/ansible/playbook.go b/pkg/util/ansible/playbook.go index b6de3ddb67..83a57213b1 100644 --- a/pkg/util/ansible/playbook.go +++ b/pkg/util/ansible/playbook.go @@ -55,6 +55,7 @@ type Playbook struct { Inventory Inventory Modules []Module PrivateKey []byte + Files map[string][]byte tmpdir string noCleanOnExit bool @@ -77,6 +78,7 @@ func (pb *Playbook) Copy() *Playbook { pb1.Inventory = gotypes.DeepCopy(pb.Inventory).(Inventory) pb1.Modules = gotypes.DeepCopy(pb.Modules).([]Module) pb1.PrivateKey = gotypes.DeepCopy(pb.PrivateKey).([]byte) + pb1.Files = gotypes.DeepCopy(pb.Files).(map[string][]byte) return pb1 } @@ -160,6 +162,22 @@ func (pb *Playbook) Run(ctx context.Context) (err error) { } } + // write out files + for name, content := range pb.Files { + path := filepath.Join(tmpdir, "files", name) + dir := filepath.Dir(path) + err = os.MkdirAll(dir, os.FileMode(0700)) + if err != nil { + err = errors.WithMessagef(err, "mkdir -p %s", dir) + return + } + err = ioutil.WriteFile(path, content, os.FileMode(0600)) + if err != nil { + err = errors.WithMessagef(err, "writing file %s", name) + return + } + } + // run modules one by one var errs []error defer func() { @@ -185,6 +203,7 @@ func (pb *Playbook) Run(ctx context.Context) (err error) { args = append(args, "--private-key", privateKey) } cmd := exec.CommandContext(ctx, "ansible", args...) + cmd.Dir = pb.tmpdir cmd.Env = os.Environ() cmd.Env = append(cmd.Env, "ANSIBLE_HOST_KEY_CHECKING=False") stdout, _ := cmd.StdoutPipe() diff --git a/pkg/util/ansible/playbook_test.go b/pkg/util/ansible/playbook_test.go index f859dcd219..d0ebcc3616 100644 --- a/pkg/util/ansible/playbook_test.go +++ b/pkg/util/ansible/playbook_test.go @@ -17,6 +17,7 @@ package ansible import ( "context" "os/exec" + "reflect" "testing" ) @@ -45,10 +46,37 @@ func TestPlaybook(t *testing.T) { { Name: "ping", }, + { + Name: "copy", + Args: []string{ + "src=afile", + "dest=/tmp/afile", + }, + }, + { + Name: "copy", + Args: []string{ + "src=adir/afile", + "dest=/tmp/adirfile", + }, + }, } - err := pb.Run(context.TODO()) - if err != nil { - t.Fatalf("not expecting err: %v", err) + pb.Files = map[string][]byte{ + "afile": []byte("afilecontent"), + "adir/afile": []byte("afilecontent under adir"), } - t.Logf("%s", pb.Output()) + + t.Run("copy", func(t *testing.T) { + pb2 := pb.Copy() + if !reflect.DeepEqual(pb2, pb) { + t.Errorf("copy and the original should be equal") + } + }) + t.Run("run", func(t *testing.T) { + err := pb.Run(context.TODO()) + t.Logf("%s", pb.Output()) + if err != nil { + t.Fatalf("not expecting err: %v", err) + } + }) }