python-vagrant-0.5.15/0000775000076500000240000000000013104101163014453 5ustar tfdstaff00000000000000python-vagrant-0.5.15/CHANGELOG.md0000664000076500000240000003666113104101111016271 0ustar tfdstaff00000000000000 # Changelog This document lists the changes (and individuals who contributed to those changes) for each release of python-vagrant. ## 0.5.15 - Pull Request #54: Create ssh() method to run shell commands in a VM Authors: Parker Thompson (https://github.com/mothran) and Todd DeLuca (https://github.com/todddeluca) - Pull Request #56: Return generator for `up` and `reload` output lines to avoid having entire output in memory. Authors: mmabey (https://github.com/mmabey) and Todd DeLuca (https://github.com/todddeluca) ## 0.5.14 - Pull Request #51: Add support for the vagrant package command. Author: Hans-Christoph Steiner (https://github.com/eighthave) ## 0.5.13 - Pull Request #50: Filter out unneeded status lines for AWS instances Author: Brian Berry (https://github.com/bryanwb) ## 0.5.11 - Pull Request #47: Add support for snapshot command (vagrant version >= 1.8.0) Author: Renat Zaripov (https://github.com/rrzaripov) ## 0.5.10 - Pull Request 46: Adds support for Vagrant 1.8 `--machine-readable` changes. See https://github.com/todddeluca/python-vagrant/pull/46. Author: Conor (https://github.com/conorsch) ## 0.5.9 - Support Python 3 in addition to Python 2 Author: Volodymyr Vitvitskyi (https://github.com/signalpillar) - Use `os.devnull` for Windows compatability. Author: Renat Zaripov (https://github.com/rrzaripov) ## 0.5.8 - Fix regression where vagrant commands were being printed to stdout. Author: Todd DeLuca (https://github.com/todddeluca) ## 0.5.7 - Allow redirection of the output of the vagrant command subprocess to a file. In order to log the output of the subprocess that runs vagrant commands, or alternatively to silence that output, `Vagrant.__init__` accepts two parameters, `out_cm` and `err_cm` that are no-argument functions that, when executed, return a context manager that yields a filehandle, etc., suitable for use with the `stdout` and `stderr` parameters of `subprocess.Popen`. Author: Manuel Sanchez (https://github.com/omoman) Author: Todd DeLuca (https://github.com/todddeluca) ## 0.5.6 - add instance attribute `Vagrant.env` which is a mapping of environment variables to be passed to the vagrant subprocess when invoked. This allows basic inter-process communication between Python and Vagrant via environment variables. Author: Alex Conrad (https://github.com/aconrad) - `Vagrant.__init__` now accepts a keyword argument `env=None` which will be assigned to the instance attribute `Vagrant.env`. Author: Alex Conrad (https://github.com/aconrad) ## 0.5.5 Oops. Pushed non-master branch to PyPI, for version 0.5.4. Pushing master branch for 0.5.5. ## 0.5.4 The major change in this version is switching to using `--machine-readable` in some vagrant commands to make the underlying `vagrant` commands return easily-parseable output. The `--machine-readable` option requires Vagrant 1.4 or higher. - Use `--machine-readable` output for `status`, `box_list`, and `plugin_list`. - Allow arbitrary status codes, so new statuses do not break parsing. Previously, statuses were constrained to known ones for the sake of parsing. Now that machine-readable vagrant output is being used, any status can be parsed. - Status value constants (e.g. vagrant.Vagrant.NOT_CREATED) have changed to match the "state" value returned by the `--machine-readable` output of the `vagrant status` command. - The box version is now returned for a box listing ## 0.5.3 - Add box update command. Author: Alex Lourie (https://github.com/alourie) ## 0.5.2 - Add resume command. Author: Renat Zaripov (https://github.com/rrzaripov) ## 0.5.1 - Find the correct executable on Cygwin systems. See `which` and https://github.com/todddeluca/python-vagrant/issues/26. Author: Todd DeLuca (https://github.com/todddeluca) ## 0.5.0 (release 2014/03/25) This is a backwards-incompatible release with a number of breaking changes to the API. Some of these changes were made to bring the python-vagrant API more closely in line with the vagrant CLI, a key design goal of python-vagrant. Other changes simplify the code. This release also includes a number of pull requests. Major (backwards-incompatible) changes: - Fix inconsistencies between python-vagrant and the vagrant CLI. A goal of the design of methods like `status()`, `box_list()`, and `plugin_list()` is to be a thin wrapper around the corresponding vagrant CLI commands, with a very similar API. These changes bring python-vagrant closer to that goal, I hope. When status() was originally written, it was with single-VM environments in mind, before provider information was available. Since then it was altered to return a dict to handle multi-VM environments. However it still did not return the provider information vagrant outputs. This command updates the status API so that it returns every tuple of VM name (i.e. target), state (i.e. status), and provider output by the underlying vagrant command. These tuples of values are returned as a list of Status classes. The decision to return a list of Statuses instead of a dict mapping VM name to Status was made because the vagrant CLI does not make clear that the status information it returns can be keyed on VM name. In the case of `vagrant box list`, box names can be repeated if there are multiple version of boxes. Therefore, returning a list of Statuses seemed more consistent with (my understanding of) vagrant's API. The box_list() method was originally written, as I recall, before providers and versions were a part of Vagrant. Then box_list_long() was written to accommodate provider information, without changing the box_list() API. Unfortunately, this meant box_list() diverged more from the output of `vagrant box list`. To bring the python-vagrant API back in line with the vagrant API, while keeping it simple, the box_list_long() method is being removed and the box_list() method is being updated to return a list of Box instances. Each box instance contains the information that the `vagrant box list` command returns for a box, the box name, provider, and version. The user who wants a list of box names can do: [box.name for box in v.box_list()] For consistency with status() and box_list(), the relatively new plugin_list() command is updated to return a list of Plugin objects instead of a list of dicts containing the plugin info from vagrant. The choice to use classes for Status, Box, and Plugin information was motivated by the lower syntactic weight compared to using a dicts. Author: Todd DeLuca (https://github.com/todddeluca) - Pull Request #22. Don't die if vagrant executable is missing when the vagrant module is imported. Wait until the Vagrant class is used. Author: Gertjan Oude Lohuis (https://github.com/gertjanol) - Move verbosity/quiet flags from `**kwargs` to instance vars. Unfortunately, this is a breaking change for people who use these keywords. Nevertheless, the proliferation of `**kwargs` in the method signatures is a bad smell. The code is not self documenting. It is not clear from the code what keywords you can pass, and it will accept keywords it does not use. Also, as new methods are added, their signatures must be polluted either by the vague `**kwargs` or a host of seemingly irrelevant keywords, like capture_output and quiet_stderr. Moving the verbosity and quietness functions to instance variables from function parameters makes their functionality more well documented, simplifies and makes more explicit many method signatures, and maintains the desired functionality. For a "loud" instance, use vagrant.Vagrant(quiet_stdout=False). Set quiet_stderr=False for an even louder version. In keeping with past behavior, vagrant instances are quiet by default. Author: Todd DeLuca (https://github.com/todddeluca) Other minor changes and fixes: - Pull Request #21. Fix Sandbox Tests Author: Gertjan Oude Lohuis (https://github.com/gertjanol) - Split internal _run_vagrant_command method into _run_vagrant_command (for capturing output) and _call_vagrant_command (when output is not needed, e.g. for parsing). Author: Todd DeLuca (https://github.com/todddeluca) - Fix provisioning test. Author: Todd DeLuca (https://github.com/todddeluca) ## 0.4.5 (released 2014/03/22) - Add a 'quiet_stderr' keyword to silence the stderr output of vagrant commands. Author: Rich Smith (https://github.com/MyNameIsMeerkat). The original author of the pull request Author: Todd DeLuca. Split the pull request and tweaked the code. - Disable broken SandboxVagrant tests. Does a Sahara user want to fix these tests? Author: Todd DeLuca. ## 0.4.4 (released 2014/03/21) This minor release *should* be backwards-compatible. Add a 'reload' command, which the Vagrant docs describe as akin to a "halt" followed by an "up". Add a 'plugin list' command that returns a list of installed plugins. Add 'version' command, which gives programmatic access to the vagrant version string. Add '--provision-with' option to 'up', 'provision', and 'reload' commands. Author: Todd DeLuca (https://github.com/todddeluca) Add support LXC statuses 'frozen' and 'stopped' Author: Allard Hoeve (https://github.com/allardhoeve) ## 0.4.3 (released 2013/12/18) Allow the underlying vagrant command output to be visible on the command line. Author: Alexandre Joseph (https://github.com/jexhson) ## 0.4.2 (released 2013/12/08) This release fixes a bug in setup.py. Author: Nick Allen (https://github.com/nick-allen). ## 0.4.1 (released 2013/12/08) This release includes improved testing, including a new VagrantTestCase. Author: Nick Allen (https://github.com/nick-allen). ## 0.4.0 (released 2013/07/30) To indicate that this release includes a significant backwards-incompatible API change to `status`, the minor version number is being bumped. Backwards-incompatible enhancements and bug fixes: - Return a dictionary from `status()` in all cases, instead of returning None for no status found, the status string for a single-VM or multi-VM with a VM name specified, or a dictionary for the multi-VM case. This change makes the return value more consistent. It also more closely parallels the return value of the underlying `vagrant status` call. Author: Alek Storm (https://github.com/alekstorm) Author: Todd DeLuca (https://github.com/todddeluca) fixed tests. Enhancements and bug fixes: - Add ability for up to take a provider option Author: Brett Cooley (https://github.com/brcooley) ## 0.3.1 (released 2013/05/09) This release includes two bug fixes aimed at getting vagrant commands to work on Windows: - Use explicit vagrant executable instead of 'vagrant' in subprocess commands. Author: Mohan Raj Rajamanickam (https://github.com/mohanraj-r) - Fix 'which' command so that it finds the vagrant executable on the PATH in Windows. Author: Todd DeLuca (https://github.com/todddeluca) Windows Tester: Mohan Raj Rajamanickam (https://github.com/mohanraj-r) ## 0.3.0 (released 2013/04/12) This release contains backwards-incompatible changes related to the changes in Vagrant 1.1+. Vagrant 1.1 introduces the concept of providers (like virtualbox or vmware_fusion) which affect the API of `vagrant box` commands and the output of `vagrant status` (and other commands). New functionality and bug fixes: - Add new vm state: ABORTED Author: Robert Strind (https://github.com/stribert) - Add new vm state: SAVED Author: Todd DeLuca (https://github.com/todddeluca) - Fix parsing of vagrant 1.1 status messages. Author: Vincent Viallet (https://github.com/zbal) Author: Todd DeLuca (https://github.com/todddeluca) - Add new lifecycle method, suspend(), corresponding to `vagrant suspend`. Author: Todd DeLuca (https://github.com/todddeluca) - Fix parsing of vagrant 1.1 ssh config output. Author: Vincent Viallet (https://github.com/zbal) Backwards-incompatible changes: - Removed redundant `box_` prefix from `box_name` and `box_url` parameters in `box_add` and `box_remove` methods. This aligns these parameter names with the parameter names in the corresponding vagrant CLI commands. Author: Todd DeLuca (https://github.com/todddeluca). - Added required parameter `provider` to `box_remove` method. This is consistent with the backwards-incompatible change in the underlying `vagrant box remove` command. Author: Todd DeLuca (https://github.com/todddeluca). - Method `init`, corresponding to `vagrant init`, has been changed to more closely reflect `vagrant init`. The parameter `box_path` has been changed to `box_url`. The method no longer attempts to interactively add a box if it has not already been added. Author: Todd DeLuca (https://github.com/todddeluca). ## 0.2.0 (released 2012/12/09) This release incorporates numerous changes from a couple of forks on github, https://github.com/kamilgrymuza/python-vagrant and https://github.com/nithinbose87/python-vagrant. - A rewritten test suite allowing easier addition of new features. Author: Kamil Grymuza (https://github.com/kamilgrymuza). - The init() method which initialized the VM based on the named base box. Author: Kamil Grymuza (https://github.com/kamilgrymuza). - The halt() method which stops the VM without destroying it. Author: Kamil Grymuza (https://github.com/kamilgrymuza). - Support for sandbox mode using the Sahara gem (https://github.com/jedi4ever/sahara). Author: Kamil Grymuza (https://github.com/kamilgrymuza). - Support for box-related commands - box_add(), box_list(), box_remove() methods. Author: Kamil Grymuza (https://github.com/kamilgrymuza). - Support for provisioning - up() accepts no_provision and there is the provision() method. Author: Kamil Grymuza (https://github.com/kamilgrymuza). - Added auto download of official boxes in the init() Author: Nithin Bose (https://github.com/nithinbose87). Additionally, support for Multi-VM environments has been added, along with several other changes: - `vagrant.Vagrant` and `vagrant.SandboxVagrant` methods which support multi-VM environments through the `vm_name` parameter. Author: Todd DeLuca (https://github.com/todddeluca). - A new subclass, SandboxVagrant, for using the sandbox extensions from the Sahara gem. Method names in SandboxVagrant were changed to conform to the cli names of sandbox. E.g. sandbox_enable() was changed to sandbox_on(). This is in keeping with the goal of python-vagrant to stick closely to the nomenclature of vagrant. Author: Todd DeLuca (https://github.com/todddeluca). - A rewritten `tests/test_vagrant.py` which removes a dependency on Fabric, adds tests for multi-VM functionality, and moves some setup and teardown up to the module level. Author: Todd DeLuca (https://github.com/todddeluca). - Vagrant and SandboxVagrant no longer invoke subprocesses with `shell=True`. This way something like `vagrant ssh -c ` could be used without worry about how to quote the command. Author: Todd DeLuca (https://github.com/todddeluca). - Configuration is now cached under the given vm_name, when relevant. Author: Todd DeLuca (https://github.com/todddeluca). - `status()` now returns multiple statuses when in a multi-VM environment. Author: Todd DeLuca (https://github.com/todddeluca). Please note that the changes to sandbox functionality are not backwards-compatible with the kamilgrymuza fork, though updating the code to use this project should be straightforward, should one want to do so. ## 0.1.0 (released 2012/06/07) This is the original release of python-vagrant as its own package. - Author: Todd DeLuca (https://github.com/todddeluca). python-vagrant-0.5.15/LICENSE.txt0000644000076500000240000000207012314364544016313 0ustar tfdstaff00000000000000The MIT License (MIT) Copyright (c) 2014 Todd F. DeLuca Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-vagrant-0.5.15/MANIFEST.in0000644000076500000240000000007412061527701016223 0ustar tfdstaff00000000000000include *.md *.txt recursive-include tests *.py Vagrantfile python-vagrant-0.5.15/PKG-INFO0000664000076500000240000002412613104101163015555 0ustar tfdstaff00000000000000Metadata-Version: 1.1 Name: python-vagrant Version: 0.5.15 Summary: Python bindings for interacting with Vagrant virtual machines. Home-page: https://github.com/todddeluca/python-vagrant Author: Todd Francis DeLuca Author-email: todddeluca@yahoo.com License: MIT Description: ## Introduction Python-vagrant is a python module that provides a _thin_ wrapper around the `vagrant` command line executable, allowing programmatic control of Vagrant virtual machines (boxes). This module is useful for: - Starting a Vagrant virtual machine (VM) (`up`). - Terminating a Vagrant VM (`destroy`). - Halting a Vagrant VM without destroying it (`halt`). - Querying the status of a VM or VMs (`status`). - Getting ssh configuration information useful for SSHing into the VM. (`host`, `port`, ...) - Running `vagrant` commands in a multi-VM environment (http://vagrantup.com/v1/docs/multivm.html) by using `vm_name` parameter. - Initializing the VM based on a named base box, using init(). - Adding, Removing, and Listing boxes (`box add`, `box remove`, `box list`). - Provisioning VMs - up() accepts options like `no_provision`, `provision`, and `provision_with`, and there is a `provision()` method. - Using sandbox mode from the Sahara gem (https://github.com/jedi4ever/sahara). This project began because I wanted python bindings for Vagrant so I could programmatically access my vagrant box using Fabric. Drop me a line to let me know how you use python-vagrant. I'd love to share more use cases. -Todd DeLuca ## Versioning and API Stability This package is _beta_ and its API is not guaranteed to be stable. The API attempts to be congruent with the `vagrant` API terminology, to facilitate knowledge transfer for users already familiar with Vagrant. Over time, the python-vagrant API has changed to better match the underling `vagrant` CLI and to evolve with the changes in that CLI. The package version numbering is in the form `0.X.Y`. The initial `0` reflects the _beta_ nature of this project. The number `X` is incremented when backwards-incompatible changes occur. The number `Y` is incremented when backwards-compatible features or bug fixes are added. ## Requirements - Vagrant 1.4 or greater (currently tested with 1.7.2). Using the latest version of Vagrant is strongly recommended. - Vagrant requires VirtualBox, VMWare, or another supported provider. - Python 2.7 (the only version this package has been tested with.) or Python 3.3 or higher. - The Sahara gem for Vagrant is optional. It will allow you to use `SandboxVagrant`. ## Installation ### Install from pypi.python.org Download and install python-vagrant: pip install python-vagrant ### Install from github.com Clone and install python-vagrant cd ~ git clone git@github.com:todddeluca/python-vagrant.git cd python-vagrant python setup.py install ## Usage A contrived example of starting a vagrant box (using a Vagrantfile from the current directory) and running a fabric task on it: import vagrant from fabric.api import env, execute, task, run @task def mytask(): run('echo $USER') v = vagrant.Vagrant() v.up() env.hosts = [v.user_hostname_port()] env.key_filename = v.keyfile() env.disable_known_hosts = True # useful for when the vagrant box ip changes. execute(mytask) # run a fabric task on the vagrant host. Another example showing how to use vagrant multi-vm feature with fabric: import vagrant from fabric.api import * @task def start(machine_name): """Starts the specified machine using vagrant""" v = vagrant.Vagrant() v.up(vm_name=machine_name) with settings(host_string= v.user_hostname_port(vm_name=machine_name), key_filename = v.keyfile(vm_name=machine_name), disable_known_hosts = True): run("echo hello") By default python vagrant instances are quiet, meaning that they capture stdout and stderr. For a "loud" instance, use `vagrant.Vagrant(quiet_stdout=False)`. Set `quiet_stderr=False` for an even louder version. ### Interacting With the Vagrant Subprocess The `Vagrant` class works by executing `vagrant` commands in a subprocess and interpreting the output. Depending on the needs of the user, the communication to and from the subprocess can be tailored by altering its environment and where it sends its stdout and stderr. #### Silencing the Stdout or Stderr of the Vagrant Subprocess The stdout and stderr of the underlying vagrant process can be silenced by using the `out_cm` and `err_cm` parameters, or by using the `quiet_stdout` and `quiet_stderr` parameters of `Vagrant.__init__`. Using `out_cm` and `err_cm` to redirect stdout and stderr to `/dev/null`: v = vagrant.Vagrant(out_cm=vagrant.devnull_cm, err_cm=vagrant.devnull_cm) v.up() # normally noisy Using `quiet_stdout` and `quiet_stderr` to redirect stdout and stderr to `/dev/null`: v = vagrant.Vagrant(quiet_stdout=True, quiet_stderr=True) v.up() # normally noisy These are functionally equivalent. #### Logging the Stdout or Stderr of the Vagrant Subprocess A user might wish to direct the stdout and stderr of a vagrant subprocess to a file, perhaps to log and analyze the results of an automated process. This can be accomplished using the `out_cm` and `err_cm` parameters of `Vagrant.__init__`. For example, log the stdout and stderr of the subprocess to the file 'deployment.log': log_cm = vagrant.make_file_cm('deployment.log') v = vagrant.Vagrant(out_cm=log_cm, err_cm=log_cm) v.up() # normally noisy #### Altering the Environment of the Vagrant Subprocess It's possible to communicate with the Vagrant subprocess using environment variables. The `Vagrantfile` could expect environment variables to be present and act accordingly. The environment variables can be set by `python-vagrant`. ```python import vagrant v = vagrant.Vagrant() os_env = os.environ.copy() os_env['USE_NFS'] = '1' v.env = os_env v.up() # will pass env to the vagrant subprocess ``` Alternatively, the environment can be passed at instantiation time. ```python import vagrant os_env = os.environ.copy() os_env['USE_NFS'] = '1' v = vagrant.Vagrant(env=env) assert v.env is env # True v.up() # will pass env to the vagrant subprocess ``` ## Contribute If you use python and vagrant and this project does not do what you want, please open an issue or a pull request on github at https://github.com/todddeluca/python-vagrant. Please see CHANGELOG.md for a detailed list of contributions and authors. When making a pull request, please include unit tests that test your changes and make sure any existing tests still work. See the Testing section below. ## Testing Running the full suite of tests might take 10 minutes or so. It involves downloading boxes and starting and stopping virtual machines several times. Run the tests from the top-level directory of the repository: nosetests Here is an example of running an individual test: nosetests tests.test_vagrant:test_boxes Manual test of functionality for controlling where the vagrant subcommand output is sent -- console or devnull: >>> import vagrant >>> import os >>> vagrantfile = '/Users/tfd/proj/python-vagrant/tests/vagrantfiles/single_box' >>> # Demonstrate a quiet Vagrant. Equivalent to out_cm=vagrant.devnull_cm ... v1 = vagrant.Vagrant(vagrantfile) >>> v1.destroy() # output to /dev/null >>> # Demonstrate a loud Vagrant. Equivalent to out_cm=vagrant.stdout_cm ... v2 = vagrant.Vagrant(vagrantfile, quiet_stdout=False) >>> v2.destroy() # stdout sent to console ==> default: VM not created. Moving on... >>> # Demonstrate that out_cm takes precedence over quiet_stdout=True ... v3 = vagrant.Vagrant(vagrantfile, out_cm=vagrant.stdout_cm) >>> v3.destroy() # output to console ==> default: VM not created. Moving on... >>> # Demonstrate a quiet Vagrant using devnull_cm directly ... v4 = vagrant.Vagrant(vagrantfile, out_cm=vagrant.devnull_cm) >>> v4.destroy() # output to console >>> Keywords: python virtual machine box vagrant virtualbox vagrantfile Platform: UNKNOWN Classifier: License :: OSI Approved :: MIT License Classifier: Development Status :: 4 - Beta Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 python-vagrant-0.5.15/python_vagrant.egg-info/0000775000076500000240000000000013104101163021210 5ustar tfdstaff00000000000000python-vagrant-0.5.15/python_vagrant.egg-info/dependency_links.txt0000644000076500000240000000000113104101162025253 0ustar tfdstaff00000000000000 python-vagrant-0.5.15/python_vagrant.egg-info/PKG-INFO0000644000076500000240000002412613104101162022307 0ustar tfdstaff00000000000000Metadata-Version: 1.1 Name: python-vagrant Version: 0.5.15 Summary: Python bindings for interacting with Vagrant virtual machines. Home-page: https://github.com/todddeluca/python-vagrant Author: Todd Francis DeLuca Author-email: todddeluca@yahoo.com License: MIT Description: ## Introduction Python-vagrant is a python module that provides a _thin_ wrapper around the `vagrant` command line executable, allowing programmatic control of Vagrant virtual machines (boxes). This module is useful for: - Starting a Vagrant virtual machine (VM) (`up`). - Terminating a Vagrant VM (`destroy`). - Halting a Vagrant VM without destroying it (`halt`). - Querying the status of a VM or VMs (`status`). - Getting ssh configuration information useful for SSHing into the VM. (`host`, `port`, ...) - Running `vagrant` commands in a multi-VM environment (http://vagrantup.com/v1/docs/multivm.html) by using `vm_name` parameter. - Initializing the VM based on a named base box, using init(). - Adding, Removing, and Listing boxes (`box add`, `box remove`, `box list`). - Provisioning VMs - up() accepts options like `no_provision`, `provision`, and `provision_with`, and there is a `provision()` method. - Using sandbox mode from the Sahara gem (https://github.com/jedi4ever/sahara). This project began because I wanted python bindings for Vagrant so I could programmatically access my vagrant box using Fabric. Drop me a line to let me know how you use python-vagrant. I'd love to share more use cases. -Todd DeLuca ## Versioning and API Stability This package is _beta_ and its API is not guaranteed to be stable. The API attempts to be congruent with the `vagrant` API terminology, to facilitate knowledge transfer for users already familiar with Vagrant. Over time, the python-vagrant API has changed to better match the underling `vagrant` CLI and to evolve with the changes in that CLI. The package version numbering is in the form `0.X.Y`. The initial `0` reflects the _beta_ nature of this project. The number `X` is incremented when backwards-incompatible changes occur. The number `Y` is incremented when backwards-compatible features or bug fixes are added. ## Requirements - Vagrant 1.4 or greater (currently tested with 1.7.2). Using the latest version of Vagrant is strongly recommended. - Vagrant requires VirtualBox, VMWare, or another supported provider. - Python 2.7 (the only version this package has been tested with.) or Python 3.3 or higher. - The Sahara gem for Vagrant is optional. It will allow you to use `SandboxVagrant`. ## Installation ### Install from pypi.python.org Download and install python-vagrant: pip install python-vagrant ### Install from github.com Clone and install python-vagrant cd ~ git clone git@github.com:todddeluca/python-vagrant.git cd python-vagrant python setup.py install ## Usage A contrived example of starting a vagrant box (using a Vagrantfile from the current directory) and running a fabric task on it: import vagrant from fabric.api import env, execute, task, run @task def mytask(): run('echo $USER') v = vagrant.Vagrant() v.up() env.hosts = [v.user_hostname_port()] env.key_filename = v.keyfile() env.disable_known_hosts = True # useful for when the vagrant box ip changes. execute(mytask) # run a fabric task on the vagrant host. Another example showing how to use vagrant multi-vm feature with fabric: import vagrant from fabric.api import * @task def start(machine_name): """Starts the specified machine using vagrant""" v = vagrant.Vagrant() v.up(vm_name=machine_name) with settings(host_string= v.user_hostname_port(vm_name=machine_name), key_filename = v.keyfile(vm_name=machine_name), disable_known_hosts = True): run("echo hello") By default python vagrant instances are quiet, meaning that they capture stdout and stderr. For a "loud" instance, use `vagrant.Vagrant(quiet_stdout=False)`. Set `quiet_stderr=False` for an even louder version. ### Interacting With the Vagrant Subprocess The `Vagrant` class works by executing `vagrant` commands in a subprocess and interpreting the output. Depending on the needs of the user, the communication to and from the subprocess can be tailored by altering its environment and where it sends its stdout and stderr. #### Silencing the Stdout or Stderr of the Vagrant Subprocess The stdout and stderr of the underlying vagrant process can be silenced by using the `out_cm` and `err_cm` parameters, or by using the `quiet_stdout` and `quiet_stderr` parameters of `Vagrant.__init__`. Using `out_cm` and `err_cm` to redirect stdout and stderr to `/dev/null`: v = vagrant.Vagrant(out_cm=vagrant.devnull_cm, err_cm=vagrant.devnull_cm) v.up() # normally noisy Using `quiet_stdout` and `quiet_stderr` to redirect stdout and stderr to `/dev/null`: v = vagrant.Vagrant(quiet_stdout=True, quiet_stderr=True) v.up() # normally noisy These are functionally equivalent. #### Logging the Stdout or Stderr of the Vagrant Subprocess A user might wish to direct the stdout and stderr of a vagrant subprocess to a file, perhaps to log and analyze the results of an automated process. This can be accomplished using the `out_cm` and `err_cm` parameters of `Vagrant.__init__`. For example, log the stdout and stderr of the subprocess to the file 'deployment.log': log_cm = vagrant.make_file_cm('deployment.log') v = vagrant.Vagrant(out_cm=log_cm, err_cm=log_cm) v.up() # normally noisy #### Altering the Environment of the Vagrant Subprocess It's possible to communicate with the Vagrant subprocess using environment variables. The `Vagrantfile` could expect environment variables to be present and act accordingly. The environment variables can be set by `python-vagrant`. ```python import vagrant v = vagrant.Vagrant() os_env = os.environ.copy() os_env['USE_NFS'] = '1' v.env = os_env v.up() # will pass env to the vagrant subprocess ``` Alternatively, the environment can be passed at instantiation time. ```python import vagrant os_env = os.environ.copy() os_env['USE_NFS'] = '1' v = vagrant.Vagrant(env=env) assert v.env is env # True v.up() # will pass env to the vagrant subprocess ``` ## Contribute If you use python and vagrant and this project does not do what you want, please open an issue or a pull request on github at https://github.com/todddeluca/python-vagrant. Please see CHANGELOG.md for a detailed list of contributions and authors. When making a pull request, please include unit tests that test your changes and make sure any existing tests still work. See the Testing section below. ## Testing Running the full suite of tests might take 10 minutes or so. It involves downloading boxes and starting and stopping virtual machines several times. Run the tests from the top-level directory of the repository: nosetests Here is an example of running an individual test: nosetests tests.test_vagrant:test_boxes Manual test of functionality for controlling where the vagrant subcommand output is sent -- console or devnull: >>> import vagrant >>> import os >>> vagrantfile = '/Users/tfd/proj/python-vagrant/tests/vagrantfiles/single_box' >>> # Demonstrate a quiet Vagrant. Equivalent to out_cm=vagrant.devnull_cm ... v1 = vagrant.Vagrant(vagrantfile) >>> v1.destroy() # output to /dev/null >>> # Demonstrate a loud Vagrant. Equivalent to out_cm=vagrant.stdout_cm ... v2 = vagrant.Vagrant(vagrantfile, quiet_stdout=False) >>> v2.destroy() # stdout sent to console ==> default: VM not created. Moving on... >>> # Demonstrate that out_cm takes precedence over quiet_stdout=True ... v3 = vagrant.Vagrant(vagrantfile, out_cm=vagrant.stdout_cm) >>> v3.destroy() # output to console ==> default: VM not created. Moving on... >>> # Demonstrate a quiet Vagrant using devnull_cm directly ... v4 = vagrant.Vagrant(vagrantfile, out_cm=vagrant.devnull_cm) >>> v4.destroy() # output to console >>> Keywords: python virtual machine box vagrant virtualbox vagrantfile Platform: UNKNOWN Classifier: License :: OSI Approved :: MIT License Classifier: Development Status :: 4 - Beta Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 python-vagrant-0.5.15/python_vagrant.egg-info/SOURCES.txt0000644000076500000240000000103013104101163023064 0ustar tfdstaff00000000000000CHANGELOG.md LICENSE.txt MANIFEST.in README.md setup.py python_vagrant.egg-info/PKG-INFO python_vagrant.egg-info/SOURCES.txt python_vagrant.egg-info/dependency_links.txt python_vagrant.egg-info/top_level.txt tests/__init__.py tests/test_vagrant.py tests/test_vagrant_test_case.py tests/vagrantfiles/multivm_Vagrantfile tests/vagrantfiles/shell_provision_Vagrantfile tests/vagrantfiles/vm_Vagrantfile tests/vagrantfiles/multi_box/Vagrantfile tests/vagrantfiles/single_box/Vagrantfile vagrant/__init__.py vagrant/compat.py vagrant/test.pypython-vagrant-0.5.15/python_vagrant.egg-info/top_level.txt0000644000076500000240000000001013104101162023726 0ustar tfdstaff00000000000000vagrant python-vagrant-0.5.15/README.md0000664000076500000240000001727712543772314015772 0ustar tfdstaff00000000000000## Introduction Python-vagrant is a python module that provides a _thin_ wrapper around the `vagrant` command line executable, allowing programmatic control of Vagrant virtual machines (boxes). This module is useful for: - Starting a Vagrant virtual machine (VM) (`up`). - Terminating a Vagrant VM (`destroy`). - Halting a Vagrant VM without destroying it (`halt`). - Querying the status of a VM or VMs (`status`). - Getting ssh configuration information useful for SSHing into the VM. (`host`, `port`, ...) - Running `vagrant` commands in a multi-VM environment (http://vagrantup.com/v1/docs/multivm.html) by using `vm_name` parameter. - Initializing the VM based on a named base box, using init(). - Adding, Removing, and Listing boxes (`box add`, `box remove`, `box list`). - Provisioning VMs - up() accepts options like `no_provision`, `provision`, and `provision_with`, and there is a `provision()` method. - Using sandbox mode from the Sahara gem (https://github.com/jedi4ever/sahara). This project began because I wanted python bindings for Vagrant so I could programmatically access my vagrant box using Fabric. Drop me a line to let me know how you use python-vagrant. I'd love to share more use cases. -Todd DeLuca ## Versioning and API Stability This package is _beta_ and its API is not guaranteed to be stable. The API attempts to be congruent with the `vagrant` API terminology, to facilitate knowledge transfer for users already familiar with Vagrant. Over time, the python-vagrant API has changed to better match the underling `vagrant` CLI and to evolve with the changes in that CLI. The package version numbering is in the form `0.X.Y`. The initial `0` reflects the _beta_ nature of this project. The number `X` is incremented when backwards-incompatible changes occur. The number `Y` is incremented when backwards-compatible features or bug fixes are added. ## Requirements - Vagrant 1.4 or greater (currently tested with 1.7.2). Using the latest version of Vagrant is strongly recommended. - Vagrant requires VirtualBox, VMWare, or another supported provider. - Python 2.7 (the only version this package has been tested with.) or Python 3.3 or higher. - The Sahara gem for Vagrant is optional. It will allow you to use `SandboxVagrant`. ## Installation ### Install from pypi.python.org Download and install python-vagrant: pip install python-vagrant ### Install from github.com Clone and install python-vagrant cd ~ git clone git@github.com:todddeluca/python-vagrant.git cd python-vagrant python setup.py install ## Usage A contrived example of starting a vagrant box (using a Vagrantfile from the current directory) and running a fabric task on it: import vagrant from fabric.api import env, execute, task, run @task def mytask(): run('echo $USER') v = vagrant.Vagrant() v.up() env.hosts = [v.user_hostname_port()] env.key_filename = v.keyfile() env.disable_known_hosts = True # useful for when the vagrant box ip changes. execute(mytask) # run a fabric task on the vagrant host. Another example showing how to use vagrant multi-vm feature with fabric: import vagrant from fabric.api import * @task def start(machine_name): """Starts the specified machine using vagrant""" v = vagrant.Vagrant() v.up(vm_name=machine_name) with settings(host_string= v.user_hostname_port(vm_name=machine_name), key_filename = v.keyfile(vm_name=machine_name), disable_known_hosts = True): run("echo hello") By default python vagrant instances are quiet, meaning that they capture stdout and stderr. For a "loud" instance, use `vagrant.Vagrant(quiet_stdout=False)`. Set `quiet_stderr=False` for an even louder version. ### Interacting With the Vagrant Subprocess The `Vagrant` class works by executing `vagrant` commands in a subprocess and interpreting the output. Depending on the needs of the user, the communication to and from the subprocess can be tailored by altering its environment and where it sends its stdout and stderr. #### Silencing the Stdout or Stderr of the Vagrant Subprocess The stdout and stderr of the underlying vagrant process can be silenced by using the `out_cm` and `err_cm` parameters, or by using the `quiet_stdout` and `quiet_stderr` parameters of `Vagrant.__init__`. Using `out_cm` and `err_cm` to redirect stdout and stderr to `/dev/null`: v = vagrant.Vagrant(out_cm=vagrant.devnull_cm, err_cm=vagrant.devnull_cm) v.up() # normally noisy Using `quiet_stdout` and `quiet_stderr` to redirect stdout and stderr to `/dev/null`: v = vagrant.Vagrant(quiet_stdout=True, quiet_stderr=True) v.up() # normally noisy These are functionally equivalent. #### Logging the Stdout or Stderr of the Vagrant Subprocess A user might wish to direct the stdout and stderr of a vagrant subprocess to a file, perhaps to log and analyze the results of an automated process. This can be accomplished using the `out_cm` and `err_cm` parameters of `Vagrant.__init__`. For example, log the stdout and stderr of the subprocess to the file 'deployment.log': log_cm = vagrant.make_file_cm('deployment.log') v = vagrant.Vagrant(out_cm=log_cm, err_cm=log_cm) v.up() # normally noisy #### Altering the Environment of the Vagrant Subprocess It's possible to communicate with the Vagrant subprocess using environment variables. The `Vagrantfile` could expect environment variables to be present and act accordingly. The environment variables can be set by `python-vagrant`. ```python import vagrant v = vagrant.Vagrant() os_env = os.environ.copy() os_env['USE_NFS'] = '1' v.env = os_env v.up() # will pass env to the vagrant subprocess ``` Alternatively, the environment can be passed at instantiation time. ```python import vagrant os_env = os.environ.copy() os_env['USE_NFS'] = '1' v = vagrant.Vagrant(env=env) assert v.env is env # True v.up() # will pass env to the vagrant subprocess ``` ## Contribute If you use python and vagrant and this project does not do what you want, please open an issue or a pull request on github at https://github.com/todddeluca/python-vagrant. Please see CHANGELOG.md for a detailed list of contributions and authors. When making a pull request, please include unit tests that test your changes and make sure any existing tests still work. See the Testing section below. ## Testing Running the full suite of tests might take 10 minutes or so. It involves downloading boxes and starting and stopping virtual machines several times. Run the tests from the top-level directory of the repository: nosetests Here is an example of running an individual test: nosetests tests.test_vagrant:test_boxes Manual test of functionality for controlling where the vagrant subcommand output is sent -- console or devnull: >>> import vagrant >>> import os >>> vagrantfile = '/Users/tfd/proj/python-vagrant/tests/vagrantfiles/single_box' >>> # Demonstrate a quiet Vagrant. Equivalent to out_cm=vagrant.devnull_cm ... v1 = vagrant.Vagrant(vagrantfile) >>> v1.destroy() # output to /dev/null >>> # Demonstrate a loud Vagrant. Equivalent to out_cm=vagrant.stdout_cm ... v2 = vagrant.Vagrant(vagrantfile, quiet_stdout=False) >>> v2.destroy() # stdout sent to console ==> default: VM not created. Moving on... >>> # Demonstrate that out_cm takes precedence over quiet_stdout=True ... v3 = vagrant.Vagrant(vagrantfile, out_cm=vagrant.stdout_cm) >>> v3.destroy() # output to console ==> default: VM not created. Moving on... >>> # Demonstrate a quiet Vagrant using devnull_cm directly ... v4 = vagrant.Vagrant(vagrantfile, out_cm=vagrant.devnull_cm) >>> v4.destroy() # output to console >>> python-vagrant-0.5.15/setup.cfg0000664000076500000240000000007313104101163016274 0ustar tfdstaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 python-vagrant-0.5.15/setup.py0000664000076500000240000000233112543666736016220 0ustar tfdstaff00000000000000import os import re from setuptools import setup # parse version from package/module without importing or evaluating the code with open('vagrant/__init__.py') as fh: for line in fh: m = re.search(r"^__version__ = '(?P[^']+)'$", line) if m: version = m.group('version') break setup( name = 'python-vagrant', version = version, license = 'MIT', description = 'Python bindings for interacting with Vagrant virtual machines.', long_description = open(os.path.join(os.path.dirname(__file__), 'README.md')).read(), keywords = 'python virtual machine box vagrant virtualbox vagrantfile', url = 'https://github.com/todddeluca/python-vagrant', author = 'Todd Francis DeLuca', author_email = 'todddeluca@yahoo.com', classifiers = ['License :: OSI Approved :: MIT License', 'Development Status :: 4 - Beta', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', ], packages = ['vagrant'], ) python-vagrant-0.5.15/tests/0000775000076500000240000000000013104101163015615 5ustar tfdstaff00000000000000python-vagrant-0.5.15/tests/__init__.py0000664000076500000240000000000012175753755017747 0ustar tfdstaff00000000000000python-vagrant-0.5.15/tests/test_vagrant.py0000664000076500000240000005372413104101111020674 0ustar tfdstaff00000000000000''' Introduces setup and teardown routines suitable for testing Vagrant. Note that the tests can take few minutes to run because of the time required to bring up/down the VM. Most test functions (decorated with `@with_setup`) will actually bring the VM up/down. This is the "proper" way of doing things (isolation). However, the downside of such a workflow is that it increases the execution time of the test suite. Before the first test a base box is added to Vagrant under the name TEST_BOX_NAME. This box is not deleted after the test suite runs in order to avoid downloading of the box file on every run. ''' from __future__ import print_function import os import re import unittest import shutil import subprocess import sys import tempfile import time from nose.tools import eq_, ok_, with_setup, assert_raises import vagrant from vagrant import compat # location of a test file on the created box by provisioning in vm_Vagrantfile TEST_FILE_PATH = '/home/vagrant/python_vagrant_test_file' # location of Vagrantfiles used for testing. MULTIVM_VAGRANTFILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vagrantfiles', 'multivm_Vagrantfile') VM_VAGRANTFILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vagrantfiles', 'vm_Vagrantfile') SHELL_PROVISION_VAGRANTFILE = os.path.join( os.path.dirname(os.path.abspath(__file__)), 'vagrantfiles', 'shell_provision_Vagrantfile') # the names of the vms from the multi-vm Vagrantfile. VM_1 = 'web' VM_2 = 'db' # name of the base box used for testing TEST_BOX_NAME = "python-vagrant-base" # url of the box file used for testing TEST_BOX_URL = "http://files.vagrantup.com/lucid32.box" # temp dir for testing. TD = None def list_box_names(): ''' Return a list of the currently installed vagrant box names. This is implemented outside of `vagrant.Vagrant`, so that it will still work even if the `Vagrant.box_list()` implementation is broken. ''' listing = compat.decode(subprocess.check_output('vagrant box list --machine-readable', shell=True)) box_names = [] for line in listing.splitlines(): # Vagrant 1.8 added additional fields to the --machine-readable output, # so unpack the fields according to the number of separators found. if line.count(',') == 3: timestamp, _, kind, data = line.split(',') else: timestamp, _, kind, data, extra_data = line.split(',') if kind == 'box-name': box_names.append(data.strip()) return box_names # MODULE-LEVEL SETUP AND TEARDOWN def setup(): ''' Creates the directory used for testing and sets up the base box if not already set up. Creates a directory in a temporary location and checks if there is a base box under the `TEST_BOX_NAME`. If not, downloads it from `TEST_BOX_URL` and adds to Vagrant. This is ran once before the first test (global setup). ''' sys.stderr.write('module setup()\n') global TD TD = tempfile.mkdtemp() sys.stderr.write('test temp dir: {}\n'.format(TD)) boxes = list_box_names() if TEST_BOX_NAME not in boxes: cmd = 'vagrant box add {} {}'.format(TEST_BOX_NAME, TEST_BOX_URL) subprocess.check_call(cmd, shell=True) def teardown(): ''' Removes the directory created in setup. This is run once after the last test. ''' sys.stderr.write('module teardown()\n') if TD is not None: shutil.rmtree(TD) # TEST-LEVEL SETUP AND TEARDOWN def make_setup_vm(vagrantfile=None): ''' Make and return a function that sets up the temporary directory with a Vagrantfile. By default, use VM_VAGRANTFILE. vagrantfile: path to a vagrantfile to use as Vagrantfile in the testing temporary directory. ''' if vagrantfile is None: vagrantfile = VM_VAGRANTFILE def setup_vm(): shutil.copy(vagrantfile, os.path.join(TD, 'Vagrantfile')) return setup_vm def teardown_vm(): ''' Attempts to destroy every VM in the Vagrantfile in the temporary directory, TD. It is not an error if a VM has already been destroyed. ''' try: # Try to destroy any vagrant box that might be running. subprocess.check_call('vagrant destroy -f', cwd=TD, shell=True) except subprocess.CalledProcessError: pass finally: # remove Vagrantfile created by setup. os.unlink(os.path.join(TD, "Vagrantfile")) @with_setup(make_setup_vm(), teardown_vm) def test_parse_plugin_list(): ''' Test the parsing the output of the `vagrant plugin list` command. ''' # listing should match output generated by `vagrant plugin list`. listing = '''1424145521,,plugin-name,sahara 1424145521,sahara,plugin-version,0.0.16 1424145521,,plugin-name,vagrant-share 1424145521,vagrant-share,plugin-version,1.1.3%!(VAGRANT_COMMA) system ''' # Can compare tuples to Plugin class b/c Plugin is a collections.namedtuple. goal = [('sahara', '0.0.16', False), ('vagrant-share', '1.1.3', True)] v = vagrant.Vagrant(TD) parsed = v._parse_plugin_list(listing) assert goal == parsed, 'The parsing of the test listing did not match the goal.\nlisting={!r}\ngoal={!r}\nparsed_listing={!r}'.format(listing, goal, parsed) @with_setup(make_setup_vm(), teardown_vm) def test_parse_box_list(): ''' Test the parsing the output of the `vagrant box list` command. ''' listing = '''1424141572,,box-name,precise64 1424141572,,box-provider,virtualbox 1424141572,,box-version,0 1424141572,,box-name,python-vagrant-base 1424141572,,box-provider,virtualbox 1424141572,,box-version,0 ''' # Can compare tuples to Box class b/c Box is a collections.namedtuple. goal = [('precise64', 'virtualbox', '0'), ('python-vagrant-base', 'virtualbox', '0')] v = vagrant.Vagrant(TD) parsed = v._parse_box_list(listing) assert goal == parsed, 'The parsing of the test listing did not match the goal.\nlisting={!r}\ngoal={!r}\nparsed_listing={!r}'.format(listing, goal, parsed) @with_setup(make_setup_vm(), teardown_vm) def test_parse_status(): ''' Test the parsing the output of the `vagrant status` command. ''' listing = '''1424098924,web,provider-name,virtualbox 1424098924,web,state,running 1424098924,web,state-human-short,running 1424098924,web,state-human-long,The VM is running. To stop this VM%!(VAGRANT_COMMA) you can run `vagrant halt` to\\nshut it down forcefully%!(VAGRANT_COMMA) or you can run `vagrant suspend` to simply\\nsuspend the virtual machine. In either case%!(VAGRANT_COMMA) to restart it again%!(VAGRANT_COMMA)\\nsimply run `vagrant up`. 1424098924,db,provider-name,virtualbox 1424098924,db,state,not_created 1424098924,db,state-human-short,not created 1424098924,db,state-human-long,The environment has not yet been created. Run `vagrant up` to\\ncreate the environment. If a machine is not created%!(VAGRANT_COMMA) only the\\ndefault provider will be shown. So if a provider is not listed%!(VAGRANT_COMMA)\\nthen the machine is not created for that environment. ''' # Can compare tuples to Status class b/c Status is a collections.namedtuple. goal = [('web', 'running', 'virtualbox'), ('db', 'not_created', 'virtualbox')] v = vagrant.Vagrant(TD) parsed = v._parse_status(listing) assert goal == parsed, 'The parsing of the test listing did not match the goal.\nlisting={!r}\ngoal={!r}\nparsed_listing={!r}'.format(listing, goal, parsed) @with_setup(make_setup_vm(), teardown_vm) def test_parse_aws_status(): ''' Test the parsing the output of the `vagrant status` command for an aws instance. ''' listing = '''1462351212,default,action,read_state,start 1462351214,default,action,read_state,end 1462351214,default,metadata,provider,aws 1462351214,default,action,read_state,start 1462351215,default,action,read_state,end 1462351215,default,action,read_state,start 1462351216,default,action,read_state,end 1462351216,default,action,read_state,start 1462351217,default,action,read_state,end 1462351217,default,provider-name,aws 1462351217,default,state,running 1462351217,default,state-human-short,running 1462351217,default,state-human-long,The EC2 instance is running. To stop this machine%!(VAGRANT_COMMA) you can run\\n`vagrant halt`. To destroy the machine%!(VAGRANT_COMMA) you can run `vagrant destroy`. 1462351217,default,action,read_state,start 1462351219,default,action,read_state,end 1462351219,,ui,info,Current machine states:\\n\\ndefault (aws)\\n\\nThe EC2 instance is running. To stop this machine%!(VAGRANT_COMMA) you can run\\n`vagrant halt`. To destroy the machine%!(VAGRANT_COMMA) you can run `vagrant destroy`. ''' # Can compare tuples to Status class b/c Status is a collections.namedtuple. goal = [('default', 'running', 'aws')] v = vagrant.Vagrant(TD) parsed = v._parse_status(listing) assert goal == parsed, 'The parsing of the test listing did not match the goal.\nlisting={!r}\ngoal={!r}\nparsed_listing={!r}'.format(listing, goal, parsed) @with_setup(make_setup_vm(), teardown_vm) def test_vm_status(): ''' Test whether vagrant.status() correctly reports state of the VM, in a single-VM environment. ''' v = vagrant.Vagrant(TD) assert v.NOT_CREATED == v.status()[0].state, "Before going up status should be vagrant.NOT_CREATED" command = 'vagrant up' subprocess.check_call(command, cwd=TD, shell=True) assert v.RUNNING in v.status()[0].state, "After going up status should be vagrant.RUNNING" command = 'vagrant halt' subprocess.check_call(command, cwd=TD, shell=True) assert v.POWEROFF in v.status()[0].state, "After halting status should be vagrant.POWEROFF" command = 'vagrant destroy -f' subprocess.check_call(command, cwd=TD, shell=True) assert v.NOT_CREATED in v.status()[0].state, "After destroying status should be vagrant.NOT_CREATED" @with_setup(make_setup_vm(), teardown_vm) def test_vm_lifecycle(): ''' Test methods controlling the VM - init(), up(), halt(), destroy(). ''' v = vagrant.Vagrant(TD) # Test init by removing Vagrantfile, since v.init() will create one. os.unlink(os.path.join(TD, 'Vagrantfile')) v.init(TEST_BOX_NAME) assert v.NOT_CREATED == v.status()[0].state v.up() assert v.RUNNING == v.status()[0].state v.suspend() assert v.SAVED == v.status()[0].state v.halt() assert v.POWEROFF == v.status()[0].state v.destroy() assert v.NOT_CREATED == v.status()[0].state @with_setup(make_setup_vm(), teardown_vm) def test_vm_config(): ''' Test methods retrieving ssh config settings, like user, hostname, and port. ''' v = vagrant.Vagrant(TD) v.up() command = "vagrant ssh-config" ssh_config = compat.decode(subprocess.check_output(command, cwd=TD, shell=True)) parsed_config = dict(line.strip().split(None, 1) for line in ssh_config.splitlines() if line.strip() and not line.strip().startswith('#')) user = v.user() expected_user = parsed_config["User"] eq_(user, expected_user) hostname = v.hostname() expected_hostname = parsed_config["HostName"] eq_(hostname, expected_hostname) port = v.port() expected_port = parsed_config["Port"] eq_(port, expected_port) user_hostname = v.user_hostname() eq_(user_hostname, "{}@{}".format(expected_user, expected_hostname)) user_hostname_port = v.user_hostname_port() eq_(user_hostname_port, "{}@{}:{}".format(expected_user, expected_hostname, expected_port)) keyfile = v.keyfile() try: eq_(keyfile, parsed_config["IdentityFile"]) except AssertionError: # Vagrant 1.8 adds quotes around the filepath for the private key. eq_(keyfile, parsed_config["IdentityFile"].lstrip('"').rstrip('"')) @with_setup(make_setup_vm(), teardown_vm) def test_vm_sandbox_mode(): ''' Test methods for enabling/disabling the sandbox mode and committing/rolling back changes. This depends on the Sahara plugin. ''' # Only test Sahara if it is installed. # This leaves the testing of Sahara to people who care. sahara_installed = _plugin_installed(vagrant.Vagrant(TD), 'sahara') if not sahara_installed: return v = vagrant.SandboxVagrant(TD) sandbox_status = v.sandbox_status() assert sandbox_status == "unknown", "Before the VM goes up the status should be 'unknown', " + "got:'{}'".format(sandbox_status) v.up() sandbox_status = v.sandbox_status() assert sandbox_status == "off", "After the VM goes up the status should be 'off', " + "got:'{}'".format(sandbox_status) v.sandbox_on() sandbox_status = v.sandbox_status() assert sandbox_status == "on", "After enabling the sandbox mode the status should be 'on', " + "got:'{}'".format(sandbox_status) v.sandbox_off() sandbox_status = v.sandbox_status() assert sandbox_status == "off", "After disabling the sandbox mode the status should be 'off', " + "got:'{}'".format(sandbox_status) v.sandbox_on() v.halt() sandbox_status = v.sandbox_status() assert sandbox_status == "on", "After halting the VM the status should be 'on', " + "got:'{}'".format(sandbox_status) v.up() sandbox_status = v.sandbox_status() assert sandbox_status == "on", "After bringing the VM up again the status should be 'on', " + "got:'{}'".format(sandbox_status) test_file_contents = _read_test_file(v) print(test_file_contents) eq_(test_file_contents, None, "There should be no test file") _write_test_file(v, "foo") test_file_contents = _read_test_file(v) print(test_file_contents) eq_(test_file_contents, "foo", "The test file should read 'foo'") v.sandbox_rollback() time.sleep(10) # https://github.com/jedi4ever/sahara/issues/16 test_file_contents = _read_test_file(v) print(test_file_contents) eq_(test_file_contents, None, "There should be no test file") _write_test_file(v, "foo") test_file_contents = _read_test_file(v) print(test_file_contents) eq_(test_file_contents, "foo", "The test file should read 'foo'") v.sandbox_commit() _write_test_file(v, "bar") test_file_contents = _read_test_file(v) print(test_file_contents) eq_(test_file_contents, "bar", "The test file should read 'bar'") v.sandbox_rollback() time.sleep(10) # https://github.com/jedi4ever/sahara/issues/16 test_file_contents = _read_test_file(v) print(test_file_contents) eq_(test_file_contents, "foo", "The test file should read 'foo'") sandbox_status = v._parse_vagrant_sandbox_status("Usage: ...") eq_(sandbox_status, "not installed", "When 'vagrant sandbox status'" + " outputs vagrant help status should be 'not installed', " + "got:'{}'".format(sandbox_status)) v.destroy() sandbox_status = v.sandbox_status() assert sandbox_status == "unknown", "After destroying the VM the status should be 'unknown', " + "got:'{}'".format(sandbox_status) @with_setup(make_setup_vm(), teardown_vm) def test_boxes(): ''' Test methods for manipulating boxes - adding, listing, removing. ''' v = vagrant.Vagrant(TD) box_name = "python-vagrant-dummy-box" provider = "virtualbox" # Start fresh with no dummy box if box_name in list_box_names(): subprocess.check_call(['vagrant', 'box', 'remove', box_name]) # Test that there is no dummy box listed ok_(box_name not in [b.name for b in v.box_list()], "There should be no dummy box before it's added.") # Add a box v.box_add(box_name, TEST_BOX_URL) # Test that there is a dummy box listed box_listing = v.box_list() ok_((box_name, provider) in [(b.name, b.provider) for b in box_listing], 'The box {box} for provider {provider} should be in the list returned by box_list(). box_list()={box_listing}'.format( box=box_name, provider=provider, box_listing=box_listing)) # Remove dummy box using a box name and provider v.box_remove(box_name, provider) # Test that there is no dummy box listed ok_(box_name not in [b.name for b in v.box_list()], "There should be no dummy box after it has been removed.") @with_setup(make_setup_vm(SHELL_PROVISION_VAGRANTFILE), teardown_vm) def test_provisioning(): ''' Test provisioning support. The tested provision config creates a file on the vm with the contents 'foo'. ''' v = vagrant.Vagrant(TD) v.up(no_provision=True) test_file_contents = _read_test_file(v) eq_(test_file_contents, None, "There should be no test file after up()") v.provision() test_file_contents = _read_test_file(v) print("Contents: {}".format(test_file_contents)) eq_(test_file_contents, "foo", "The test file should contain 'foo'") @with_setup(make_setup_vm(MULTIVM_VAGRANTFILE), teardown_vm) def test_multivm_lifecycle(): v = vagrant.Vagrant(TD) # test getting multiple statuses at once eq_(v.status(VM_1)[0].state, v.NOT_CREATED) eq_(v.status(VM_2)[0].state, v.NOT_CREATED) v.up(vm_name=VM_1) eq_(v.status(VM_1)[0].state, v.RUNNING) eq_(v.status(VM_2)[0].state, v.NOT_CREATED) # start both vms v.up() eq_(v.status(VM_1)[0].state, v.RUNNING) eq_(v.status(VM_2)[0].state, v.RUNNING) v.halt(vm_name=VM_1) eq_(v.status(VM_1)[0].state, v.POWEROFF) eq_(v.status(VM_2)[0].state, v.RUNNING) v.destroy(vm_name=VM_1) eq_(v.status(VM_1)[0].state, v.NOT_CREATED) eq_(v.status(VM_2)[0].state, v.RUNNING) v.suspend(vm_name=VM_2) eq_(v.status(VM_1)[0].state, v.NOT_CREATED) eq_(v.status(VM_2)[0].state, v.SAVED) v.destroy(vm_name=VM_2) eq_(v.status(VM_1)[0].state, v.NOT_CREATED) eq_(v.status(VM_2)[0].state, v.NOT_CREATED) @with_setup(make_setup_vm(MULTIVM_VAGRANTFILE), teardown_vm) def test_multivm_config(): ''' Test methods retrieving configuration settings. ''' v = vagrant.Vagrant(TD, quiet_stdout=False, quiet_stderr=False) v.up(vm_name=VM_1) command = "vagrant ssh-config " + VM_1 ssh_config = compat.decode(subprocess.check_output(command, cwd=TD, shell=True)) parsed_config = dict(line.strip().split(None, 1) for line in ssh_config.splitlines() if line.strip() and not line.strip().startswith('#')) user = v.user(vm_name=VM_1) expected_user = parsed_config["User"] eq_(user, expected_user) hostname = v.hostname(vm_name=VM_1) expected_hostname = parsed_config["HostName"] eq_(hostname, expected_hostname) port = v.port(vm_name=VM_1) expected_port = parsed_config["Port"] eq_(port, expected_port) user_hostname = v.user_hostname(vm_name=VM_1) eq_(user_hostname, "{}@{}".format(expected_user, expected_hostname)) user_hostname_port = v.user_hostname_port(vm_name=VM_1) eq_(user_hostname_port, "{}@{}:{}".format(expected_user, expected_hostname, expected_port)) keyfile = v.keyfile(vm_name=VM_1) try: eq_(keyfile, parsed_config["IdentityFile"]) except AssertionError: # Vagrant 1.8 adds quotes around the filepath for the private key. eq_(keyfile, parsed_config["IdentityFile"].lstrip('"').rstrip('"')) @with_setup(make_setup_vm(), teardown_vm) def test_ssh_command(): ''' Test executing a command via ssh on a vm. ''' v = vagrant.Vagrant(TD) v.up() output = v.ssh(command='echo hello') assert output.strip() == 'hello' @with_setup(make_setup_vm(MULTIVM_VAGRANTFILE), teardown_vm) def test_ssh_command_multivm(): ''' Test executing a command via ssh on a specific vm ''' v = vagrant.Vagrant(TD) v.up() output = v.ssh(vm_name=VM_1, command='echo hello') assert output.strip() == 'hello' output = v.ssh(vm_name=VM_2, command='echo I like your hat') assert output.strip() == 'I like your hat' @with_setup(make_setup_vm(), teardown_vm) def test_streaming_output(): """ Test streaming output of up or reload. """ test_string = 'Waiting for machine to boot.' v = vagrant.Vagrant(TD) with assert_raises(subprocess.CalledProcessError): v.up(vm_name='incorrect-name') streaming_up = False for line in v.up(stream_output=True): print('output line:', line) if test_string in line: streaming_up = True assert streaming_up streaming_reload = False for line in v.reload(stream_output=True): print('output line:', line) if test_string in line: streaming_reload = True assert streaming_reload def test_make_file_cm(): filename = os.path.join(TD, 'test.log') if os.path.exists(filename): os.remove(filename) # Test writing to the filehandle yielded by cm cm = vagrant.make_file_cm(filename) with cm() as fh: fh.write('one\n') with open(filename) as read_fh: assert read_fh.read() == 'one\n' # Test appending to the file yielded by cm with cm() as fh: fh.write('two\n') with open(filename) as read_fh: assert read_fh.read() == 'one\ntwo\n' def _execute_command_in_vm(v, command): ''' Run command via ssh on the test vagrant box. Returns a tuple of the return code and output of the command. ''' vagrant_exe = vagrant.get_vagrant_executable() if not vagrant_exe: raise RuntimeError(vagrant.VAGRANT_NOT_FOUND_WARNING) # ignore the fact that this host is not in our known hosts ssh_command = [vagrant_exe, 'ssh', '-c', command] return compat.decode(subprocess.check_output(ssh_command, cwd=v.root)) def _write_test_file(v, file_contents): ''' Writes given contents to the test file. ''' command = "echo '{}' > {}".format(file_contents, TEST_FILE_PATH) _execute_command_in_vm(v, command) def _read_test_file(v): ''' Returns the contents of the test file stored in the VM or None if there is no file. ''' command = 'cat {}'.format(TEST_FILE_PATH) try: output = _execute_command_in_vm(v, command) return output.strip() except subprocess.CalledProcessError: return None def _plugin_installed(v, plugin_name): plugins = v.plugin_list() return plugin_name in [plugin.name for plugin in plugins] python-vagrant-0.5.15/tests/test_vagrant_test_case.py0000664000076500000240000000361612314324155022742 0ustar tfdstaff00000000000000""" Tests for the various functionality provided by the VagrantTestCase class There are a handful of classes to try to provide multiple different varying samples of possible setups """ import os from vagrant import Vagrant from vagrant.test import VagrantTestCase def get_vagrant_root(test_vagrant_root_path): return os.path.dirname(os.path.realpath(__file__)) + '/vagrantfiles/' + test_vagrant_root_path SINGLE_BOX = get_vagrant_root('single_box') MULTI_BOX = get_vagrant_root('multi_box') class AllMultiBoxesTests(VagrantTestCase): """Tests for a multiple box setup where vagrant_boxes is left empty""" vagrant_root = MULTI_BOX def test_default_boxes_list(self): """Tests that all boxes in a Vagrantfile if vagrant_boxes is not defined""" self.assertGreater(len(self.vagrant_boxes), 0) class SingleBoxTests(VagrantTestCase): """Tests for a single box setup""" vagrant_root = SINGLE_BOX def test_box_up(self): """Tests that the box starts as expected""" state = self.vagrant.status(vm_name=self.vagrant_boxes[0])[0].state self.assertEqual(state, Vagrant.RUNNING) class SpecificMultiBoxTests(VagrantTestCase): """Tests for a multiple box setup where only some of the boxes are to be on""" vagrant_boxes = ['precise32'] vagrant_root = MULTI_BOX def test_all_boxes_up(self): """Tests that all boxes listed are up after starting""" for box_name in self.vagrant_boxes: state = self.vagrant.status(vm_name=box_name)[0].state self.assertEqual(state, Vagrant.RUNNING) def test_unlisted_boxes_ignored(self): """Tests that the boxes not listed are not brought up""" for box_name in [s.name for s in self.vagrant.status()]: if box_name in self.vagrant_boxes: self.assertBoxUp(box_name) else: self.assertBoxNotCreated(box_name) python-vagrant-0.5.15/tests/vagrantfiles/0000775000076500000240000000000013104101163020302 5ustar tfdstaff00000000000000python-vagrant-0.5.15/tests/vagrantfiles/multi_box/0000775000076500000240000000000013104101163022304 5ustar tfdstaff00000000000000python-vagrant-0.5.15/tests/vagrantfiles/multi_box/Vagrantfile0000664000076500000240000000050712251110004024467 0ustar tfdstaff00000000000000# -*- mode: ruby -*- # vi: set ft=ruby : VAGRANTFILE_API_VERSION = "2" boxes = ['precise32', 'precise64'] Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| boxes.each do |box| config.vm.define box do |conf| conf.vm.box = box conf.vm.box_url = "http://files.vagrantup.com/#{box}.box" end end end python-vagrant-0.5.15/tests/vagrantfiles/multivm_Vagrantfile0000644000076500000240000000063612132015210024244 0ustar tfdstaff00000000000000 # http://docs.vagrantup.com/v2/multi-machine/index.html # http://docs.vagrantup.com/v2/networking/ Vagrant.configure("2") do |config| config.vm.define :web do |web| web.vm.box = "python-vagrant-base" web.vm.network :forwarded_port, guest: 80, host: 8080 end config.vm.define :db do |db| db.vm.box = "python-vagrant-base" db.vm.network :forwarded_port, guest: 3306, host: 3306 end end python-vagrant-0.5.15/tests/vagrantfiles/shell_provision_Vagrantfile0000664000076500000240000000030612314111052025765 0ustar tfdstaff00000000000000 Vagrant.configure("2") do |config| config.vm.box = "python-vagrant-base" # test v.provision() config.vm.provision :shell, :inline => "echo 'foo' > /home/vagrant/python_vagrant_test_file" end python-vagrant-0.5.15/tests/vagrantfiles/single_box/0000775000076500000240000000000013104101163022433 5ustar tfdstaff00000000000000python-vagrant-0.5.15/tests/vagrantfiles/single_box/Vagrantfile0000664000076500000240000000034312251110004024614 0ustar tfdstaff00000000000000# -*- mode: ruby -*- # vi: set ft=ruby : VAGRANTFILE_API_VERSION = "2" Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.box = 'precise64' config.vm.box_url = 'http://files.vagrantup.com/precise64.box' end python-vagrant-0.5.15/tests/vagrantfiles/vm_Vagrantfile0000664000076500000240000000012012314077206023176 0ustar tfdstaff00000000000000 Vagrant.configure("2") do |config| config.vm.box = "python-vagrant-base" end python-vagrant-0.5.15/vagrant/0000775000076500000240000000000013104101163016115 5ustar tfdstaff00000000000000python-vagrant-0.5.15/vagrant/__init__.py0000664000076500000240000012464113104101111020227 0ustar tfdstaff00000000000000''' Python bindings for working with Vagrant and Vagrantfiles. Do useful things with the `vagrant` CLI without the boilerplate (and errors) of calling `vagrant` and parsing the results. The API attempts to conform closely to the API of the `vagrant` command line, including method names and parameter names. Documentation of usage, testing, installation, etc., can be found at https://github.com/todddeluca/python-vagrant. ''' # std import collections import contextlib import itertools import os import re import subprocess import sys import logging # local from . import compat # python package version # should match r"^__version__ = '(?P[^']+)'$" for setup.py __version__ = '0.5.15' log = logging.getLogger(__name__) ########################################### # Determine Where The Vagrant Executable Is VAGRANT_NOT_FOUND_WARNING = 'The Vagrant executable cannot be found. ' \ 'Please check if it is in the system path.' def which(program): ''' Emulate unix 'which' command. If program is a path to an executable file (i.e. it contains any directory components, like './myscript'), return program. Otherwise, if an executable file matching program is found in one of the directories in the PATH environment variable, return the first match found. On Windows, if PATHEXT is defined and program does not include an extension, include the extensions in PATHEXT when searching for a matching executable file. Return None if no executable file is found. http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python/377028#377028 https://github.com/webcoyote/vagrant/blob/f70507062e3b30c00db1f0d8b90f9245c4c997d4/lib/vagrant/util/file_util.rb Python3.3+ implementation: https://hg.python.org/cpython/file/default/Lib/shutil.py ''' def is_exe(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) # Shortcut: If program contains any dir components, do not search the path # e.g. './backup', '/bin/ls' if os.path.dirname(program): if is_exe(program): return program else: return None # Are we on windows? # http://stackoverflow.com/questions/1325581/how-do-i-check-if-im-running-on-windows-in-python windows = (os.name == 'nt') # Or cygwin? # https://docs.python.org/2/library/sys.html#sys.platform cygwin = sys.platform.startswith('cygwin') # Paths: a list of directories path_str = os.environ.get('PATH', os.defpath) if not path_str: paths = [] else: paths = path_str.split(os.pathsep) # The current directory takes precedence on Windows. if windows: paths.insert(0, os.curdir) # Only search PATH if there is one to search. if not paths: return None # Files: add any necessary extensions to program # On cygwin and non-windows systems do not add extensions when searching # for the executable if cygwin or not windows: files = [program] else: # windows path extensions in PATHEXT. # e.g. ['.EXE', '.CMD', '.BAT'] # http://environmentvariables.org/PathExt # This might not properly use extensions that have been "registered" in # Windows. In the future it might make sense to use one of the many # "which" packages on PyPI. exts = os.environ.get('PATHEXT', '').split(os.pathsep) # if the program ends with one of the extensions, only test that one. # otherwise test all the extensions. matching_exts = [ext for ext in exts if program.lower().endswith(ext.lower())] if matching_exts: files = [program + ext for ext in matching_exts] else: files = [program + ext for ext in exts] # Check each combination of path, program, and extension, returning # the first combination that exists and is executable. for path in paths: for f in files: fpath = os.path.normcase(os.path.join(path, f)) if is_exe(fpath): return fpath return None # The full path to the vagrant executable, e.g. '/usr/bin/vagrant' def get_vagrant_executable(): return which('vagrant') if get_vagrant_executable() is None: log.warn(VAGRANT_NOT_FOUND_WARNING) # Classes for listings of Statuses, Boxes, and Plugins Status = collections.namedtuple('Status', ['name', 'state', 'provider']) Box = collections.namedtuple('Box', ['name', 'provider', 'version']) Plugin = collections.namedtuple('Plugin', ['name', 'version', 'system']) ######################################################################### # Context Managers for Handling the Output of Vagrant Subprocess Commands @contextlib.contextmanager def stdout_cm(): ''' Redirect the stdout or stderr of the child process to sys.stdout. ''' yield sys.stdout @contextlib.contextmanager def stderr_cm(): ''' Redirect the stdout or stderr of the child process to sys.stderr. ''' yield sys.stderr @contextlib.contextmanager def devnull_cm(): ''' Redirect the stdout or stderr of the child process to /dev/null. ''' with open(os.devnull, 'w') as fh: yield fh @contextlib.contextmanager def none_cm(): ''' Use the stdout or stderr file handle of the parent process. ''' yield None def make_file_cm(filename, mode='a'): ''' Open a file for appending and yield the open filehandle. Close the filehandle after yielding it. This is useful for creating a context manager for logging the output of a `Vagrant` instance. filename: a path to a file mode: The mode in which to open the file. Defaults to 'a', append Usage example: log_cm = make_file_cm('application.log') v = Vagrant(out_cm=log_cm, err_cm=log_cm) ''' @contextlib.contextmanager def cm(): with open(filename, mode=mode) as fh: yield fh return cm class Vagrant(object): ''' Object to up (launch) and destroy (terminate) vagrant virtual machines, to check the status of the machine and to report on the configuration of the machine. Works by using the `vagrant` executable and a `Vagrantfile`. ''' # Some machine-readable state values returned by status # There are likely some missing, but if you use vagrant you should # know what you are looking for. # These exist partly for convenience and partly to document the output # of vagrant. RUNNING = 'running' # vagrant up NOT_CREATED = 'not_created' # vagrant destroy POWEROFF = 'poweroff' # vagrant halt ABORTED = 'aborted' # The VM is in an aborted state SAVED = 'saved' # vagrant suspend # LXC statuses STOPPED = 'stopped' FROZEN = 'frozen' # libvirt SHUTOFF = 'shutoff' BASE_BOXES = { 'ubuntu-Lucid32': 'http://files.vagrantup.com/lucid32.box', 'ubuntu-lucid32': 'http://files.vagrantup.com/lucid32.box', 'ubuntu-lucid64': 'http://files.vagrantup.com/lucid64.box', 'ubuntu-precise32': 'http://files.vagrantup.com/precise32.box', 'ubuntu-precise64': 'http://files.vagrantup.com/precise64.box', } def __init__(self, root=None, quiet_stdout=True, quiet_stderr=True, env=None, out_cm=None, err_cm=None): ''' root: a directory containing a file named Vagrantfile. Defaults to os.getcwd(). This is the directory and Vagrantfile that the Vagrant instance will operate on. env: a dict of environment variables (string keys and values) passed to the vagrant command subprocess or None. Defaults to None. If env is None, `subprocess.Popen` uses the current process environment. out_cm: a no-argument function that returns a ContextManager that yields a filehandle or other object suitable to be passed as the `stdout` parameter of a subprocess that runs a vagrant command. Using a context manager allows one to close the filehandle in case of an Exception, if necessary. Defaults to none_cm, a context manager that yields None. See `make_file_cm` for an example of how to log stdout to a file. Note that commands that parse the output of a vagrant command, like `status`, capture output for their own use, ignoring the value of `out_cm` and `quiet_stdout`. err_cm: a no-argument function that returns a ContextManager, like out_cm, for handling the stderr of the vagrant subprocess. Defaults to none_cm. quiet_stdout: Ignored if out_cm is not None. If True, the stdout of vagrant commands whose output is not captured for further processing will be sent to devnull. quiet_stderr: Ignored if out_cm is not None. If True, the stderr of vagrant commands whose output is not captured for further processing will be sent to devnull. ''' self.root = os.path.abspath(root) if root is not None else os.getcwd() self._cached_conf = {} self._vagrant_exe = None # cache vagrant executable path self.env = env if out_cm is not None: self.out_cm = out_cm elif quiet_stdout: self.out_cm = devnull_cm else: # Using none_cm instead of stdout_cm, because in some situations, # e.g. using nosetests, sys.stdout is a StringIO object, not a # filehandle. Also, passing None to the subprocess is consistent # with past behavior. self.out_cm = none_cm if err_cm is not None: self.err_cm = err_cm elif quiet_stderr: self.err_cm = devnull_cm else: self.err_cm = none_cm def version(self): ''' Return the installed vagrant version, as a string, e.g. '1.5.0' ''' output = self._run_vagrant_command(['--version']) m = re.search(r'^Vagrant (?P.+)$', output) if m is None: raise Exception('Failed to parse vagrant --version output. output={!r}'.format(output)) return m.group('version') def init(self, box_name=None, box_url=None): ''' From the Vagrant docs: This initializes the current directory to be a Vagrant environment by creating an initial Vagrantfile if one doesn't already exist. If box_name is given, it will prepopulate the config.vm.box setting in the created Vagrantfile. If box_url is given, it will prepopulate the config.vm.box_url setting in the created Vagrantfile. Note: if box_url is given, box_name should also be given. ''' self._call_vagrant_command(['init', box_name, box_url]) def up(self, no_provision=False, provider=None, vm_name=None, provision=None, provision_with=None, stream_output=False): ''' Invoke `vagrant up` to start a box or boxes, possibly streaming the command output. vm_name=None: name of VM. provision_with: optional list of provisioners to enable. provider: Back the machine with a specific provider no_provision: if True, disable provisioning. Same as 'provision=False'. provision: optional boolean. Enable or disable provisioning. Default behavior is to use the underlying vagrant default. stream_output: if True, return a generator that yields each line of the output of running the command. Consume the generator or the subprocess might hang. if False, None is returned and the command is run to completion without streaming the output. Defaults to False. Note: If provision and no_provision are not None, no_provision will be ignored. returns: None or a generator yielding lines of output. ''' provider_arg = '--provider=%s' % provider if provider else None prov_with_arg = None if provision_with is None else '--provision-with' providers_arg = None if provision_with is None else ','.join(provision_with) # For the sake of backward compatibility, no_provision is allowed. # However it is ignored if provision is set. if provision is not None: no_provision = None no_provision_arg = '--no-provision' if no_provision else None provision_arg = None if provision is None else '--provision' if provision else '--no-provision' args = ['up', vm_name, no_provision_arg, provision_arg, provider_arg, prov_with_arg, providers_arg] if stream_output: generator = self._stream_vagrant_command(args) else: self._call_vagrant_command(args) self._cached_conf[vm_name] = None # remove cached configuration return generator if stream_output else None def provision(self, vm_name=None, provision_with=None): ''' Runs the provisioners defined in the Vagrantfile. vm_name: optional VM name string. provision_with: optional list of provisioners to enable. e.g. ['shell', 'chef_solo'] ''' prov_with_arg = None if provision_with is None else '--provision-with' providers_arg = None if provision_with is None else ','.join(provision_with) self._call_vagrant_command(['provision', vm_name, prov_with_arg, providers_arg]) def reload(self, vm_name=None, provision=None, provision_with=None, stream_output=False): ''' Quoting from Vagrant docs: > The equivalent of running a halt followed by an up. > This command is usually required for changes made in the Vagrantfile to take effect. After making any modifications to the Vagrantfile, a reload should be called. > The configured provisioners will not run again, by default. You can force the provisioners to re-run by specifying the --provision flag. provision: optional boolean. Enable or disable provisioning. Default behavior is to use the underlying vagrant default. provision_with: optional list of provisioners to enable. e.g. ['shell', 'chef_solo'] stream_output: if True, return a generator that yields each line of the output of running the command. Consume the generator or the subprocess might hang. if False, None is returned and the command is run to completion without streaming the output. Defaults to False. returns: None or a generator yielding lines of output. ''' prov_with_arg = None if provision_with is None else '--provision-with' providers_arg = None if provision_with is None else ','.join(provision_with) provision_arg = None if provision is None else '--provision' if provision else '--no-provision' args = ['reload', vm_name, provision_arg, prov_with_arg, providers_arg] if stream_output: generator = self._stream_vagrant_command(args) else: self._call_vagrant_command(args) self._cached_conf[vm_name] = None # remove cached configuration return generator if stream_output else None def suspend(self, vm_name=None): ''' Suspend/save the machine. ''' self._call_vagrant_command(['suspend', vm_name]) self._cached_conf[vm_name] = None # remove cached configuration def resume(self, vm_name=None): ''' Resume suspended machine. ''' self._call_vagrant_command(['resume', vm_name]) self._cached_conf[vm_name] = None # remove cached configuration def halt(self, vm_name=None, force=False): ''' Halt the Vagrant box. force: If True, force shut down. ''' force_opt = '--force' if force else None self._call_vagrant_command(['halt', vm_name, force_opt]) self._cached_conf[vm_name] = None # remove cached configuration def destroy(self, vm_name=None): ''' Terminate the running Vagrant box. ''' self._call_vagrant_command(['destroy', vm_name, '--force']) self._cached_conf[vm_name] = None # remove cached configuration def status(self, vm_name=None): ''' Return the results of a `vagrant status` call as a list of one or more Status objects. A Status contains the following attributes: - name: The VM name in a multi-vm environment. 'default' otherwise. - state: The state of the underlying guest machine (i.e. VM). - provider: the name of the VM provider, e.g. 'virtualbox'. None if no provider is output by vagrant. Example return values for a multi-VM environment: [Status(name='web', state='not created', provider='virtualbox'), Status(name='db', state='not created', provider='virtualbox')] And for a single-VM environment: [Status(name='default', state='not created', provider='virtualbox')] Possible states include, but are not limited to (since new states are being added as Vagrant evolves): - 'not_created' if the vm is destroyed - 'running' if the vm is up - 'poweroff' if the vm is halted - 'saved' if the vm is suspended - 'aborted' if the vm is aborted Implementation Details: This command uses the `--machine-readable` flag added in Vagrant 1.5, mapping the target name, state, and provider-name to a Status object. Example with no VM name and multi-vm Vagrantfile: $ vagrant status --machine-readable 1424098924,web,provider-name,virtualbox 1424098924,web,state,running 1424098924,web,state-human-short,running 1424098924,web,state-human-long,The VM is running. To stop this VM%!(VAGRANT_COMMA) you can run `vagrant halt` to\nshut it down forcefully%!(VAGRANT_COMMA) or you can run `vagrant suspend` to simply\nsuspend the virtual machine. In either case%!(VAGRANT_COMMA) to restart it again%!(VAGRANT_COMMA)\nsimply run `vagrant up`. 1424098924,db,provider-name,virtualbox 1424098924,db,state,not_created 1424098924,db,state-human-short,not created 1424098924,db,state-human-long,The environment has not yet been created. Run `vagrant up` to\ncreate the environment. If a machine is not created%!(VAGRANT_COMMA) only the\ndefault provider will be shown. So if a provider is not listed%!(VAGRANT_COMMA)\nthen the machine is not created for that environment. Example with VM name: $ vagrant status --machine-readable web 1424099027,web,provider-name,virtualbox 1424099027,web,state,running 1424099027,web,state-human-short,running 1424099027,web,state-human-long,The VM is running. To stop this VM%!(VAGRANT_COMMA) you can run `vagrant halt` to\nshut it down forcefully%!(VAGRANT_COMMA) or you can run `vagrant suspend` to simply\nsuspend the virtual machine. In either case%!(VAGRANT_COMMA) to restart it again%!(VAGRANT_COMMA)\nsimply run `vagrant up`. Example with no VM name and single-vm Vagrantfile: $ vagrant status --machine-readable 1424100021,default,provider-name,virtualbox 1424100021,default,state,not_created 1424100021,default,state-human-short,not created 1424100021,default,state-human-long,The environment has not yet been created. Run `vagrant up` to\ncreate the environment. If a machine is not created%!(VAGRANT_COMMA) only the\ndefault provider will be shown. So if a provider is not listed%!(VAGRANT_COMMA)\nthen the machine is not created for that environment. Error example with incorrect VM name: $ vagrant status --machine-readable api 1424099042,,error-exit,Vagrant::Errors::MachineNotFound,The machine with the name 'api' was not found configured for\nthis Vagrant environment. Error example with missing Vagrantfile: $ vagrant status --machine-readable 1424099094,,error-exit,Vagrant::Errors::NoEnvironmentError,A Vagrant environment or target machine is required to run this\ncommand. Run `vagrant init` to create a new Vagrant environment. Or%!(VAGRANT_COMMA)\nget an ID of a target machine from `vagrant global-status` to run\nthis command on. A final option is to change to a directory with a\nVagrantfile and to try again. ''' # machine-readable output are CSV lines output = self._run_vagrant_command(['status', '--machine-readable', vm_name]) return self._parse_status(output) def _parse_status(self, output): ''' Unit testing is so much easier when Vagrant is removed from the equation. ''' parsed = self._parse_machine_readable_output(output) statuses = [] # group tuples by target name # assuming tuples are sorted by target name, this should group all # the tuples with info for each target. for target, tuples in itertools.groupby(parsed, lambda tup: tup[1]): # transform tuples into a dict mapping "type" to "data" info = {kind: data for timestamp, _, kind, data in tuples} status = Status(name=target, state=info.get('state'), provider=info.get('provider-name')) statuses.append(status) return statuses def conf(self, ssh_config=None, vm_name=None): ''' Parse ssh_config into a dict containing the keys defined in ssh_config, which should include these keys (listed with example values): 'User' (e.g. 'vagrant'), 'HostName' (e.g. 'localhost'), 'Port' (e.g. '2222'), 'IdentityFile' (e.g. '/home/todd/.ssh/id_dsa'). Cache the parsed configuration dict. Return the dict. If ssh_config is not given, return the cached dict. If there is no cached configuration, call ssh_config() to get the configuration, then parse, cache, and return the config dict. Calling ssh_config() raises an Exception if the Vagrant box has not yet been created or has been destroyed. vm_name: required in a Multi-VM Vagrant environment. This name will be used to get the configuration for the named vm and associate the config with the vm name in the cache. ssh_config: a valid ssh confige file host section. Defaults to the value returned from ssh_config(). For speed, the configuration parsed from ssh_config is cached for subsequent calls. ''' if self._cached_conf.get(vm_name) is None or ssh_config is not None: if ssh_config is None: ssh_config = self.ssh_config(vm_name=vm_name) conf = self._parse_config(ssh_config) self._cached_conf[vm_name] = conf return self._cached_conf[vm_name] def ssh_config(self, vm_name=None): ''' Return the output of 'vagrant ssh-config' which appears to be a valid Host section suitable for use in an ssh config file. Raises an Exception if the Vagrant box has not yet been created or has been destroyed. vm_name: required in a multi-VM environment. Example output: Host default HostName 127.0.0.1 User vagrant Port 2222 UserKnownHostsFile /dev/null StrictHostKeyChecking no PasswordAuthentication no IdentityFile /Users/todd/.vagrant.d/insecure_private_key IdentitiesOnly yes ''' # capture ssh configuration from vagrant return self._run_vagrant_command(['ssh-config', vm_name]) def user(self, vm_name=None): ''' Return the ssh user of the vagrant box, e.g. 'vagrant' or None if there is no user in the ssh_config. Raises an Exception if the Vagrant box has not yet been created or has been destroyed. ''' return self.conf(vm_name=vm_name).get('User') def hostname(self, vm_name=None): ''' Return the vagrant box hostname, e.g. '127.0.0.1' or None if there is no hostname in the ssh_config. Raises an Exception if the Vagrant box has not yet been created or has been destroyed. ''' return self.conf(vm_name=vm_name).get('HostName') def port(self, vm_name=None): ''' Return the vagrant box ssh port, e.g. '2222' or None if there is no port in the ssh_config. Raises an Exception if the Vagrant box has not yet been created or has been destroyed. ''' return self.conf(vm_name=vm_name).get('Port') def keyfile(self, vm_name=None): ''' Return the path to the private key used to log in to the vagrant box or None if there is no keyfile (IdentityFile) in the ssh_config. E.g. '/Users/todd/.vagrant.d/insecure_private_key' Raises an Exception if the Vagrant box has not yet been created or has been destroyed. KeyFile is a synonym for IdentityFile. ''' return self.conf(vm_name=vm_name).get('IdentityFile') def user_hostname(self, vm_name=None): ''' Return a string combining user and hostname, e.g. 'vagrant@127.0.0.1'. This string is suitable for use in an ssh commmand. If user is None or empty, it will be left out of the string, e.g. 'localhost'. If hostname is None, have bigger problems. Raises an Exception if the Vagrant box has not yet been created or has been destroyed. ''' user = self.user(vm_name=vm_name) user_prefix = user + '@' if user else '' return user_prefix + self.hostname(vm_name=vm_name) def user_hostname_port(self, vm_name=None): ''' Return a string combining user, hostname and port, e.g. 'vagrant@127.0.0.1:2222'. This string is suitable for use with Fabric, in env.hosts. If user or port is None or empty, they will be left out of the string. E.g. 'vagrant@localhost', or 'localhost:2222' or 'localhost'. If hostname is None, you have bigger problems. Raises an Exception if the Vagrant box has not yet been created or has been destroyed. ''' user = self.user(vm_name=vm_name) port = self.port(vm_name=vm_name) user_prefix = user + '@' if user else '' port_suffix = ':' + port if port else '' return user_prefix + self.hostname(vm_name=vm_name) + port_suffix def box_add(self, name, url, provider=None, force=False): ''' Adds a box with given name, from given url. force: If True, overwrite an existing box if it exists. ''' force_opt = '--force' if force else None cmd = ['box', 'add', name, url, force_opt] if provider is not None: cmd += ['--provider', provider] self._call_vagrant_command(cmd) def box_list(self): ''' Run `vagrant box list --machine-readable` and return a list of Box objects containing the results. A Box object has the following attributes: - name: the box-name. - provider: the box-provider. - version: the box-version. Example output: [Box(name='precise32', provider='virtualbox', version='0'), Box(name='precise64', provider='virtualbox', version=None), Box(name='trusty64', provider='virtualbox', version=None)] Implementation Details: Example machine-readable box listing output: 1424141572,,box-name,precise64 1424141572,,box-provider,virtualbox 1424141572,,box-version,0 1424141572,,box-name,python-vagrant-base 1424141572,,box-provider,virtualbox 1424141572,,box-version,0 Note that the box information iterates within the same blank target value (the 2nd column). ''' # machine-readable output are CSV lines output = self._run_vagrant_command(['box', 'list', '--machine-readable']) return self._parse_box_list(output) def package(self, vm_name=None, base=None, output=None, vagrantfile=None): ''' Packages a running vagrant environment into a box. vm_name=None: name of VM. base=None: name of a VM in virtualbox to package as a base box output=None: name of the file to output vagrantfile=None: Vagrantfile to package with this box ''' cmd = ['package', vm_name] if output is not None: cmd += ['--output', output] if vagrantfile is not None: cmd += ['--vagrantfile', vagrantfile] self._call_vagrant_command(cmd) def snapshot_push(self): ''' This takes a snapshot and pushes it onto the snapshot stack. ''' self._call_vagrant_command(['snapshot', 'push']) def snapshot_pop(self): ''' This command is the inverse of vagrant snapshot push: it will restore the pushed state. ''' NO_SNAPSHOTS_PUSHED = 'No pushed snapshot found!' output = self._run_vagrant_command(['snapshot', 'pop']) if NO_SNAPSHOTS_PUSHED in output: raise RuntimeError(NO_SNAPSHOTS_PUSHED) def snapshot_save(self, name): ''' This command saves a new named snapshot. If this command is used, the push and pop subcommands cannot be safely used. ''' self._call_vagrant_command(['snapshot', 'save', name]) def snapshot_restore(self, name): ''' This command restores the named snapshot. ''' self._call_vagrant_command(['snapshot', 'restore', name]) def snapshot_list(self): ''' This command will list all the snapshots taken. ''' NO_SNAPSHOTS_TAKEN = 'No snapshots have been taken yet!' output = self._run_vagrant_command(['snapshot', 'list']) if NO_SNAPSHOTS_TAKEN in output: return [] else: return output.splitlines() def snapshot_delete(self, name): ''' This command will delete the named snapshot. ''' self._call_vagrant_command(['snapshot', 'delete', name]) def ssh(self, vm_name=None, command=None, extra_ssh_args=None): ''' Execute a command via ssh on the vm specified. command: The command to execute via ssh. extra_ssh_args: Corresponds to '--' option in the vagrant ssh command Returns the output of running the command. ''' cmd = ['ssh', vm_name, '--command', command] if extra_ssh_args is not None: cmd += ['--', extra_ssh_args] return self._run_vagrant_command(cmd) def _parse_box_list(self, output): ''' Remove Vagrant usage for unit testing ''' # Parse box list output boxes = [] # initialize box values name = provider = version = None for timestamp, target, kind, data in self._parse_machine_readable_output(output): if kind == 'box-name': # finish the previous box, if any if name is not None: boxes.append(Box(name=name, provider=provider, version=version)) # start a new box name = data # box name provider = version = None elif kind == 'box-provider': provider = data elif kind == 'box-version': version = data # finish the previous box, if any if name is not None: boxes.append(Box(name=name, provider=provider, version=version)) return boxes def box_update(self, name, provider): ''' Updates the box matching name and provider. It is an error if no box matches name and provider. ''' self._call_vagrant_command(['box', 'update', name, provider]) def box_remove(self, name, provider): ''' Removes the box matching name and provider. It is an error if no box matches name and provider. ''' self._call_vagrant_command(['box', 'remove', name, provider]) def plugin_list(self): ''' Return a list of Plugin objects containing the following information about installed plugins: - name: The plugin name, as a string. - version: The plugin version, as a string. - system: A boolean, presumably indicating whether this plugin is a "core" part of vagrant, though the feature is not yet documented in the Vagrant 1.5 docs. Example output: [Plugin(name='sahara', version='0.0.16', system=False), Plugin(name='vagrant-login', version='1.0.1', system=True), Plugin(name='vagrant-share', version='1.0.1', system=True)] Implementation Details: Example output of `vagrant plugin list --machine-readable`: $ vagrant plugin list --machine-readable 1424145521,,plugin-name,sahara 1424145521,sahara,plugin-version,0.0.16 1424145521,,plugin-name,vagrant-share 1424145521,vagrant-share,plugin-version,1.1.3%!(VAGRANT_COMMA) system Note that the information for each plugin seems grouped within consecutive lines. That information is also associated sometimes with an empty target name and sometimes with the plugin name as the target name. Note also that a plugin version can be like '0.0.16' or '1.1.3, system'. ''' output = self._run_vagrant_command(['plugin', 'list', '--machine-readable']) return self._parse_plugin_list(output) def _parse_plugin_list(self, output): ''' Remove Vagrant from the equation for unit testing. ''' ENCODED_COMMA = '%!(VAGRANT_COMMA)' plugins = [] # initialize plugin values name = None version = None system = False for timestamp, target, kind, data in self._parse_machine_readable_output(output): if kind == 'plugin-name': # finish the previous plugin, if any if name is not None: plugins.append(Plugin(name=name, version=version, system=system)) # start a new plugin name = data # plugin name version = None system = False elif kind == 'plugin-version': if ENCODED_COMMA in data: version, etc = data.split(ENCODED_COMMA) system = (etc.strip().lower() == 'system') else: version = data system = False # finish the previous plugin, if any if name is not None: plugins.append(Plugin(name=name, version=version, system=system)) return plugins def _parse_machine_readable_output(self, output): ''' param output: a string containing the output of a vagrant command with the `--machine-readable` option. returns: a dict mapping each 'target' in the machine readable output to a dict. The dict of each target, maps each target line type/kind to its data. Machine-readable output is a collection of CSV lines in the format: timestamp, target, kind, data Target is a VM name, possibly 'default', or ''. The empty string denotes information not specific to a particular VM, such as the results of `vagrant box list`. ''' # each line is a tuple of (timestamp, target, type, data) # target is the VM name # type is the type of data, e.g. 'provider-name', 'box-version' # data is a (possibly comma separated) type-specific value, e.g. 'virtualbox', '0' parsed_lines = [line.split(',', 4) for line in output.splitlines() if line.strip()] # vagrant 1.8 adds additional fields that aren't required, # and will break parsing if included in the status lines. # filter them out pending future implementation. parsed_lines = list(filter(lambda x: x[2] not in ["metadata", "ui", "action"], parsed_lines)) return parsed_lines def _parse_config(self, ssh_config): ''' This lame parser does not parse the full grammar of an ssh config file. It makes assumptions that are (hopefully) correct for the output of `vagrant ssh-config [vm-name]`. Specifically it assumes that there is only one Host section, the default vagrant host. It assumes that the parameters of the ssh config are not changing. every line is of the form 'key value', where key is a single token without any whitespace and value is the remaining part of the line. Value may optionally be surrounded in double quotes. All leading and trailing whitespace is removed from key and value. Example lines: ' User vagrant\n' ' IdentityFile "/home/robert/.vagrant.d/insecure_private_key"\n' Lines with '#' as the first non-whitespace character are considered comments and ignored. Whitespace-only lines are ignored. This parser does NOT handle using an '=' in options. Values surrounded in double quotes will have the double quotes removed. See https://github.com/bitprophet/ssh/blob/master/ssh/config.py for a more compliant ssh config file parser. ''' conf = dict() started_parsing = False for line in ssh_config.splitlines(): if line.strip().startswith('Host ') and not started_parsing: started_parsing = True if not started_parsing or not line.strip() or line.strip().startswith('#'): continue key, value = line.strip().split(None, 1) # Remove leading and trailing " from the values conf[key] = value.strip('"') return conf def _make_vagrant_command(self, args): if self._vagrant_exe is None: self._vagrant_exe = get_vagrant_executable() if not self._vagrant_exe: raise RuntimeError(VAGRANT_NOT_FOUND_WARNING) # filter out None args. Since vm_name is None in non-Multi-VM # environments, this quitely removes it from the arguments list # when it is not specified. return [self._vagrant_exe] + [arg for arg in args if arg is not None] def _call_vagrant_command(self, args): ''' Run a vagrant command. Return None. args: A sequence of arguments to a vagrant command line. ''' # Make subprocess command command = self._make_vagrant_command(args) with self.out_cm() as out_fh, self.err_cm() as err_fh: subprocess.check_call(command, cwd=self.root, stdout=out_fh, stderr=err_fh, env=self.env) def _run_vagrant_command(self, args): ''' Run a vagrant command and return its stdout. args: A sequence of arguments to a vagrant command line. e.g. ['up', 'my_vm_name', '--no-provision'] or ['up', None, '--no-provision'] for a non-Multi-VM environment. ''' # Make subprocess command command = self._make_vagrant_command(args) with self.err_cm() as err_fh: return compat.decode(subprocess.check_output(command, cwd=self.root, env=self.env, stderr=err_fh)) def _stream_vagrant_command(self, args): """ Execute a vagrant command, returning a generator of the output lines. Caller should consume the entire generator to avoid the hanging the subprocess. :param args: Arguments for the Vagrant command. :return: generator that yields each line of the command stdout. :rtype: generator iterator """ py3 = sys.version_info > (3, 0) # Make subprocess command command = self._make_vagrant_command(args) with self.err_cm() as err_fh: sp_args = dict(args=command, cwd=self.root, env=self.env, stdout=subprocess.PIPE, stderr=err_fh, bufsize=1) # Iterate over output lines. # See http://stackoverflow.com/questions/2715847/python-read-streaming-input-from-subprocess-communicate#17698359 p = subprocess.Popen(**sp_args) with p.stdout: for line in iter(p.stdout.readline, b''): yield compat.decode(line) # if PY3 decode bytestrings p.wait() # Raise CalledProcessError for consistency with _call_vagrant_command if p.returncode != 0: raise subprocess.CalledProcessError(p.returncode, command) class SandboxVagrant(Vagrant): ''' Support for sandbox mode using the Sahara gem (https://github.com/jedi4ever/sahara). ''' def _run_sandbox_command(self, args): return self._run_vagrant_command(['sandbox'] + list(args)) def sandbox_commit(self, vm_name=None): ''' Permanently writes all the changes made to the VM. ''' self._run_sandbox_command(['commit', vm_name]) def sandbox_off(self, vm_name=None): ''' Disables the sandbox mode. ''' self._run_sandbox_command(['off', vm_name]) def sandbox_on(self, vm_name=None): ''' Enables the sandbox mode. This requires the Sahara gem to be installed (https://github.com/jedi4ever/sahara). ''' self._run_sandbox_command(['on', vm_name]) def sandbox_rollback(self, vm_name=None): ''' Reverts all the changes made to the VM since the last commit. ''' self._run_sandbox_command(['rollback', vm_name]) def sandbox_status(self, vm_name=None): ''' Returns the status of the sandbox mode. Possible values are: - on - off - unknown - not installed ''' vagrant_sandbox_output = self._run_sandbox_command(['status', vm_name]) return self._parse_vagrant_sandbox_status(vagrant_sandbox_output) def _parse_vagrant_sandbox_status(self, vagrant_output): ''' Returns the status of the sandbox mode given output from 'vagrant sandbox status'. ''' # typical output # [default] - snapshot mode is off # or # [default] - machine not created # if the box VM is down tokens = [token.strip() for token in vagrant_output.split(' ')] if tokens[0] == 'Usage:': sahara_status = 'not installed' elif "{} {}".format(tokens[-2], tokens[-1]) == 'not created': sahara_status = 'unknown' else: sahara_status = tokens[-1] return sahara_status python-vagrant-0.5.15/vagrant/compat.py0000664000076500000240000000077213103160235017764 0ustar tfdstaff00000000000000""" vagrant.compat -------------- Python 2/3 compatiblity module. """ # std import locale import sys PY2 = sys.version_info[0] == 2 def decode(value): """Decode binary data to text if needed (for Python 3). Use with the functions that return in Python 2 value of `str` type and for Python 3 encoded bytes. :param value: Encoded bytes for Python 3 and `str` for Python 2. :return: Value as a text. """ return value.decode(locale.getpreferredencoding()) if not PY2 else value python-vagrant-0.5.15/vagrant/test.py0000664000076500000240000000655312543666736017513 0ustar tfdstaff00000000000000""" A TestCase class, tying together the Vagrant class and removing some of the boilerplate involved in writing tests that leverage vagrant boxes. """ from unittest import TestCase from vagrant import Vagrant, stderr_cm __author__ = 'nick' class VagrantTestCase(TestCase): """ TestCase class to control vagrant boxes during testing vagrant_boxes: An iterable of vagrant boxes. If empty or None, all boxes will be used. Defaults to [] vagrant_root: The root directory that holds a Vagrantfile for configuration. Defaults to the working directory restart_boxes: If True, the boxes will be restored to their initial states between each test, otherwise the boxes will remain up. Defaults to False """ vagrant_boxes = [] vagrant_root = None restart_boxes = False __initial_box_statuses = {} __cleanup_actions = { Vagrant.NOT_CREATED: 'destroy', Vagrant.POWEROFF: 'halt', Vagrant.SAVED: 'suspend', } def __init__(self, *args, **kwargs): """Check that the vagrant_boxes attribute is not left empty, and is populated by all boxes if left blank""" self.vagrant = Vagrant(self.vagrant_root, err_cm=stderr_cm) if not self.vagrant_boxes: boxes = [s.name for s in self.vagrant.status()] if len(boxes) == 1: self.vagrant_boxes = ['default'] else: self.vagrant_boxes = boxes super(VagrantTestCase, self).__init__(*args, **kwargs) def assertBoxStatus(self, box, status): """Assertion for a box status""" box_status = [s.state for s in self.vagrant.status() if s.name == box][0] if box_status != status: self.failureException('{} has status {}, not {}'.format(box, box_status, status)) def assertBoxUp(self, box): """Assertion for a box being up""" self.assertBoxStatus(box, Vagrant.RUNNING) def assertBoxSuspended(self, box): """Assertion for a box being up""" self.assertBoxStatus(box, Vagrant.SAVED) def assertBoxHalted(self, box): """Assertion for a box being up""" self.assertBoxStatus(box, Vagrant.POWEROFF) def assertBoxNotCreated(self, box): """Assertion for a box being up""" self.assertBoxStatus(box, Vagrant.NOT_CREATED) def run(self, result=None): """Override run to have provide a hook into an alternative to tearDownClass with a reference to self""" self.setUpOnce() run = super(VagrantTestCase, self).run(result) self.tearDownOnce() return run def setUpOnce(self): """Collect the box states before starting""" for box_name in self.vagrant_boxes: box_state = [s.state for s in self.vagrant.status() if s.name == box_name][0] self.__initial_box_statuses[box_name] = box_state def tearDownOnce(self): """Restore all boxes to their initial states after running all tests, unless tearDown handled it already""" if not self.restart_boxes: self.restore_box_states() def restore_box_states(self): """Restores all boxes to their original states""" for box_name in self.vagrant_boxes: action = self.__cleanup_actions.get(self.__initial_box_statuses[box_name]) if action: getattr(self.vagrant, action)(vm_name=box_name) def setUp(self): """Starts all boxes before running tests""" for box_name in self.vagrant_boxes: self.vagrant.up(vm_name=box_name) super(VagrantTestCase, self).setUp() def tearDown(self): """Returns boxes to their initial status after each test if self.restart_boxes is True""" if self.restart_boxes: self.restore_box_states() super(VagrantTestCase, self).tearDown()