From 4797d8cdcf175e681aeb0c890edec7e9404a1d99 Mon Sep 17 00:00:00 2001 From: Billie Cleek Date: Sun, 25 Sep 2016 15:57:02 -0700 Subject: [PATCH] provisioner/ansible: assume scp target is file Assume the scp target is a file instead of a directory. Assuming the scp target is a file instead of a directory allows uploading files to a node being provisioned with the ssh communciator using sftp and with the winrm communicator. It is fully compatible with ansible; ansible communicators only allow for files to be uploaded (when the copy module is used to upload a directory, ansible walks the directory and uploads files one at a time). Update docuemntation to explain how to provision a Windows image. Extend tests that use ssh to communicate with the node to include single files, recursive copies, and content-only recursive copies. Add test to verify support for the winrm communicator. Remove the err argument from adapter.scpExec, because it was unused. Fixes #3911 --- provisioner/ansible/adapter.go | 12 +- provisioner/ansible/scp.go | 29 +- .../provisioner-ansible/all_options.json | 2 +- .../connection_plugins/packer.py | 16 + .../dir/contents-only/file.txt | 1 + .../provisioner-ansible/dir/subdir/file.txt | 1 + .../fixtures/provisioner-ansible/playbook.yml | 26 +- .../provisioner-ansible/scp-to-sftp.json | 24 + test/fixtures/provisioner-ansible/scp.json | 4 +- .../provisioner-ansible/win-playbook.yml | 17 + test/fixtures/provisioner-ansible/winrm.json | 31 ++ test/provisioner_ansible.bats | 16 + vendor/github.com/google/shlex/COPYING | 202 +++++++++ vendor/github.com/google/shlex/README | 2 + vendor/github.com/google/shlex/shlex.go | 417 ++++++++++++++++++ vendor/vendor.json | 6 + .../source/docs/provisioners/ansible.html.md | 57 +++ 17 files changed, 843 insertions(+), 20 deletions(-) create mode 100644 test/fixtures/provisioner-ansible/connection_plugins/packer.py create mode 100644 test/fixtures/provisioner-ansible/dir/contents-only/file.txt create mode 100644 test/fixtures/provisioner-ansible/dir/subdir/file.txt create mode 100644 test/fixtures/provisioner-ansible/scp-to-sftp.json create mode 100644 test/fixtures/provisioner-ansible/win-playbook.yml create mode 100644 test/fixtures/provisioner-ansible/winrm.json create mode 100644 vendor/github.com/google/shlex/COPYING create mode 100644 vendor/github.com/google/shlex/README create mode 100644 vendor/github.com/google/shlex/shlex.go diff --git a/provisioner/ansible/adapter.go b/provisioner/ansible/adapter.go index 2c622ef1df8..eb481abd62a 100644 --- a/provisioner/ansible/adapter.go +++ b/provisioner/ansible/adapter.go @@ -10,6 +10,7 @@ import ( "net" "strings" + "github.com/google/shlex" "github.com/mitchellh/packer/packer" "golang.org/x/crypto/ssh" ) @@ -189,7 +190,7 @@ func (c *adapter) exec(command string, in io.Reader, out io.Writer, err io.Write var exitStatus int switch { case strings.HasPrefix(command, "scp ") && serveSCP(command[4:]): - err := c.scpExec(command[4:], in, out, err) + err := c.scpExec(command[4:], in, out) if err != nil { log.Println(err) exitStatus = 1 @@ -205,9 +206,16 @@ func serveSCP(args string) bool { return bytes.IndexAny(opts, "tf") >= 0 } -func (c *adapter) scpExec(args string, in io.Reader, out io.Writer, err io.Writer) error { +func (c *adapter) scpExec(args string, in io.Reader, out io.Writer) error { opts, rest := scpOptions(args) + // remove the quoting that ansible added to rest for shell safety. + shargs, err := shlex.Split(rest) + if err != nil { + return err + } + rest = strings.Join(shargs, "") + if i := bytes.IndexByte(opts, 't'); i >= 0 { return scpUploadSession(opts, rest, in, out, c.comm) } diff --git a/provisioner/ansible/scp.go b/provisioner/ansible/scp.go index 21ccf18962f..912b4faa715 100644 --- a/provisioner/ansible/scp.go +++ b/provisioner/ansible/scp.go @@ -50,7 +50,12 @@ func scpUploadSession(opts []byte, rest string, in io.Reader, out io.Writer, com } defer os.RemoveAll(d) - state := &scpUploadState{destRoot: rest, srcRoot: d, comm: comm} + // To properly implement scp, rest should be checked to see if it is a + // directory on the remote side, but ansible only sends files, so there's no + // need to set targetIsDir, because it can be safely assumed that rest is + // intended to be a file, and whatever names are used in 'C' commands are + // irrelavant. + state := &scpUploadState{target: rest, srcRoot: d, comm: comm} fmt.Fprintf(out, scpOK) // signal the client to start the transfer. return state.Protocol(bufio.NewReader(in), out) @@ -117,16 +122,17 @@ func (state *scpDownloadState) FileProtocol(path string, info os.FileInfo, in *b } type scpUploadState struct { - comm packer.Communicator - destRoot string // destRoot is the directory on the target - srcRoot string // srcRoot is the directory on the host - mtime time.Time - atime time.Time - dir string // dir is a path relative to the roots + comm packer.Communicator + target string // target is the directory on the target + srcRoot string // srcRoot is the directory on the host + mtime time.Time + atime time.Time + dir string // dir is a path relative to the roots + targetIsDir bool } func (scp scpUploadState) DestPath() string { - return filepath.Join(scp.destRoot, scp.dir) + return filepath.Join(scp.target, scp.dir) } func (scp scpUploadState) SrcPath() string { @@ -177,7 +183,12 @@ func (state *scpUploadState) FileProtocol(in *bufio.Reader, out io.Writer) error var fi os.FileInfo = fileInfo{name: name, size: size, mode: mode, mtime: state.mtime} - err = state.comm.Upload(filepath.Join(state.DestPath(), fi.Name()), io.LimitReader(in, fi.Size()), &fi) + dest := state.DestPath() + if state.targetIsDir { + dest = filepath.Join(dest, fi.Name()) + } + + err = state.comm.Upload(dest, io.LimitReader(in, fi.Size()), &fi) if err != nil { fmt.Fprintf(out, scpEmptyError) return err diff --git a/test/fixtures/provisioner-ansible/all_options.json b/test/fixtures/provisioner-ansible/all_options.json index 4f7e16255df..5313d13c340 100644 --- a/test/fixtures/provisioner-ansible/all_options.json +++ b/test/fixtures/provisioner-ansible/all_options.json @@ -15,7 +15,7 @@ "type": "ansible", "playbook_file": "./playbook.yml", "extra_arguments": [ - "-vvvv", "--private-key", "ansible-test-id" + "--private-key", "ansible-test-id" ], "sftp_command": "/usr/lib/sftp-server -e -l INFO", "use_sftp": true, diff --git a/test/fixtures/provisioner-ansible/connection_plugins/packer.py b/test/fixtures/provisioner-ansible/connection_plugins/packer.py new file mode 100644 index 00000000000..f0bb10da841 --- /dev/null +++ b/test/fixtures/provisioner-ansible/connection_plugins/packer.py @@ -0,0 +1,16 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.plugins.connection.ssh import Connection as SSHConnection + +class Connection(SSHConnection): + ''' ssh based connections for powershell via packer''' + + transport = 'packer' + has_pipelining = True + become_methods = [] + allow_executable = False + module_implementation_preferences = ('.ps1', '') + + def __init__(self, *args, **kwargs): + super(Connection, self).__init__(*args, **kwargs) diff --git a/test/fixtures/provisioner-ansible/dir/contents-only/file.txt b/test/fixtures/provisioner-ansible/dir/contents-only/file.txt new file mode 100644 index 00000000000..426dfd53efb --- /dev/null +++ b/test/fixtures/provisioner-ansible/dir/contents-only/file.txt @@ -0,0 +1 @@ +this file's parent directory should not be transferred to the node. diff --git a/test/fixtures/provisioner-ansible/dir/subdir/file.txt b/test/fixtures/provisioner-ansible/dir/subdir/file.txt new file mode 100644 index 00000000000..faba4c25ddc --- /dev/null +++ b/test/fixtures/provisioner-ansible/dir/subdir/file.txt @@ -0,0 +1 @@ +This file and its parent directory should be transferred to the node. diff --git a/test/fixtures/provisioner-ansible/playbook.yml b/test/fixtures/provisioner-ansible/playbook.yml index b352387c097..d53e56199dc 100644 --- a/test/fixtures/provisioner-ansible/playbook.yml +++ b/test/fixtures/provisioner-ansible/playbook.yml @@ -2,12 +2,26 @@ - hosts: default:packer-test gather_facts: no tasks: - - raw: touch /root/ansible-raw-test - - raw: date - - command: echo "the command module" - - command: mkdir /tmp/remote-dir + - name: touch /root/ansible-raw-test + raw: touch /root/ansible-raw-test + - name: raw test + raw: date + - name: command test + command: echo "the command module" + - name: prepare remote directory + command: mkdir /tmp/remote-dir args: creates: /tmp/remote-dir - - copy: src=dir/file.txt dest=/tmp/remote-dir/file.txt - - fetch: src=/tmp/remote-dir/file.txt dest=fetched-dir validate=yes fail_on_missing=yes + - name: transfer file.txt + copy: src=dir/file.txt dest=/tmp/remote-dir/file.txt + - name: fetch file.text + fetch: src=/tmp/remote-dir/file.txt dest=fetched-dir validate=yes fail_on_missing=yes + - name: copy contents of directory + copy: src=dir/contents-only/ dest=/tmp/remote-dir + - name: fetch contents of directory + fetch: src=/tmp/remote-dir/file.txt dest="fetched-dir/{{ inventory_hostname }}/tmp/remote-dir/contents-only/" flat=yes validate=yes fail_on_missing=yes + - name: copy directory recursively + copy: src=dir/subdir dest=/tmp/remote-dir + - name: fetch recursively copied directory + fetch: src=/tmp/remote-dir/subdir/file.txt dest=fetched-dir validate=yes fail_on_missing=yes - copy: src=largish-file.txt dest=/tmp/largish-file.txt diff --git a/test/fixtures/provisioner-ansible/scp-to-sftp.json b/test/fixtures/provisioner-ansible/scp-to-sftp.json new file mode 100644 index 00000000000..19a386e0cf2 --- /dev/null +++ b/test/fixtures/provisioner-ansible/scp-to-sftp.json @@ -0,0 +1,24 @@ +{ + "variables": {}, + "provisioners": [ + { + "type": "ansible", + "playbook_file": "./playbook.yml", + "extra_arguments": [ + ], + "sftp_command": "/usr/bin/false", + "use_sftp": false + } + ], + "builders": [ + { + "type": "googlecompute", + "account_file": "{{user `account_file`}}", + "project_id": "{{user `project_id`}}", + "image_name": "packerbats-scp-to-sftp-{{timestamp}}", + "source_image": "debian-7-wheezy-v20141108", + "zone": "us-central1-a", + "ssh_file_transfer_method": "sftp" + } + ] +} diff --git a/test/fixtures/provisioner-ansible/scp.json b/test/fixtures/provisioner-ansible/scp.json index b94078f2a36..98ff0ff4c32 100644 --- a/test/fixtures/provisioner-ansible/scp.json +++ b/test/fixtures/provisioner-ansible/scp.json @@ -5,9 +5,9 @@ "type": "ansible", "playbook_file": "./playbook.yml", "extra_arguments": [ - "-vvvv" ], - "sftp_command": "/usr/bin/false" + "sftp_command": "/usr/bin/false", + "use_sftp": false } ], "builders": [ diff --git a/test/fixtures/provisioner-ansible/win-playbook.yml b/test/fixtures/provisioner-ansible/win-playbook.yml new file mode 100644 index 00000000000..45b39b432b0 --- /dev/null +++ b/test/fixtures/provisioner-ansible/win-playbook.yml @@ -0,0 +1,17 @@ +--- +- hosts: default:packer-test + gather_facts: no + tasks: + #- debug: msg="testing regular modules that function with Windows: raw, fetch, slurp, setup" + - name: raw test + raw: date /t + - debug: msg="testing windows modules" + #- win_file: path=tmp/remote-dir state=directory + #- name: win_shell test + #win_shell: date /t + - name: win_copy test + win_copy: src=dir/file.txt dest=file.txt + #- win_copy: src=dir/file.txt dest=/tmp/remote-dir/file.txt + #- fetch: src=/tmp/remote-dir/file.txt dest=fetched-dir validate=yes fail_on_missing=yes + #- win_copy: src=largish-file.txt dest=/tmp/largish-file.txt + - debug: msg="packer does not support downloading from windows" diff --git a/test/fixtures/provisioner-ansible/winrm.json b/test/fixtures/provisioner-ansible/winrm.json new file mode 100644 index 00000000000..979b8b60e50 --- /dev/null +++ b/test/fixtures/provisioner-ansible/winrm.json @@ -0,0 +1,31 @@ +{ + "variables": {}, + "provisioners": [ + { + "type": "ansible", + "playbook_file": "./win-playbook.yml", + "extra_arguments": [ + "--connection", "packer", + "--extra-vars", "ansible_shell_type=powershell ansible_shell_executable=None" + ] + } + ], + "builders": [ + { + "type": "googlecompute", + "account_file": "{{user `account_file`}}", + "project_id": "{{user `project_id`}}", + "image_name": "packerbats-winrm-{{timestamp}}", + "source_image": "windows-server-2012-r2-dc-v20160916", + "communicator": "winrm", + "zone": "us-central1-a", + "disk_size": 50, + "winrm_username": "packer", + "winrm_use_ssl": true, + "winrm_insecure": true, + "metadata": { + "sysprep-specialize-script-cmd": "winrm set winrm/config/service/auth @{Basic=\"true\"}" + } + } + ] +} diff --git a/test/provisioner_ansible.bats b/test/provisioner_ansible.bats index 537435ae650..19620cb1e38 100755 --- a/test/provisioner_ansible.bats +++ b/test/provisioner_ansible.bats @@ -67,6 +67,14 @@ teardown() { diff -r dir fetched-dir/default/tmp/remote-dir > /dev/null } +@test "ansible provisioner: build scp-to-sftp.json" { + cd $FIXTURE_ROOT + run packer build ${USER_VARS} $FIXTURE_ROOT/scp-to-sftp.json + [ "$status" -eq 0 ] + [ "$(gc_has_image "packerbats-scp-to-sftp")" -eq 1 ] + diff -r dir fetched-dir/default/tmp/remote-dir > /dev/null +} + @test "ansible provisioner: build sftp.json" { cd $FIXTURE_ROOT run packer build ${USER_VARS} $FIXTURE_ROOT/sftp.json @@ -75,3 +83,11 @@ teardown() { diff -r dir fetched-dir/default/tmp/remote-dir > /dev/null } +@test "ansible provisioner: build winrm.json" { + cd $FIXTURE_ROOT + run packer build ${USER_VARS} $FIXTURE_ROOT/winrm.json + [ "$status" -eq 0 ] + [ "$(gc_has_image "packerbats-winrm")" -eq 1 ] + echo "packer does not support downloading files from download, skipping verification" + #diff -r dir fetched-dir/default/tmp/remote-dir > /dev/null +} diff --git a/vendor/github.com/google/shlex/COPYING b/vendor/github.com/google/shlex/COPYING new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/vendor/github.com/google/shlex/COPYING @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/github.com/google/shlex/README b/vendor/github.com/google/shlex/README new file mode 100644 index 00000000000..c86bcc066fd --- /dev/null +++ b/vendor/github.com/google/shlex/README @@ -0,0 +1,2 @@ +go-shlex is a simple lexer for go that supports shell-style quoting, +commenting, and escaping. diff --git a/vendor/github.com/google/shlex/shlex.go b/vendor/github.com/google/shlex/shlex.go new file mode 100644 index 00000000000..db4ac8e22e5 --- /dev/null +++ b/vendor/github.com/google/shlex/shlex.go @@ -0,0 +1,417 @@ +/* +Copyright 2012 Google Inc. All Rights Reserved. + +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 shlex implements a simple lexer which splits input in to tokens using +shell-style rules for quoting and commenting. + +The basic use case uses the default ASCII lexer to split a string into sub-strings: + + shlex.Split("one \"two three\" four") -> []string{"one", "two three", "four"} + +To process a stream of strings: + + l := NewLexer(os.Stdin) + for ; token, err := l.Next(); err != nil { + // process token + } + +To access the raw token stream (which includes tokens for comments): + + t := NewTokenizer(os.Stdin) + for ; token, err := t.Next(); err != nil { + // process token + } + +*/ +package shlex + +import ( + "bufio" + "fmt" + "io" + "strings" +) + +// TokenType is a top-level token classification: A word, space, comment, unknown. +type TokenType int + +// runeTokenClass is the type of a UTF-8 character classification: A quote, space, escape. +type runeTokenClass int + +// the internal state used by the lexer state machine +type lexerState int + +// Token is a (type, value) pair representing a lexographical token. +type Token struct { + tokenType TokenType + value string +} + +// Equal reports whether tokens a, and b, are equal. +// Two tokens are equal if both their types and values are equal. A nil token can +// never be equal to another token. +func (a *Token) Equal(b *Token) bool { + if a == nil || b == nil { + return false + } + if a.tokenType != b.tokenType { + return false + } + return a.value == b.value +} + +// Named classes of UTF-8 runes +const ( + spaceRunes = " \t\r\n" + escapingQuoteRunes = `"` + nonEscapingQuoteRunes = "'" + escapeRunes = `\` + commentRunes = "#" +) + +// Classes of rune token +const ( + unknownRuneClass runeTokenClass = iota + spaceRuneClass + escapingQuoteRuneClass + nonEscapingQuoteRuneClass + escapeRuneClass + commentRuneClass + eofRuneClass +) + +// Classes of lexographic token +const ( + UnknownToken TokenType = iota + WordToken + SpaceToken + CommentToken +) + +// Lexer state machine states +const ( + startState lexerState = iota // no runes have been seen + inWordState // processing regular runes in a word + escapingState // we have just consumed an escape rune; the next rune is literal + escapingQuotedState // we have just consumed an escape rune within a quoted string + quotingEscapingState // we are within a quoted string that supports escaping ("...") + quotingState // we are within a string that does not support escaping ('...') + commentState // we are within a comment (everything following an unquoted or unescaped # +) + +// tokenClassifier is used for classifying rune characters. +type tokenClassifier map[rune]runeTokenClass + +func (typeMap tokenClassifier) addRuneClass(runes string, tokenType runeTokenClass) { + for _, runeChar := range runes { + typeMap[runeChar] = tokenType + } +} + +// newDefaultClassifier creates a new classifier for ASCII characters. +func newDefaultClassifier() tokenClassifier { + t := tokenClassifier{} + t.addRuneClass(spaceRunes, spaceRuneClass) + t.addRuneClass(escapingQuoteRunes, escapingQuoteRuneClass) + t.addRuneClass(nonEscapingQuoteRunes, nonEscapingQuoteRuneClass) + t.addRuneClass(escapeRunes, escapeRuneClass) + t.addRuneClass(commentRunes, commentRuneClass) + return t +} + +// ClassifyRune classifiees a rune +func (t tokenClassifier) ClassifyRune(runeVal rune) runeTokenClass { + return t[runeVal] +} + +// Lexer turns an input stream into a sequence of tokens. Whitespace and comments are skipped. +type Lexer Tokenizer + +// NewLexer creates a new lexer from an input stream. +func NewLexer(r io.Reader) *Lexer { + + return (*Lexer)(NewTokenizer(r)) +} + +// Next returns the next word, or an error. If there are no more words, +// the error will be io.EOF. +func (l *Lexer) Next() (string, error) { + for { + token, err := (*Tokenizer)(l).Next() + if err != nil { + return "", err + } + switch token.tokenType { + case WordToken: + return token.value, nil + case CommentToken: + // skip comments + default: + return "", fmt.Errorf("Unknown token type: %v", token.tokenType) + } + } +} + +// Tokenizer turns an input stream into a sequence of typed tokens +type Tokenizer struct { + input bufio.Reader + classifier tokenClassifier +} + +// NewTokenizer creates a new tokenizer from an input stream. +func NewTokenizer(r io.Reader) *Tokenizer { + input := bufio.NewReader(r) + classifier := newDefaultClassifier() + return &Tokenizer{ + input: *input, + classifier: classifier} +} + +// scanStream scans the stream for the next token using the internal state machine. +// It will panic if it encounters a rune which it does not know how to handle. +func (t *Tokenizer) scanStream() (*Token, error) { + state := startState + var tokenType TokenType + var value []rune + var nextRune rune + var nextRuneType runeTokenClass + var err error + + for { + nextRune, _, err = t.input.ReadRune() + nextRuneType = t.classifier.ClassifyRune(nextRune) + + if err == io.EOF { + nextRuneType = eofRuneClass + err = nil + } else if err != nil { + return nil, err + } + + switch state { + case startState: // no runes read yet + { + switch nextRuneType { + case eofRuneClass: + { + return nil, io.EOF + } + case spaceRuneClass: + { + } + case escapingQuoteRuneClass: + { + tokenType = WordToken + state = quotingEscapingState + } + case nonEscapingQuoteRuneClass: + { + tokenType = WordToken + state = quotingState + } + case escapeRuneClass: + { + tokenType = WordToken + state = escapingState + } + case commentRuneClass: + { + tokenType = CommentToken + state = commentState + } + default: + { + tokenType = WordToken + value = append(value, nextRune) + state = inWordState + } + } + } + case inWordState: // in a regular word + { + switch nextRuneType { + case eofRuneClass: + { + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case spaceRuneClass: + { + t.input.UnreadRune() + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case escapingQuoteRuneClass: + { + state = quotingEscapingState + } + case nonEscapingQuoteRuneClass: + { + state = quotingState + } + case escapeRuneClass: + { + state = escapingState + } + default: + { + value = append(value, nextRune) + } + } + } + case escapingState: // the rune after an escape character + { + switch nextRuneType { + case eofRuneClass: + { + err = fmt.Errorf("EOF found after escape character") + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + default: + { + state = inWordState + value = append(value, nextRune) + } + } + } + case escapingQuotedState: // the next rune after an escape character, in double quotes + { + switch nextRuneType { + case eofRuneClass: + { + err = fmt.Errorf("EOF found after escape character") + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + default: + { + state = quotingEscapingState + value = append(value, nextRune) + } + } + } + case quotingEscapingState: // in escaping double quotes + { + switch nextRuneType { + case eofRuneClass: + { + err = fmt.Errorf("EOF found when expecting closing quote") + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case escapingQuoteRuneClass: + { + state = inWordState + } + case escapeRuneClass: + { + state = escapingQuotedState + } + default: + { + value = append(value, nextRune) + } + } + } + case quotingState: // in non-escaping single quotes + { + switch nextRuneType { + case eofRuneClass: + { + err = fmt.Errorf("EOF found when expecting closing quote") + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case nonEscapingQuoteRuneClass: + { + state = inWordState + } + default: + { + value = append(value, nextRune) + } + } + } + case commentState: // in a comment + { + switch nextRuneType { + case eofRuneClass: + { + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } + case spaceRuneClass: + { + if nextRune == '\n' { + state = startState + token := &Token{ + tokenType: tokenType, + value: string(value)} + return token, err + } else { + value = append(value, nextRune) + } + } + default: + { + value = append(value, nextRune) + } + } + } + default: + { + return nil, fmt.Errorf("Unexpected state: %v", state) + } + } + } +} + +// Next returns the next token in the stream. +func (t *Tokenizer) Next() (*Token, error) { + return t.scanStream() +} + +// Split partitions a string into a slice of strings. +func Split(s string) ([]string, error) { + l := NewLexer(strings.NewReader(s)) + subStrings := make([]string, 0) + for { + word, err := l.Next() + if err != nil { + if err == io.EOF { + return subStrings, nil + } + return subStrings, err + } + subStrings = append(subStrings, word) + } +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 416f3f0e9d5..d5bc1f935af 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -338,6 +338,12 @@ "path": "github.com/google/go-querystring/query", "revision": "2a60fc2ba6c19de80291203597d752e9ba58e4c0" }, + { + "checksumSHA1": "e/Kc2UOy1lKAy31xWlK37M1r2e8=", + "path": "github.com/google/shlex", + "revision": "6f45313302b9c56850fc17f99e40caebce98c716", + "revisionTime": "2015-01-27T13:39:51Z" + }, { "checksumSHA1": "FUiF2WLrih0JdHsUTMMDz3DRokw=", "comment": "20141209094003-92-g95fa852", diff --git a/website/source/docs/provisioners/ansible.html.md b/website/source/docs/provisioners/ansible.html.md index 6ca6ce61414..78acb74d110 100644 --- a/website/source/docs/provisioners/ansible.html.md +++ b/website/source/docs/provisioners/ansible.html.md @@ -113,3 +113,60 @@ Optional Parameters: ==> virtualbox-ovf: starting sftp subsystem virtualbox-ovf: fatal: [default]: UNREACHABLE! => {"changed": false, "msg": "SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh", "unreachable": true} ``` + +- Windows builds require a custom Ansible communicator and a particular configuration. + +Assuming a directory named `connection_plugins` is next to the playbook and contains a file named `packer.py` whose contents is + +``` +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.plugins.connection.ssh import Connection as SSHConnection + +class Connection(SSHConnection): + ''' ssh based connections for powershell via packer''' + + transport = 'packer' + has_pipelining = True + become_methods = [] + allow_executable = False + module_implementation_preferences = ('.ps1', '') + + def __init__(self, *args, **kwargs): + super(Connection, self).__init__(*args, **kwargs) +``` + +This template should build a Windows Server 2012 image on Google Cloud Platform: + +``` +{ + "variables": {}, + "provisioners": [ + { + "type": "ansible", + "playbook_file": "./win-playbook.yml", + "extra_arguments": [ + "--connection", "packer", + "--extra-vars", "ansible_shell_type=powershell ansible_shell_executable=None" + ] + } + ], + "builders": [ + { + "type": "googlecompute", + "account_file": "{{user `account_file`}}", + "project_id": "{{user `project_id`}}", + "source_image": "windows-server-2012-r2-dc-v20160916", + "communicator": "winrm", + "zone": "us-central1-a", + "disk_size": 50, + "winrm_username": "packer", + "winrm_use_ssl": true, + "winrm_insecure": true, + "metadata": { + "sysprep-specialize-script-cmd": "winrm set winrm/config/service/auth @{Basic=\"true\"}" + } + } + ] +}