diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..d85570a --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +export VIRTUAL_ENV=./venv +layout python-venv python3.7 diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..bdc619f --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,43 @@ +name: Python package + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install docopt clint requests flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest + - name: Generate coverage report + run: | + pytest --cov=mech --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + yml: ./codecov.yml + fail_ci_if_error: true diff --git a/.gitignore b/.gitignore index 82132df..39b1294 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .DS_Store +cov_html/ +.coverage *.pyc .idea/ build/ @@ -6,3 +8,5 @@ dist/ mech.egg-info/ .mech/ Mechfile +.*swp +venv/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9ea08df --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,81 @@ +# Contributing to mech + +Anyone can open a pull request to help expand/enhance the functionality are essential to mech's growth. + +This guide should help get you started contributing to mech. + + +## Dev Setup + +```sh +# Clone the repo +git clone git@github.com:mkinney/mech.git + +# Change into that cloned directory +cd mech + +# If virtualenv is not installed: +sudo apt-get install virtualenv + +# Create a virtualenv +virtualenv -p python3 venv + +# Activate the python virtual environment +source venv/bin/activate + +# consider installing/using direnv (there is a .envrc in this repo) +# may need to run "direnv allow" + +# install mech from this code +python setup.py install + +# if doing development +pip install docopt clint requests flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist pytest-timeout + +# also optional +pip install autopep8 +# use like this: autopep8 --in-place --aggressive --aggressive somefile.py + +# Configure git to use pre-commit hook +flake8 --install-hook git + +# for running unit tests: +pytest + +# for code coverage +pytest --cov mech + +# to see what lines are not covered +pytest --cov-report term-missing --cov mech + +# or to get a nice html output +pytest --cov-report html:cov_html --cov=mech +# then open cov_html/index.html +# or if you want all coverage html report +pytest --cov-report html:cov_html --cov=mech -m"int or not int" + +# if you want to do a quick-ish (takes 1.5 minutes) smoke test +# this runs thru most basic functionality of mech +./smoke_test + +# to see the slowest unit tests +pytest --durations=0 + +# if you have a unittest that is taking too long, but cannot find out which one +# add "timeout = 10" (for 10 seconds) + +# for testing/validation, we have also some integration tests +# NOTE: Can take 5+ minutes. +# cd tests/int (see "all" file) +# You can run the int tests by themselves directly if you change +# into the tests/int directory. +# Or, you can run them from the main project directory like this: +pytest -m"int" + +# To run all tests (with verbose output and show local variables): +pytest -m"int or not int" -vv -l +# or just run "./full_test" + +# Or, just one run int test like this (with verbose and show local variables): +pytest -m"int" -k"provision" -v -l +``` diff --git a/README.md b/README.md index d4da221..6483d22 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ # mech -I made this because I don't like VirtualBox and I wanted to use vagrant -with VMWare Fusion but was too cheap to buy the Vagrant plugin. +![Python package](https://github.com/mkinney/mech/workflows/Python%20package/badge.svg?branch=multi-pr) +[![codecov](https://codecov.io/gh/mkinney/mech/branch/multi-pr/graph/badge.svg)](https://codecov.io/gh/mkinney/mech) + +One of the authors made this because they don't like VirtualBox and wanted to use vagrant +with VMmare Fusion but was too cheap to buy the Vagrant plugin. https://blog.kchung.co/mech-vagrant-with-vmware-integration-for-free/ @@ -16,35 +19,56 @@ Options: --debug Show debug messages. Common commands: - (list|ls) lists all available boxes - init initializes a new Mech environment by creating a Mechfile - destroy stops and deletes all traces of the Mech machine - (up|start) starts and provisions the Mech environment - (down|stop|halt) stops the Mech machine - suspend suspends the machine - pause pauses the Mech machine - ssh connects to machine via SSH - ssh-config outputs OpenSSH valid configuration to connect to the machine - scp copies files to and from the machine via SCP - ip outputs ip of the Mech machine - box manages boxes: installation, removal, etc. - global-status outputs status Mech environments for this user - status outputs status of the Mech machine - ps list running processes in Guest OS - provision provisions the Mech machine - reload restarts Mech machine, loads new Mechfile configuration - resume resume a paused/suspended Mech machine - snapshot manages snapshots: saving, restoring, etc. - port displays information about guest port mappings - push deploys code in this environment to a configured destination + box manages boxes: add, list remove, etc. + destroy stops and deletes all traces of the instances + (down|stop|halt) stops the instances + global-status outputs status of all virutal machines on this host + init initializes a new Mech environment by creating a Mechfile + ip outputs ip of an instance + (list|ls) lists all available boxes + pause pauses the instances + port displays information about guest port mappings + provision provisions the Mech machine + ps list running processes for an instance + reload restarts Mech machine, loads new Mechfile configuration + resume resume a paused/suspended Mech machine + scp copies files to/from the machine via SCP + snapshot manages snapshots: save, list, remove, etc. + ssh connects to an instance via SSH + ssh-config outputs OpenSSH valid configuration to connect to the instances + status outputs status of the instances + suspend suspends the instances + (up|start) starts instances (aka virtual machines) + upgrade upgrade the instances For help on any individual command run `mech -h` Example: + mech up --help + +% mech up --help +Starts and provisions the mech environment. + +Usage: mech up [options] [] + +Options: + --disable-provisioning Do not provision + --disable-shared-folders Do not share folders with VM + --gui Start GUI + --memsize 1024 Specify the size of memory for VM + --no-cache Do not save the downloaded box + --no-nat Do not use NAT network (i.e., bridged) + --numvcpus 1 Specify the number of vcpus for VM + -h, --help Print this help + -r, --remove-vagrant Remove vagrant user + +Example using mech: + + Initializing and using a machine from HashiCorp's Vagrant Cloud: - mech init bento/ubuntu-14.04 + mech init bento/ubuntu-18.04 mech up mech ssh ``` @@ -52,8 +76,7 @@ Initializing and using a machine from HashiCorp's Vagrant Cloud: `mech init` can be used to pull a box file which will be installed and generate a Mechfile in the current directory. You can also pull boxes from Vagrant Cloud with `mech init freebsd/FreeBSD-11.1-RELEASE`. -Barring that, `mech up ` can also be used to specify a vmx file -to start. +See the `mech up -h` page for more information. # Install @@ -63,6 +86,11 @@ or for the latest: `pip install -U git+https://github.com/mechboxes/mech.git` +There are some open PRs that have yet to be merged. Until they are, you may consider +installing from: + +`pip install -U git+https://github.com/mkinney/mech.git@multi-pr#egg=mech` + # Shared Folders If the box you init was created properly, you will be able to access @@ -85,3 +113,57 @@ or ```bash vmhgfs-fuse .host:/mech /mnt/hgfs ``` + +# Changing vcpus and/or memory size + +If you do not specify how many vcpus or memory, then the values +in the .box file will be used. To override, use appropriate settings: + +`mech up --numvcpus 2 --memsize 1024` + + +# Want zsh completion for commands/options (aka "tab completion")? +1. add these lines to ~/.zshrc + +```bash +# folder of all of your autocomplete functions +fpath=($HOME/.zsh-completions $fpath) +# enable autocomplete function +autoload -U compinit +compinit +``` + +2. Copy script to something in fpath (Note: Run `echo $fpath` to show value.) + +```bash +cp _mech ~/.zsh-completions/ +``` + +3. Reload zsh + +```bash +exec zsh +``` + +4. Try it out by typing `mech `. It should show the options available. + +# Want bash completion for commands/options (aka "tab completion")? +1. add these lines to ~/.bash_profile + +```bash +[ -f /usr/local/etc/bash_completion ] && . /usr/local/etc/bash_completion +``` + +2. Copy script to path above + +```bash +cp mech_completion.sh /usr/local/etc/bash_completion/ +``` + +3. Reload .bash_profile + +```bash +source ~/.bash_profile +``` + +4. Try it out by typing `mech `. It should show the options available. diff --git a/_mech b/_mech new file mode 100644 index 0000000..68dfb77 --- /dev/null +++ b/_mech @@ -0,0 +1,296 @@ +#compdef _mech mech + +# zsh for mech + +function _mech { + local line + + _arguments -C \ + "-h[Show help information]" \ + "--help[Show help information]" \ + "1: :(box down global-status halt init ip list ls pause port provision ps reload resume scp snapshot ssh-config stop suspend up)" \ + "*::arg:->args" + + case $line[1] in + box) _mech_box;; + down) _mech_down ;; + global-status) _mech_global_status;; + halt) _mech_down;; + init) _mech_init;; + ip) _mech_ip ;; + list) _mech_list ;; + ls) _mech_list ;; + pause) _mech_pause;; + port) _mech_port ;; + provision) _mech_provision ;; + ps) _mech_ps;; + reload) _mech_reload;; + resume) _mech_resume ;; + scp) _mech_scp;; + snapshot) _mech_snapshot ;; + stop) _mech_down;; + ssh-config) _mech_ssh-config ;; + suspend) _mech_suspend ;; + up) _mech_up ;; + esac +} + +__mech_box_list () { + _wanted application expl 'command' compadd $(command mech box list | \ + awk '(NR > 1) { printf("%s ", $1) }') +} + +function _mech_box_arguments { + _arguments \ + "-h[Show help information]" \ + "1: :(add delete list ls remove)" + } + +function _mech_box { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + "1: :(add delete list ls remove)" \ + '*::options:->options' + + case $state in + (options) + case $line[1] in + (delete|remove) _arguments ':feature:__mech_box_list' ;; + esac + ;; + esac +} + +__mech_list () { + _wanted application expl 'command' compadd $(command mech list | \ + awk '(NR > 1) { printf("%s ", $1) }') +} + +function _mech_down { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + "-f[Force a hard stop]" \ + "--force[Force a hard stop]" \ + "-h[Show help information]" \ + "--help[Show help information]" \ + '::options:->options' + + case $state in + (options) + case $line[1] in + (*) _arguments ':feature:__mech_list' ;; + esac + esac +} + +function _mech_global_status { + _arguments -C \ + "-h[Show help information]" \ + "--help[Show help information]" +} + +function _mech_init { + _arguments \ + "--box=[BOXNAME Name of the box (ex: bento/ubuntu-18.04)]" \ + "--box-version=[VERSION Constrain version of the added box]" \ + "-f[Overwrite exsting Mechfile]" \ + "--force[Overwrite existing Mechfile]" \ + "-h[Show help information]" \ + "--help[Show help information]" \ + "--name=[NAME Name of the instance (ex: first)]" +} + +function _mech_ip { + _arguments \ + "-h[Show help information]" \ + "--help[Show help information]" +} + +function _mech_list { + _arguments \ + "-d[Print detailed info]" \ + "--detail[Print detailed info]" \ + "-h[Show help information]" \ + "--help[Show help information]" +} + +function _mech_pause { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + "-h[Show help information]" \ + "--help[Show help information]" \ + '::options:->options' + + case $state in + (options) + case $line[1] in + (*) _arguments ':feature:__mech_list' ;; + esac + esac +} + +function _mech_port { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + "--guest[PORT Output the host port that maps to the given guest port]" \ + "-h[Show help information]" \ + "--help[Show help information]" \ + '::options:->options' + + case $state in + (options) + case $line[1] in + (*) _arguments ':feature:__mech_list' ;; + esac + esac +} + +function _mech_provision { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + "-h[Show help information]" \ + "--help[Show help information]" \ + "-s[Show the provisioning info (do not run)]" \ + "--show[Show the provisioning info (do not run)]" \ + '::options:->options' + + case $state in + (options) + case $line[1] in + (*) _arguments ':feature:__mech_list' ;; + esac + esac +} + +function _mech_ps { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + "-h[Show help information]" \ + "--help[Show help information]" \ + '::options:->options' + + case $state in + (options) + case $line[1] in + (*) _arguments ':feature:__mech_list' ;; + esac + esac +} + + +function _mech_reload { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + "-h[Show help information]" \ + "--help[Show help information]" \ + '::options:->options' + + case $state in + (options) + case $line[1] in + (*) _arguments ':feature:__mech_list' ;; + esac + esac +} + +function _mech_resume { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + "--disable-shared-folders[Do not share folders with VM]" \ + "-h[Show help information]" \ + "--help[Show help information]" \ + '::options:->options' + + case $state in + (options) + case $line[1] in + (*) _arguments ':feature:__mech_list' ;; + esac + esac +} + +function _mech_scp { + _arguments \ + "-h[Show help information]" \ + "--help[Show help information]" +} + +function _mech_ssh_config { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + "-h[Show help information]" \ + "--help[Show help information]" \ + '::options:->options' + + case $state in + (options) + case $line[1] in + (*) _arguments ':feature:__mech_list' ;; + esac + esac +} + +function _mech_snapshot { + _arguments -C \ + "-h[Show help information]" \ + "--help[Show help information]" \ + "1: :(delete list ls remove save)" +} + +function _mech_suspend { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + "-h[Show help information]" \ + "--help[Show help information]" \ + '::options:->options' + + case $state in + (options) + case $line[1] in + (*) _arguments ':feature:__mech_list' ;; + esac + esac +} + +function _mech_up { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + "--disable-provisioning[Do not provision]" \ + "--disable-shared-folders[Do not share folders with VM]" \ + "--gui[Start GUI]" \ + "-h[Show help information]" \ + "--help[Show help information]" \ + "--memsize=[SIZE Specify the size of memory for VM (ex: 1024)]" \ + "--no-cache[Do not save the downloaded box]" \ + "--no-nat[Do not use NAT network (i.e., bridged)]" \ + "--numvcpus=[NUMBER Specify the number of vcpus for VM (ex: 2)]" \ + '::options:->options' + + case $state in + (options) + _arguments ':feature:__mech_list' ;; + esac +} +# vim: ft=zsh sw=2 ts=2 et diff --git a/full_test b/full_test new file mode 100755 index 0000000..ddf73ee --- /dev/null +++ b/full_test @@ -0,0 +1,2 @@ +# convenience script to run all tests (unit and integration) +pytest -m"int or not int" -vv -l diff --git a/mech/__init__.py b/mech/__init__.py index f81418d..26f70fd 100644 --- a/mech/__init__.py +++ b/mech/__init__.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # # Copyright (c) 2018 German Mendez Bravo (Kronuz) +# Copyright (c) 2020 Mike Kinney # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -21,6 +22,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # +"""Initialize mech with version info.""" -__version__ = '0.7.6' +__version__ = '0.7.7' VERSION = "{} v{}".format(__name__, __version__) diff --git a/mech/__main__.py b/mech/__main__.py index c1840af..718e1ee 100644 --- a/mech/__main__.py +++ b/mech/__main__.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # # Copyright (c) 2018 German Mendez Bravo (Kronuz) +# Copyright (c) 2020 Mike Kinney # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -21,20 +22,19 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # +"""Main entry for 'mech' command.""" from __future__ import absolute_import -def main(): - try: - import os - import sys +import sys + +from . import VERSION +from .mech import Mech - from . import VERSION - from .mech import Mech - from .utils import makedirs - HOME = os.path.expanduser('~/.mech') - makedirs(HOME) +def main(): + """Main function.""" + try: arguments = Mech.docopt(Mech.__doc__, argv=sys.argv[1:], version=VERSION) return Mech(arguments)() except KeyboardInterrupt: diff --git a/mech/command.py b/mech/command.py index fa3172b..9e491c9 100644 --- a/mech/command.py +++ b/mech/command.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (c) 2018 German Mendez Bravo (Kronuz) +# Copyright (c) 2020 Mike Kinney # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -20,6 +21,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # +"""Handle the mech options using docopt.""" from __future__ import absolute_import @@ -33,27 +35,37 @@ NBSP = '__' + def cmd_usage(doc): + """Show the command usage.""" return doc.replace(NBSP, ' ') + docopt_extras_ref = docopt.extras -def docopt_extras(help, version, options, doc): - return docopt_extras_ref(help, version, options, cmd_usage(doc)) + + +def docopt_extras(the_help, version, options, doc): + """Show the "Extra" help info.""" + return docopt_extras_ref(the_help, version, options, cmd_usage(doc)) + def DocoptExit____init__(self, message=''): + """Constructor for docopt.""" SystemExit.__init__(self, (message + '\n' + cmd_usage(self.usage)).strip()) + docopt.extras = docopt_extras docopt.DocoptExit.__init__ = DocoptExit____init__ def spaced(name): + """Return the command name.""" name = re.sub(r'[ _]+', r' ', name) name = re.sub(r'(?<=[^_])([A-Z])', r' \1', name).lower() return re.sub(r'^( *)(.*?)( *)$', r'\2', name) -class Command(object): +class Command(): """ Usage: command [...] """ @@ -63,6 +75,7 @@ class Command(object): @staticmethod def docopt(doc, **kwargs): + """Parse comments for arguments.""" name = kwargs.pop('name', "") name = spaced(name) doc = textwrap.dedent(doc).replace(name, name.replace(' ', NBSP)) @@ -73,6 +86,7 @@ def __init__(self, arguments): self.arguments = arguments def __call__(self): + """Invoke the command with the arguments.""" if self.subcommand_name in self.arguments: cmd = self.arguments[self.subcommand_name] cmd_attr = cmd.replace('-', '_') @@ -83,7 +97,8 @@ def __call__(self): cmd = meth_func.__name__.replace('_', '-') name = '{} {}'.format(self.__class__.__name__, cmd) if klass.__doc__: - arguments = self.docopt(klass.__doc__, argv=self.arguments.get(self.argv_name, []), name=name) + arguments = self.docopt(klass.__doc__, + argv=self.arguments.get(self.argv_name, []), name=name) else: arguments = [] obj = klass(arguments) @@ -96,4 +111,5 @@ def __call__(self): return obj def run(self): + """Run the command.""" raise docopt.DocoptExit() diff --git a/mech/compat.py b/mech/compat.py index d2d2007..dc97a68 100644 --- a/mech/compat.py +++ b/mech/compat.py @@ -2,6 +2,7 @@ # # Copyright (c) 2016-2017 Kevin Chung # Copyright (c) 2018 German Mendez Bravo (Kronuz) +# Copyright (c) 2020 Mike Kinney # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -21,6 +22,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # +"""Compatibility code for different versions of python.""" import sys import operator import functools @@ -38,7 +40,11 @@ _builtin = functools.partial(getattr, _builtins_) #: No operation lambda -_noop = lambda obj: obj + + +def _noop(obj): + return obj + if PY3: meth_func = '__func__' @@ -60,11 +66,18 @@ s = _noop b = _noop -#: Octal number compatibility shim -o = lambda numstr: int(numstr, 8) -#: Get a bound method's function -get_meth_func = lambda klass: operator.attrgetter(meth_func)(klass) if hasattr(klass, meth_func) else None +def o(numstr): + """Octal number compatibility shim.""" + return int(numstr, 8) + + +def get_meth_func(klass): + """Get a bound method's function.""" + return operator.attrgetter( + meth_func)(klass) if hasattr(klass, meth_func) else None + -#: "safe" form of ``b``. Checks for binary type before operating. -b2s = lambda bytestr: s(bytestr) if isinstance(bytestr, binary_type) else bytestr +def b2s(bytestr): + '''"safe" form of ``b``. Checks for binary type before operating.''' + return s(bytestr) if isinstance(bytestr, binary_type) else bytestr diff --git a/mech/conftest.py b/mech/conftest.py new file mode 100644 index 0000000..47e0dcf --- /dev/null +++ b/mech/conftest.py @@ -0,0 +1,192 @@ +# Copyright (c) 2020 Mike Kinney + +"""Common pytest code.""" +import json +import os +import pytest +import subprocess + + +from shutil import rmtree + + +@pytest.fixture +def mechfile_one_entry(): + """Return one mechfile entry.""" + return { + 'first': { + 'name': 'first', + 'box': 'bento/ubuntu-18.04', + 'box_version': '201912.04.0' + } + } + + +@pytest.fixture +def mechfile_one_entry_with_auth(): + """Return one mechfile entry with auth.""" + return { + 'first': { + 'name': 'first', + 'box': 'bento/ubuntu-18.04', + 'box_version': '201912.04.0', + 'auth': { + 'username': 'bob', + 'pub_key': 'some_pub_key_data' + } + } + } + + +@pytest.fixture +def mechfile_two_entries(): + """Return two mechfile entries.""" + return { + 'first': { + 'name': 'first', + 'box': 'bento/ubuntu-18.04', + 'box_version': '201912.04.0', + 'shared_folders': [ + { + "host_path": ".", + "share_name": "mech" + } + ], + 'url': + 'https://vagrantcloud.com/bento/boxes/ubuntu-18.04/' + 'versions/201912.04.0/providers/vmware_desktop.box' + }, + 'second': { + 'name': 'second', + 'box': 'bento/ubuntu-18.04', + 'box_version': '201912.04.0', + 'url': + 'https://vagrantcloud.com/bento/boxes/ubuntu-18.04/' + 'versions/201912.04.0/providers/vmware_desktop.box' + } + } + + +CATALOG = """{ + "description": "foo", + "short_description": "foo", + "name": "bento/ubuntu-18.04", + "versions": [ + { + "version": "aaa", + "status": "active", + "description_html": "foo", + "description_markdown": "foo", + "providers": [ + { + "name": "vmware_desktop", + "url": "https://vagrantcloud.com/bento/boxes/ubuntu-18.04/\ +versions/aaa/providers/vmware_desktop.box", + "checksum": null, + "checksum_type": null + } + ] + } + ] +}""" +@pytest.fixture +def catalog(): + """Return a catalog.""" + return CATALOG + + +@pytest.fixture +def catalog_as_json(): + """Return a catalog as json.""" + return json.loads(CATALOG) + + +@pytest.fixture +def mech_add_arguments(): + """Return the default 'mech add' arguments.""" + return { + '--force': False, + '--box-version': None, + '--name': None, + '--box': None, + '--add-me': None, + '--use-me': None, + '': None, + } + + +@pytest.fixture +def mech_box_arguments(): + """Return the default 'mech box' arguments.""" + return { + '--force': False, + '--box-version': None, + '': None, + } + + +@pytest.fixture +def mech_init_arguments(): + """Return the default 'mech init' arguments.""" + return { + '--force': False, + '--box-version': None, + '--name': None, + '--box': None, + '--add-me': None, + '--use-me': None, + '': None, + } + + +class Helpers: + @staticmethod + def get_mock_data_written(a_mock): + """Helper function to get the data written to a mocked file.""" + written = '' + for call in a_mock.mock_calls: + tmp = '{}'.format(call) + if tmp.startswith('call().write('): + line = tmp.replace("call().write('", '') + line = line.replace("')", '') + line = line.replace("\\n", '\n') + written += line + return written + + @staticmethod + def kill_pids(pids): + """Kill all pids.""" + for pid in pids: + results = subprocess.run(args='kill {}'.format(pid), shell=True, capture_output=True) + if results.returncode != 0: + print("Could not kill pid:{}".format(pid)) + + @staticmethod + def find_vmx_for_dir(part_of_dir): + """Return all pids that that are VMware VMs where + the .vmx part_of_dir matches the full path.""" + pids = [] + results = subprocess.run(args='ps -ef | grep vmware-vmx | grep {} | grep -v grep' + .format(part_of_dir), shell=True, capture_output=True) + if results.returncode == 0: + # we found a proc + stdout = results.stdout.decode('utf-8') + for line in stdout.split('\n'): + data = line.split() + if len(data) > 2: + # add pid to the collection + pids.append(data[1]) + return pids + + @staticmethod + def cleanup_dir_and_vms_from_dir(a_dir): + """Kill any vms from this directory, remove directory and re-create the directory.""" + Helpers.kill_pids(Helpers.find_vmx_for_dir(a_dir + '/.mech/')) + rmtree(a_dir, ignore_errors=True) + os.mkdir(a_dir) + + +@pytest.fixture +def helpers(): + """Helper functions for testing.""" + return Helpers diff --git a/mech/mech.py b/mech/mech.py index afdcaae..4c12e97 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -2,6 +2,7 @@ # # Copyright (c) 2016-2017 Kevin Chung # Copyright (c) 2018 German Mendez Bravo (Kronuz) +# Copyright (c) 2020 Mike Kinney # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -21,451 +22,26 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # +"""Mech class""" from __future__ import print_function, absolute_import import os -import re import sys -import time -import fnmatch import logging -import tempfile import textwrap import shutil -import subprocess -from clint.textui import colored, puts_err +from clint.textui import colored from . import utils from .vmrun import VMrun -from .command import Command - -logger = logging.getLogger(__name__) - -DEFAULT_HOST = 'mech' -DEFAULT_USER = 'vagrant' -DEFAULT_PASSWORD = 'vagrant' -INSECURE_PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzI -w+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoP -kcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2 -hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NO -Td0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcW -yLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQIBIwKCAQEA4iqWPJXtzZA68mKd -ELs4jJsdyky+ewdZeNds5tjcnHU5zUYE25K+ffJED9qUWICcLZDc81TGWjHyAqD1 -Bw7XpgUwFgeUJwUlzQurAv+/ySnxiwuaGJfhFM1CaQHzfXphgVml+fZUvnJUTvzf -TK2Lg6EdbUE9TarUlBf/xPfuEhMSlIE5keb/Zz3/LUlRg8yDqz5w+QWVJ4utnKnK -iqwZN0mwpwU7YSyJhlT4YV1F3n4YjLswM5wJs2oqm0jssQu/BT0tyEXNDYBLEF4A -sClaWuSJ2kjq7KhrrYXzagqhnSei9ODYFShJu8UWVec3Ihb5ZXlzO6vdNQ1J9Xsf -4m+2ywKBgQD6qFxx/Rv9CNN96l/4rb14HKirC2o/orApiHmHDsURs5rUKDx0f9iP -cXN7S1uePXuJRK/5hsubaOCx3Owd2u9gD6Oq0CsMkE4CUSiJcYrMANtx54cGH7Rk -EjFZxK8xAv1ldELEyxrFqkbE4BKd8QOt414qjvTGyAK+OLD3M2QdCQKBgQDtx8pN -CAxR7yhHbIWT1AH66+XWN8bXq7l3RO/ukeaci98JfkbkxURZhtxV/HHuvUhnPLdX -3TwygPBYZFNo4pzVEhzWoTtnEtrFueKxyc3+LjZpuo+mBlQ6ORtfgkr9gBVphXZG -YEzkCD3lVdl8L4cw9BVpKrJCs1c5taGjDgdInQKBgHm/fVvv96bJxc9x1tffXAcj -3OVdUN0UgXNCSaf/3A/phbeBQe9xS+3mpc4r6qvx+iy69mNBeNZ0xOitIjpjBo2+ -dBEjSBwLk5q5tJqHmy/jKMJL4n9ROlx93XS+njxgibTvU6Fp9w+NOFD/HvxB3Tcz -6+jJF85D5BNAG3DBMKBjAoGBAOAxZvgsKN+JuENXsST7F89Tck2iTcQIT8g5rwWC -P9Vt74yboe2kDT531w8+egz7nAmRBKNM751U/95P9t88EDacDI/Z2OwnuFQHCPDF -llYOUI+SpLJ6/vURRbHSnnn8a/XG+nzedGH5JGqEJNQsz+xT2axM0/W/CRknmGaJ -kda/AoGANWrLCz708y7VYgAtW2Uf1DPOIYMdvo6fxIB5i9ZfISgcJ/bbCUkFrhoH -+vq/5CIWxCPp0f85R4qxxQ5ihxJ0YDQT9Jpx4TMss4PSavPaBH3RXow5Ohe+bYoQ -NE5OgEXk2wVfZczCZpigBKbKZHNYcelXtTt/nP3rsCuGcM4h53s= ------END RSA PRIVATE KEY----- -""" - -HOME = os.path.expanduser("~/.mech") - - -class MechCommand(Command): - active_mechfile = None - - def activate_mechfile(self, path): - if path in self.mechfiles: - self.active_mechfile = self.mechfiles[path] - else: - self.active_mechfile = self.mechfiles[path] = utils.load_mechfile(path) - - def activate(self, instance_name=None): - if not hasattr(self, 'mechfiles'): - self.mechfiles = {} - if instance_name: - instance = utils.settle_instance(instance_name) - path = instance.get('path') - if not path: - puts_err(colored.red(textwrap.fill("Cannot find a valid path for '{}' instance".format(instance_name)))) - sys.exit(1) - path = os.path.abspath(os.path.expanduser(path)) - os.chdir(path) - self.activate_mechfile(path) - else: - path = os.getcwd() - self.activate_mechfile(path) - instance_name = self.active_mechfile.get('name') or os.path.basename(path) # Use the Mechfile's name if available - return instance_name - - def get(self, name, default=None): - if self.active_mechfile is None: - raise AttributeError("Must activate(instance_name) first.") - return self.active_mechfile.get(name, default) - - def get_vmx(self, silent=False): - self.get("") # Check if there's a Mechfile - return utils.get_vmx(silent=silent) - - @property - def vmx(self): - return self.get_vmx() - - @property - def box_name(self): - box_name = self.get('box') - if not box_name: - puts_err(colored.red(textwrap.fill("Cannot find a box configured in the Mechfile"))) - sys.exit(1) - return box_name - - @property - def box_version(self): - return self.get('box_version') - - @property - def user(self): - return self.get('user', DEFAULT_USER) - - @property - def password(self): - return self.get('password', DEFAULT_PASSWORD) - - @property - def config(self): - return self.get('config', {}).get('ssh', {}) - - @property - def config_ssh(self): - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - lookup = self.get("enable_ip_lookup", False) - ip = vmrun.getGuestIPAddress(wait=False, lookup=lookup) if vmrun.installedTools() else None - if not ip: - puts_err(colored.red(textwrap.fill( - "This Mech machine is reporting that it is not yet ready for SSH. " - "Make sure your machine is created and running and try again. " - "Additionally, check the output of `mech status` to verify " - "that the machine is in the state that you expect." - ))) - sys.exit(1) - - insecure_private_key = os.path.abspath(os.path.join(HOME, "insecure_private_key")) - if not os.path.exists(insecure_private_key): - with open(insecure_private_key, 'w') as f: - f.write(INSECURE_PRIVATE_KEY) - os.chmod(insecure_private_key, 0o400) - config = { - "Host": DEFAULT_HOST, - "User": self.user, - "Port": "22", - "UserKnownHostsFile": "/dev/null", - "StrictHostKeyChecking": "no", - "PasswordAuthentication": "no", - "IdentityFile": insecure_private_key, - "IdentitiesOnly": "yes", - "LogLevel": "FATAL", - } - for k, v in self.config.items(): - k = re.sub(r'[ _]+', r' ', k) - k = re.sub(r'(?<=[^_])([A-Z])', r' \1', k).lower() - k = re.sub(r'^( *)(.*?)( *)$', r'\2', k) - callback = lambda pat: pat.group(1).upper() - k = re.sub(r' (\w)', callback, k) - if k[0].islower(): - k = k[0].upper() + k[1:] - config[k] = v - config.update({ - "HostName": ip, - }) - return config - - -class MechBox(MechCommand): - """ - Usage: mech box [...] - - Available subcommands: - add add a box to the catalog of available boxes - list list available boxes in the catalog - outdated checks for outdated boxes - prune removes old versions of installed boxes - remove removes a box that matches the given name - repackage - update - - For help on any individual subcommand run `mech box -h` - """ - - def add(self, arguments): - """ - Add a box to the catalog of available boxes. - - Usage: mech box add [options] [] [] - - Notes: - The box descriptor can be the name of a box on HashiCorp's Vagrant Cloud, - or a URL, a local .box or .tar file, or a local .json file containing - the catalog metadata. - - Options: - -f, --force Overwrite an existing box if it exists - --insecure Do not validate SSL certificates - --cacert FILE CA certificate for SSL download - --capath DIR CA certificate directory for SSL download - --cert FILE A client SSL cert, if needed - --box-version VERSION Constrain version of the added box - --checksum CHECKSUM Checksum for the box - --checksum-type TYPE Checksum type (md5, sha1, sha256) - -h, --help Print this help - """ - url = arguments[''] - if url: - name = arguments[''] - else: - url = arguments[''] - name = None - version = arguments['--box-version'] - force = arguments['--force'] - requests_kwargs = utils.get_requests_kwargs(arguments) - utils.add_box(url, name=name, version=version, force=force, requests_kwargs=requests_kwargs) - - def list(self, arguments): - """ - List all available boxes in the catalog. - - Usage: mech box list [options] - - Options: - -i, --box-info Displays additional information about the boxes - -h, --help Print this help - """ - - print("{}\t{}".format( - 'BOX'.rjust(35), - 'VERSION'.rjust(12), - )) - path = os.path.abspath(os.path.join(HOME, 'boxes')) - for root, dirnames, filenames in os.walk(path): - for filename in fnmatch.filter(filenames, '*.box'): - directory = os.path.dirname(os.path.join(root, filename))[len(path) + 1:] - account, box, version = (directory.split('/', 2) + ['', ''])[:3] - print("{}\t{}".format( - "{}/{}".format(account, box).rjust(35), - version.rjust(12), - )) - ls = list - - def outdated(self, arguments): - """ - Checks if there is a new version available for the box. - - Usage: mech box outdated [options] - - Options: - --global Check all boxes installed - --insecure Do not validate SSL certificates - --cacert FILE CA certificate for SSL download - --capath DIR CA certificate directory for SSL download - --cert FILE A client SSL cert, if needed - -h, --help Print this help - """ - puts_err(colored.red("Not implemented!")) - - def prune(self, arguments): - """ - Remove old versions of installed boxes. +from .mech_instance import MechInstance +from .mech_command import MechCommand +from .mech_box import MechBox +from .mech_snapshot import MechSnapshot - Usage: mech box prune [options] [] - - Notes: - If the box is currently in use mech will ask for confirmation. - - Options: - -n, --dry-run Only print the boxes that would be removed. - -f, --force Destroy without confirmation even when box is in use. - -h, --help Print this help - """ - puts_err(colored.red("Not implemented!")) - - def remove(self, arguments): - """ - Remove a box from mech that matches the given name. - - Usage: mech box remove [options] - - Options: - -f, --force Remove without confirmation. - --box-version VERSION The specific version of the box to remove - --all Remove all available versions of the box - -h, --help Print this help - """ - puts_err(colored.red("Not implemented!")) - - def repackage(self, arguments): - """ - Repackage the box that is in use in the current mech environment. - - Usage: mech box repackage [options] - - Notes: - Puts it in the current directory so you can redistribute it. - The name and version of the box can be retrieved using mech box list. - - Options: - -h, --help Print this help - """ - puts_err(colored.red("Not implemented!")) - - def update(self, arguments): - """ - Update the box that is in use in the current mech environment. - - Usage: mech box update [options] [] - - Notes: - Only if there any updates available. This does not destroy/recreate - the machine, so you'll have to do that to see changes. - - Options: - -f, --force Overwrite an existing box if it exists - --insecure Do not validate SSL certificates - --cacert FILE CA certificate for SSL download - --capath DIR CA certificate directory for SSL download - --cert FILE A client SSL cert, if needed - -h, --help Print this help - """ - puts_err(colored.red("Not implemented!")) - - -class MechSnapshot(MechCommand): - """ - Usage: mech snapshot [...] - - Available subcommands: - delete delete a snapshot taken previously with snapshot save - list list all snapshots taken for a machine - pop restore state that was pushed with `mech snapshot push` - push push a snapshot of the current state of the machine - restore restore a snapshot taken previously with snapshot save - save take a snapshot of the current state of the machine - - For help on any individual subcommand run `mech snapshot -h` - """ - - def delete(self, arguments): - """ - Delete a snapshot taken previously with snapshot save. - - Usage: mech snapshot delete [options] [] - - Options: - -h, --help Print this help - """ - name = arguments[''] - - instance_name = arguments[''] - instance_name = self.activate(instance_name) - - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - if vmrun.deleteSnapshot(name) is None: - puts_err(colored.red("Cannot delete name")) - else: - puts_err(colored.green("Snapshot {} deleted".format(name))) - - def list(self, arguments): - """ - List all snapshots taken for a machine. - - Usage: mech snapshot list [options] [] - - Options: - -h, --help Print this help - """ - instance_name = arguments[''] - instance_name = self.activate(instance_name) - - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - print(vmrun.listSnapshots()) - - def pop(self, arguments): - """ - Restore state that was pushed with `mech snapshot push`. - - Usage: mech snapshot pop [options] [] - - Options: - --provision Enable provisioning - --no-delete Don't delete the snapshot after the restore - -h, --help Print this help - """ - puts_err(colored.red("Not implemented!")) - - def push(self, arguments): - """ - Push a snapshot of the current state of the machine. - - Usage: mech snapshot push [options] [] - - Notes: - Take a snapshot of the current state of the machine and 'push' - it onto the stack of states. You can use `mech snapshot pop` - to restore back to this state at any time. - - If you use `mech snapshot save` or restore at any point after - a push, pop will still bring you back to this pushed state. - - Options: - -h, --help Print this help - """ - puts_err(colored.red("Not implemented!")) - - def restore(self, arguments): - """ - Restore a snapshot taken previously with snapshot save. - - Usage: mech snapshot restore [options] [] - - Options: - --provision Enable provisioning - -h, --help Print this help - """ - puts_err(colored.red("Not implemented!")) - - def save(self, arguments): - """ - Take a snapshot of the current state of the machine. - - Usage: mech snapshot save [options] [] - - Notes: - Take a snapshot of the current state of the machine. The snapshot - can be restored via `mech snapshot restore` at any point in the - future to get back to this exact machine state. - - Snapshots are useful for experimenting in a machine and being able - to rollback quickly. - - Options: - -f --force Replace snapshot without confirmation - -h, --help Print this help - """ - name = arguments[''] - - instance_name = arguments[''] - instance_name = self.activate(instance_name) - - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - if vmrun.snapshot(name) is None: - puts_err(colored.red("Cannot take snapshot")) - else: - puts_err(colored.green("Snapshot {} taken".format(name))) +LOGGER = logging.getLogger(__name__) class Mech(MechCommand): @@ -478,35 +54,37 @@ class Mech(MechCommand): --debug Show debug messages. Common commands: - (list|ls) lists all available boxes + box manages boxes: add, list remove, etc. + destroy stops and deletes all traces of the instances + (down|stop|halt) stops the instances + global-status outputs status of all virutal machines on this host init initializes a new Mech environment by creating a Mechfile - destroy stops and deletes all traces of the Mech machine - (up|start) starts and provisions the Mech environment - (down|stop|halt) stops the Mech machine - suspend suspends the machine - pause pauses the Mech machine - ssh connects to machine via SSH - ssh-config outputs OpenSSH valid configuration to connect to the machine - scp copies files to and from the machine via SCP - ip outputs ip of the Mech machine - box manages boxes: installation, removal, etc. - global-status outputs status Mech environments for this user - status outputs status of the Mech machine - ps list running processes in Guest OS + ip outputs ip of an instance + (list|ls) lists all available boxes + pause pauses the instances + port displays information about guest port mappings provision provisions the Mech machine + ps list running processes for an instance reload restarts Mech machine, loads new Mechfile configuration resume resume a paused/suspended Mech machine - snapshot manages snapshots: saving, restoring, etc. - port displays information about guest port mappings - push deploys code in this environment to a configured destination + scp copies files to/from the machine via SCP + snapshot manages snapshots: save, list, remove, etc. + ssh connects to an instance via SSH + ssh-config outputs OpenSSH valid configuration to connect to the instances + status outputs status of the instances + suspend suspends the instances + (up|start) starts instances (aka virtual machines) + upgrade upgrade the instances For help on any individual command run `mech -h` + All "state" will be saved in .mech directory. (boxes and instances) + Example: Initializing and using a machine from HashiCorp's Vagrant Cloud: - mech init bento/ubuntu-14.04 + mech init bento/ubuntu-18.04 mech up mech ssh """ @@ -518,7 +96,8 @@ def __init__(self, arguments): logger = logging.getLogger() handler = logging.StreamHandler(sys.stderr) - formatter = logging.Formatter('%(levelname)s: %(message)s') + formatter = logging.Formatter('%(filename)s:%(lineno)s %(funcName)s() ' + '%(levelname)s: %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) if arguments['--debug']: @@ -527,140 +106,294 @@ def __init__(self, arguments): box = MechBox snapshot = MechSnapshot - def init(self, arguments): + def init(self, arguments): # pylint: disable=no-self-use """ Initializes a new mech environment by creating a Mechfile. - Usage: mech init [options] [] [] + Usage: mech init [options] Notes: - The box descriptor can be the name of a box on HashiCorp's Vagrant Cloud, - or a URL, a local .box or .tar file, or a local .json file containing - the catalog metadata. - - Options: - -f, --force Overwrite existing Mechfile - --insecure Do not validate SSL certificates - --cacert FILE CA certificate for SSL download - --capath DIR CA certificate directory for SSL download - --cert FILE A client SSL cert, if needed + - The location can be a: + + URL (ex: 'http://example.com/foo.box'), + + box file (ex: 'file:/mnt/boxen/foo.box'), + + json file (ex: 'file:/tmp/foo.json'), or + + HashiCorp account/box (ex: 'bento/ubuntu-18.04'). + - A default shared folder name 'mech' will be available + in the guest for the current directory. + - The 'add-me' option will add the currently logged in user to the guest, + add the same user to sudoers, and add the id_rsa.pub key to the + authorized_hosts file for that user. + - The 'use-me' option will use the currently logged in user for + future interactions with the guest instead of the vagrant user. + The first provisioning run will be done with 'vagrant' user. + + Options: + -a, --add-me Add the current host user/pubkey to guest + --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) --box-version VERSION Constrain version of the added box - --checksum CHECKSUM Checksum for the box - --checksum-type TYPE Checksum type (md5, sha1, sha256) - --name INSTANCE Name of the instance + -f, --force Overwrite existing Mechfile -h, --help Print this help + --name INSTANCE Name of the instance (myinst1) + -u, --use-me Use the current user for mech interactions """ - url = arguments[''] - if url: - name = arguments[''] - else: - url = arguments[''] - name = None - version = arguments['--box-version'] - instance_name = arguments['--name'] + add_me = arguments['--add-me'] + use_me = arguments['--use-me'] + name = arguments['--name'] + box_version = arguments['--box-version'] + box = arguments['--box'] + location = arguments[''] + + if not name or name == "": + name = "first" + force = arguments['--force'] - requests_kwargs = utils.get_requests_kwargs(arguments) + + LOGGER.debug('name:%s box:%s box_version:%s location:%s', name, box, box_version, location) if os.path.exists('Mechfile') and not force: - puts_err(colored.red(textwrap.fill( + sys.exit(colored.red(textwrap.fill( "`Mechfile` already exists in this directory. Remove it " - "before running `mech init`." - ))) - return - - puts_err(colored.green("Initializing mech")) - if utils.init_mechfile(instance_name, url, name=name, version=version, requests_kwargs=requests_kwargs): - puts_err(colored.green(textwrap.fill( - "A `Mechfile` has been initialized and placed in this directory. " - "You are now ready to `mech up` your first virtual environment!" - ))) + "before running `mech init`."))) + + print(colored.green("Initializing mech")) + utils.init_mechfile( + location=location, + box=box, + name=name, + box_version=box_version, + add_me=add_me, + use_me=use_me) + print(colored.green(textwrap.fill( + "A `Mechfile` has been initialized and placed in this directory. " + "You are now ready to `mech up` your first virtual environment!"))) + + def add(self, arguments): # pylint: disable=no-self-use + """ + Add instance to the Mechfile. + + Usage: mech add [options] + + Example box: bento/ubuntu-18.04 + + Notes: + - The 'add-me' option will add the currently logged in user to the guest, + add the same user to sudoers, and add the id_rsa.pub key to the authorized_hosts file + for that user. + + Options: + -a, --add-me Add the current host user/pubkey to guest + --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) + --box-version VERSION Constrain version of the added box + -h, --help Print this help + -u, --use-me Use the current user for mech interactions + """ + name = arguments[''] + box_version = arguments['--box-version'] + box = arguments['--box'] + add_me = arguments['--add-me'] + use_me = arguments['--use-me'] + location = arguments[''] + + if not name or name == "": + sys.exit(colored.red("Need to provide a name for the instance to add to the Mechfile.")) + + LOGGER.debug('name:%s box:%s box_version:%s location:%s', name, box, box_version, location) + + print(colored.green("Adding ({}) to the Mechfile.".format(name))) + + utils.add_to_mechfile( + location=location, + box=box, + name=name, + box_version=box_version, + add_me=add_me, + use_me=use_me) + print(colored.green("Added to the Mechfile.")) + + def remove(self, arguments): + """ + Remove instance from the Mechfile. + + Usage: mech remove [options] + + Options: + -h, --help Print this help + """ + name = arguments[''] + + if not name or name == "": + sys.exit(colored.red("Need to provide a name to be removed from the Mechfile.")) + + LOGGER.debug('name:%s', name) + + self.activate_mechfile() + inst = self.mechfile.get(name, None) + if inst: + print(colored.green("Removing ({}) from the Mechfile.".format(name))) + utils.remove_mechfile_entry(name=name) + print(colored.green("Removed from the Mechfile.")) else: - puts_err(colored.red("Couldn't initialize mech")) + sys.exit(colored.red("There is no instance called ({}) in the Mechfile.".format(name))) + + # add aliases for 'mech delete' + delete = remove + rm = remove - def up(self, arguments): + def up(self, arguments): # pylint: disable=invalid-name """ Starts and provisions the mech environment. Usage: mech up [options] [] - Options: + Notes: + - If no instance is specified, all instances will be started. + - The options (memsize, numvcpus, and no-nat) will only be applied + upon first run of the 'up' command. + - The 'no-nat' option will only be applied if there is no network + interface supplied in the box file. + - Unless 'disable-shared-folders' is used, a default read/write + share called "mech" will be mounted from the current directory. + (ex: '/mnt/hgfs/mech' on guest will have the file "Mechfile".) + To change shared folders, modify the Mechfile directly. + - The 'remove-vagrant' option will remove the vagrant account from the + guest VM which is what 'mech' uses to communicate with the VM. + Be sure you can connect/admin the instance before using this option. + + Options: + --disable-provisioning Do not provision + --disable-shared-folders Do not share folders with VM --gui Start GUI - --provision Enable provisioning - --insecure Do not validate SSL certificates - --cacert FILE CA certificate for SSL download - --capath DIR CA certificate directory for SSL download - --cert FILE A client SSL cert, if needed - --checksum CHECKSUM Checksum for the box - --checksum-type TYPE Checksum type (md5, sha1, sha256) + --memsize 1024 Specify the size of memory for VM --no-cache Do not save the downloaded box + --no-nat Do not use NAT network (i.e., bridged) + --numvcpus 1 Specify the number of vcpus for VM -h, --help Print this help + -r, --remove-vagrant Remove vagrant user """ gui = arguments['--gui'] + disable_shared_folders = arguments['--disable-shared-folders'] + disable_provisioning = arguments['--disable-provisioning'] save = not arguments['--no-cache'] - requests_kwargs = utils.get_requests_kwargs(arguments) + remove_vagrant = arguments['--remove-vagrant'] + + memsize = arguments['--memsize'] + numvcpus = arguments['--numvcpus'] + no_nat = arguments['--no-nat'] instance_name = arguments[''] - instance_name = self.activate(instance_name) - utils.index_active_instance(instance_name) + LOGGER.debug('gui:%s disable_shared_folders:%s disable_provisioning:%s ' + 'save:%s numvcpus:%s memsize:%s no_nat:%s', gui, + disable_shared_folders, disable_provisioning, save, + numvcpus, memsize, no_nat) - vmx = utils.init_box(self.box_name, self.box_version, requests_kwargs=requests_kwargs, save=save) - vmrun = VMrun(vmx, user=self.user, password=self.password) - puts_err(colored.blue("Bringing machine up...")) - started = vmrun.start(gui=gui) - if started is None: - puts_err(colored.red("VM not started")) + if instance_name: + # single instance + instances = [instance_name] else: - time.sleep(3) - puts_err(colored.blue("Getting IP address...")) - lookup = self.get("enable_ip_lookup", False) - ip = vmrun.getGuestIPAddress(lookup=lookup) - puts_err(colored.blue("Sharing current folder...")) - vmrun.enableSharedFolders() - vmrun.addSharedFolder('mech', os.getcwd(), quiet=True) - if ip: - if started: - puts_err(colored.green("VM started on {}".format(ip))) - else: - puts_err(colored.yellow("VM was already started on {}".format(ip))) + # multiple instances + instances = self.instances() + + for instance in instances: + inst = MechInstance(instance) + + location = inst.url + if not location: + location = inst.box_file + + # only run init_box on first "up" + if not inst.created: + inst.vmx = utils.init_box( + instance, + box=inst.box, + box_version=inst.box_version, + location=location, + instance_path=inst.path, + save=save, + numvcpus=numvcpus, + memsize=memsize, + no_nat=no_nat) + inst.created = True + + # Note: user/password is needed for provisioning + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + print(colored.blue("Bringing machine ({}) up...".format(instance))) + started = vmrun.start(gui=gui) + if started is None: + print(colored.red("VM not started")) else: - if started: - puts_err(colored.green("VM started on an unknown IP address")) + print(colored.blue("Getting IP address...")) + lookup = inst.enable_ip_lookup + ip_address = vmrun.get_guest_ip_address(lookup=lookup) + if not disable_shared_folders: + utils.share_folders(vmrun, inst) + if ip_address: + if started: + print(colored.green("VM ({})" + "started on {}".format(instance, ip_address))) + else: + print(colored.yellow("VM ({}) was already started " + "on {}".format(instance, ip_address))) else: - puts_err(colored.yellow("VM was already started on an unknown IP address")) + if started: + print(colored.green("VM ({}) started on an unknown " + "IP address".format(instance))) + else: + print(colored.yellow("VM ({}) was already started on an " + "unknown IP address".format(instance))) + + # if not already using preshared key, switch to it + if not inst.use_psk and inst.auth: + utils.add_auth(inst) + inst.switch_to_psk() + + if remove_vagrant: + utils.del_user(inst, 'vagrant') + + if not disable_provisioning: + utils.provision(inst, show=False) + + # allows "mech start" to alias to "mech up" start = up - def global_status(self, arguments): + def global_status(self, arguments): # pylint: disable=no-self-use,unused-argument """ - Outputs mech environments status for this user. + Outputs info about all VMs running on this computer. Usage: mech global-status [options] Options: - --prune Prune invalid entries -h, --help Print this help """ vmrun = VMrun() print(vmrun.list()) - def ps(self, arguments): + def ps(self, arguments): # pylint: disable=invalid-name,no-self-use """ List running processes in Guest OS. - Usage: mech ps [options] [] + Usage: mech ps [options] Options: -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) - vmrun = VMrun(self.vmx, self.user, self.password) - print(vmrun.listProcessesInGuest()) + inst = MechInstance(instance_name) + + if inst.created: + # Note: user/password is needed for ps + vmrun = VMrun(inst.vmx, inst.user, inst.password) + print(vmrun.list_processes_in_guest()) + else: + print("VM {} not created.".format(instance_name)) + + # alias "mech process_status" to "mech ps" + process_status = ps def status(self, arguments): """ - Outputs status of the Mech machine. + Outputs status of the instances. Usage: mech status [options] [] @@ -668,32 +401,46 @@ def status(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) - - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - - box_name = self.box_name - lookup = self.get("enable_ip_lookup", False) - ip = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) - state = vmrun.checkToolsState(quiet=True) - print("Current machine states:" + os.linesep) - if ip is None: - ip = "poweroff" - elif not ip: - ip = "unknown" - print("%s\t%s\t(VMware Tools %s)" % (box_name, ip, state)) - - if ip == "poweroff": - print(os.linesep + "The VM is powered off. To restart the VM, simply run `mech up`") - elif ip == "unknown": - print(os.linesep + "The VM is on. but it has no IP to connect to, VMware Tools must be installed") - elif state in ("installed", "running"): - print(os.linesep + "The VM is ready. Connect to it using `mech ssh`") + if instance_name: + # single instance + instances = [instance_name] + else: + # multiple instances + instances = self.instances() + + for instance in instances: + inst = MechInstance(instance) + + if inst.created: + vmrun = VMrun(inst.vmx) + + lookup = inst.enable_ip_lookup + ip_address = vmrun.get_guest_ip_address(wait=False, quiet=True, lookup=lookup) + state = vmrun.check_tools_state(quiet=True) + + print("Current machine state:" + os.linesep) + if ip_address is None: + ip_address = "poweroff" + elif not ip_address: + ip_address = "unknown" + print("%s\t%s\t%s\t(VMware Tools %s)" % (inst.name, inst.box, ip_address, state)) + + if ip_address == "poweroff": + print(os.linesep + "The VM is powered off. To restart the VM, " + "simply run `mech up {}`".format(instance)) + elif ip_address == "unknown": + print(os.linesep + "The VM is on. but it has no IP to connect to," + "VMware Tools must be installed") + elif state in ("installed", "running"): + print(os.linesep + "The VM is ready. Connect to it " + "using `mech ssh {}`".format(instance)) + else: + print("The VM ({}) has not been created.".format(instance)) def destroy(self, arguments): """ - Stops and deletes all traces of the Mech machine. + Stops and deletes all traces of the instances. Usage: mech destroy [options] [] @@ -704,58 +451,76 @@ def destroy(self, arguments): force = arguments['--force'] instance_name = arguments[''] - instance_name = self.activate(instance_name) if instance_name: - instance = utils.settle_instance(instance_name) - path = instance['path'] + # single instance + instances = [instance_name] else: - path = os.getcwd() - mech_path = os.path.join(path, '.mech') - - if os.path.exists(mech_path): - if force or utils.confirm("Are you sure you want to delete {instance_name} at {path}".format(instance_name=instance_name, path=path), default='n'): - puts_err(colored.green("Deleting...")) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - vmrun.stop(mode='hard', quiet=True) - time.sleep(3) - vmrun.deleteVM() - shutil.rmtree(mech_path) + # multiple instances + instances = self.instances() + + for instance in instances: + inst = MechInstance(instance) + + if os.path.exists(inst.path): + if force or utils.confirm("Are you sure you want to delete {} " + "at {}".format(inst.name, inst.path), default='n'): + print(colored.green("Deleting ({})...".format(instance))) + vmrun = VMrun(inst.vmx) + vmrun.stop(mode='hard', quiet=True) + vmrun.delete_vm() + if os.path.exists(inst.path): + shutil.rmtree(inst.path) + print("Deleted") + else: + print(colored.red("Delete aborted.")) else: - puts_err(colored.red("Deletion aborted")) - else: - puts_err(colored.red("The box hasn't been initialized.")) + print(colored.red("VM ({}) not created.".format(instance))) def down(self, arguments): """ - Stops the Mech machine. + Stops the instances. Usage: mech down [options] [] Options: - --force Force a hard stop + -f, --force Force a hard stop -h, --help Print this help """ force = arguments['--force'] instance_name = arguments[''] - instance_name = self.activate(instance_name) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - if not force and vmrun.installedTools(): - stopped = vmrun.stop() - else: - stopped = vmrun.stop(mode='hard') - if stopped is None: - puts_err(colored.red("Not stopped", vmrun)) + if instance_name: + # single instance + instances = [instance_name] else: - puts_err(colored.green("Stopped", vmrun)) + # multiple instances + instances = self.instances() + + for instance in instances: + inst = MechInstance(instance) + + if inst.created: + vmrun = VMrun(inst.vmx) + if not force and vmrun.installed_tools(): + stopped = vmrun.stop() + else: + stopped = vmrun.stop(mode='hard') + if stopped is None: + print(colored.red("Not stopped", vmrun)) + else: + print(colored.green("Stopped", vmrun)) + else: + print(colored.red("VM ({}) not created.".format(instance))) + + # alias 'mech stop' and 'mech halt' to 'mech down' stop = down halt = down def pause(self, arguments): """ - Pauses the Mech machine. + Pauses the instances. Usage: mech pause [options] [] @@ -763,69 +528,140 @@ def pause(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - if vmrun.pause() is None: - puts_err(colored.red("Not paused", vmrun)) + if instance_name: + # single instance + instances = [instance_name] else: - puts_err(colored.yellow("Paused", vmrun)) + # multiple instances + instances = self.instances() - def resume(self, arguments): + for instance in instances: + inst = MechInstance(instance) + + if inst.created: + vmrun = VMrun(inst.vmx) + if vmrun.pause() is None: + print(colored.red("Not paused", vmrun)) + else: + print(colored.yellow("Paused", vmrun)) + else: + print(colored.red("VM ({}) not created.".format(instance))) + + def upgrade(self, arguments): """ - Resume a paused/suspended Mech machine. + Upgrade the vm file format and virtual hardware for the instance(s). - Usage: mech resume [options] [] + Usage: mech upgrade [options] [] + + Note: The VMs must be created and stopped. Options: - --provision Enable provisioning -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) - utils.index_active_instance(instance_name) + if instance_name: + # single instance + instances = [instance_name] + else: + # multiple instances + instances = self.instances() - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + for instance in instances: + inst = MechInstance(instance) - # Try to unpause - if vmrun.unpause(quiet=True) is not None: - time.sleep(1) - puts_err(colored.blue("Getting IP address...")) - lookup = self.get("enable_ip_lookup", False) - ip = vmrun.getGuestIPAddress(lookup=lookup) - if ip: - puts_err(colored.green("VM resumed on {}".format(ip))) + if inst.created: + vmrun = VMrun(inst.vmx) + state = vmrun.check_tools_state(quiet=True) + if state == "running": + print("VM must be stopped before doing upgrade.") + else: + if vmrun.upgradevm(quiet=False) is None: + print(colored.red("Not upgraded", vmrun)) + else: + print(colored.yellow("Upgraded", vmrun)) else: - puts_err(colored.green("VM resumed on an unknown IP address")) + print(colored.red("VM ({}) not created.".format(instance))) + + def resume(self, arguments): + """ + Resume a paused/suspended instances. - # Otherwise try starting + Usage: mech resume [options] [] + + Options: + --disable-shared-folders Do not share folders with VM + -h, --help Print this help + """ + instance_name = arguments[''] + disable_shared_folders = arguments['--disable-shared-folders'] + + LOGGER.debug('instance_name:%s ' + 'disable_shared_folders:%s', instance_name, disable_shared_folders) + + if instance_name: + # single instance + instances = [instance_name] else: - started = vmrun.start() - if started is None: - puts_err(colored.red("VM not started")) - else: - time.sleep(3) - puts_err(colored.blue("Getting IP address...")) - lookup = self.get("enable_ip_lookup", False) - ip = vmrun.getGuestIPAddress(lookup=lookup) - puts_err(colored.blue("Sharing current folder...")) - vmrun.enableSharedFolders() - vmrun.addSharedFolder('mech', os.getcwd(), quiet=True) - if ip: - if started: - puts_err(colored.green("VM started on {}".format(ip))) + # multiple instances + instances = self.instances() + + for instance in instances: + inst = MechInstance(instance) + LOGGER.debug('instance:%s inst.vmx:%s', instance, inst.vmx) + + # if we have started this instance before, try to unpause + if inst.created: + + vmrun = VMrun(inst.vmx) + + if vmrun.unpause(quiet=True) is not None: + print(colored.blue("Getting IP address...")) + lookup = inst.enable_ip_lookup + ip_address = vmrun.get_guest_ip_address(lookup=lookup) + if not disable_shared_folders: + utils.share_folders(vmrun, inst) + else: + print(colored.blue("Disabling shared folders...")) + vmrun.disable_shared_folders(quiet=False) + if ip_address: + print(colored.green("VM resumed on {}".format(ip_address))) else: - puts_err(colored.yellow("VM already was started on {}".format(ip))) + print(colored.green("VM resumed on an unknown IP address")) + else: - if started: - puts_err(colored.green("VM started on an unknown IP address")) + # Otherwise try starting + vmrun = VMrun(inst.vmx) + started = vmrun.start() + if started is None: + print(colored.red("VM not started")) else: - puts_err(colored.yellow("VM already was started on an unknown IP address")) + print(colored.blue("Getting IP address...")) + lookup = inst.enable_ip_lookup + ip_address = vmrun.get_guest_ip_address(lookup=lookup) + if not disable_shared_folders: + utils.share_folders(vmrun, inst) + if ip_address: + if started: + print(colored.green("VM ({}) started on " + "{}".format(instance, ip_address))) + else: + print(colored.yellow("VM ({}) already was started " + "on {}".format(instance, ip_address))) + else: + if started: + print(colored.green("VM ({}) started on an unknown " + "IP address".format(instance))) + else: + print(colored.yellow("VM ({}) already was started on an " + "unknown IP address".format(instance))) + else: + print(colored.red("VM not created")) def suspend(self, arguments): """ - Suspends the machine. + Suspends instances. Usage: mech suspend [options] [] @@ -833,13 +669,25 @@ def suspend(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - if vmrun.suspend() is None: - puts_err(colored.red("Not suspended", vmrun)) + if instance_name: + # single instance + instances = [instance_name] else: - puts_err(colored.green("Suspended", vmrun)) + # multiple instances + instances = self.instances() + + for instance in instances: + inst = MechInstance(instance) + + if inst.created: + vmrun = VMrun(inst.vmx) + if vmrun.suspend() is None: + print(colored.red("Not suspended", vmrun)) + else: + print(colored.green("Suspended", vmrun)) + else: + print("VM has not been created.") def ssh_config(self, arguments): """ @@ -851,15 +699,26 @@ def ssh_config(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) - print(utils.config_ssh_string(self.config_ssh)) + if instance_name: + # single instance + instances = [instance_name] + else: + # multiple instances + instances = self.instances() - def ssh(self, arguments): + for instance in instances: + inst = MechInstance(instance) + if inst.created: + print(utils.config_ssh_string(inst.config_ssh())) + else: + print(colored.red("VM ({}) is not created.".format(instance))) + + def ssh(self, arguments): # pylint: disable=no-self-use """ Connects to machine via SSH. - Usage: mech ssh [options] [] [-- ...] + Usage: mech ssh [options] [-- ...] Options: -c, --command COMMAND Execute an SSH command directly @@ -867,52 +726,43 @@ def ssh(self, arguments): -h, --help Print this help """ plain = arguments['--plain'] - extra = arguments[''] + extra = arguments[''] command = arguments['--command'] - instance_name = arguments[''] - instance_name = self.activate(instance_name) - - config_ssh = self.config_ssh - fp = tempfile.NamedTemporaryFile(delete=False) - try: - fp.write(utils.config_ssh_string(config_ssh).encode('utf-8')) - fp.close() - - cmds = ['ssh'] - if not plain: - cmds.extend(('-F', fp.name)) - if extra: - cmds.extend(extra) - if not plain: - cmds.append(config_ssh['Host']) - if command: - cmds.extend(('--', command)) - - logger.debug(" ".join("'{}'".format(c.replace("'", "\\'")) if ' ' in c else c for c in cmds)) - return subprocess.call(cmds) - finally: - os.unlink(fp.name) - - def scp(self, arguments): + instance = arguments[''] + + inst = MechInstance(instance) + + if inst.created: + rc, stdout, stderr = utils.ssh(inst, command, plain, extra) + LOGGER.debug('command:%s rc:%d stdout:%s stderr:%s', command, rc, stdout, stderr) + if stdout: + print(stdout) + if stderr: + print(stderr) + sys.exit(rc) + else: + print("VM not created.") + + def scp(self, arguments): # pylint: disable=no-self-use """ Copies files to and from the machine via SCP. - Usage: mech scp [options] [-- ...] + Usage: mech scp [options] [-- ...] Options: -h, --help Print this help """ - extra = arguments[''] + extra = arguments[''] src = arguments[''] dst = arguments[''] dst_instance, dst_is_host, dst = dst.partition(':') src_instance, src_is_host, src = src.partition(':') + instance_name = None if dst_is_host and src_is_host: - puts_err(colored.red("Both src and host are host destinations")) - sys.exit(1) + sys.exit(colored.red("Both src and dst are host destinations")) if dst_is_host: instance_name = dst_instance else: @@ -922,48 +772,42 @@ def scp(self, arguments): else: src = src_instance - instance_name = self.activate(instance_name) - - config_ssh = self.config_ssh - fp = tempfile.NamedTemporaryFile(delete=False) - try: - fp.write(utils.config_ssh_string(config_ssh)) - fp.close() + if instance_name is None: + sys.exit(colored.red("Could not determine instance name.")) - cmds = ['scp'] - cmds.extend(('-F', fp.name)) - if extra: - cmds.extend(extra) + inst = MechInstance(instance_name) - host = config_ssh['Host'] - dst = '{}:{}'.format(host, dst) if dst_is_host else dst - src = '{}:{}'.format(host, src) if src_is_host else src - cmds.extend((src, dst)) - - logger.debug(" ".join("'{}'".format(c.replace("'", "\\'")) if ' ' in c else c for c in cmds)) - return subprocess.call(cmds) - finally: - os.unlink(fp.name) + if inst.created: + utils.scp(inst, src, dst, dst_is_host, extra) + else: + print(colored.red('VM not created.')) - def ip(self, arguments): + def ip(self, arguments): # pylint: disable=invalid-name,no-self-use """ Outputs ip of the Mech machine. - Usage: mech ip [options] [] + Usage: mech ip [options] Options: -h, --help Print this help """ - instance_name = arguments[''] - instance_name = self.activate(instance_name) + instance = arguments[''] + + inst = MechInstance(instance) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - lookup = self.get("enable_ip_lookup", False) - ip = vmrun.getGuestIPAddress(lookup=lookup) - if ip: - puts_err(colored.green(ip)) + if inst.created: + vmrun = VMrun(inst.vmx) + lookup = inst.enable_ip_lookup + ip_address = vmrun.get_guest_ip_address(lookup=lookup) + if ip_address: + print(colored.green(ip_address)) + else: + print(colored.red("Unknown IP address")) else: - puts_err(colored.red("Unknown IP address")) + print(colored.yellow("VM not created")) + + # alias 'mech ip_address' to 'mech ip' + ip_address = ip def provision(self, arguments): """ @@ -973,46 +817,25 @@ def provision(self, arguments): Options: -h, --help Print this help + -s, --show-only Show the provisioning info (do not run) """ + show = arguments['--show-only'] instance_name = arguments[''] - instance_name = self.activate(instance_name) - - vmrun = VMrun(self.vmx, self.user, self.password) - - if not vmrun.installedTools(): - puts_err(colored.red("Tools not installed")) - return - - provisioned = 0 - for i, provision in enumerate(self.get('provision', [])): - - if provision.get('type') == 'file': - source = provision.get('source') - destination = provision.get('destination') - if utils.provision_file(vmrun, source, destination) is None: - puts_err(colored.red("Not Provisioned")) - return - provisioned += 1 - - elif provision.get('type') == 'shell': - inline = provision.get('inline') - path = provision.get('path') - args = provision.get('args') - if not isinstance(args, list): - args = [args] - if utils.provision_shell(vmrun, inline, path, args) is None: - puts_err(colored.red("Not Provisioned")) - return - provisioned += 1 - else: - puts_err(colored.red("Not Provisioned ({}".format(i))) - return + if instance_name: + # single instance + instances = [instance_name] else: - puts_err(colored.green("Provisioned {} entries".format(provisioned))) - return + # multiple instances + instances = self.instances() + + for instance in instances: + inst = MechInstance(instance) - puts_err(colored.red("Not Provisioned ({}".format(i))) + if inst.created: + utils.provision(inst, show) + else: + print("VM not created.") def reload(self, arguments): """ @@ -1021,33 +844,47 @@ def reload(self, arguments): Usage: mech reload [options] [] Options: - --provision Enable provisioning -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - - puts_err(colored.blue("Reloading machine...")) - started = vmrun.reset() - if started is None: - puts_err(colored.red("VM not restarted")) + if instance_name: + # single instance + instances = [instance_name] else: - time.sleep(3) - puts_err(colored.blue("Getting IP address...")) - lookup = self.get("enable_ip_lookup", False) - ip = vmrun.getGuestIPAddress(lookup=lookup) - if ip: - if started: - puts_err(colored.green("VM started on {}".format(ip))) + # multiple instances + instances = self.instances() + + for instance in instances: + inst = MechInstance(instance) + + if inst.created: + vmrun = VMrun(inst.vmx) + + print(colored.blue("Reloading machine...")) + started = vmrun.reset() + if started is None: + print(colored.red("VM not restarted")) else: - puts_err(colored.yellow("VM already was started on {}".format(ip))) + print(colored.blue("Getting IP address...")) + lookup = inst.enable_ip_lookup + ip_address = vmrun.get_guest_ip_address(lookup=lookup) + if ip_address: + if started: + print(colored.green("VM ({}) started " + "on {}".format(instance, ip_address))) + else: + print(colored.yellow("VM ({}) already was started on " + "{}".format(instance, ip_address))) + else: + if started: + print(colored.green("VM ({}) started on an unknown IP " + "address".format(instance))) + else: + print(colored.yellow("VM ({}) already was started " + "on an unknown IP address".format(instance))) else: - if started: - puts_err(colored.green("VM started on an unknown IP address")) - else: - puts_err(colored.yellow("VM already was started on an unknown IP address")) + print("VM not created.") def port(self, arguments): """ @@ -1057,76 +894,87 @@ def port(self, arguments): Options: --guest PORT Output the host port that maps to the given guest port - --machine-readable Display machine-readable output -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) - - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - for network in vmrun.listHostNetworks().split('\n'): - network = network.split() - if len(network) > 2 and network[2] == 'nat': - print(vmrun.listPortForwardings(network[1])) - break + + if instance_name: + # single instance + instances = [instance_name] else: - puts_err(colored.red("Cannot find a nat network")) + # multiple instances + instances = self.instances() + + # FUTURE: implement port forwarding? + for instance in instances: + inst = MechInstance(instance) + + print('Instance ({}):'. format(instance)) + nat_found = False + vmrun = VMrun(inst.vmx) + for line in vmrun.list_host_networks().split('\n'): + network = line.split() + if len(network) > 2 and network[2] == 'nat': + print(vmrun.list_port_forwardings(network[1])) + nat_found = True + if not nat_found: + print(colored.red("Cannot find a nat network"), file=sys.stderr) - def push(self, arguments): + def list(self, arguments): """ - Deploys code in this environment to a configured destination. + Lists all available boxes from Mechfile. - Usage: mech push [options] [] + Usage: mech list [options] Options: + -d, --detail Print detailed info -h, --help Print this help """ - puts_err(colored.red("Not implemented!")) - def list(self, arguments): - """ - Lists all available boxes. + detail = arguments['--detail'] - Usage: mech list [options] + self.activate_mechfile() - Options: - -h, --help Print this help - """ - print("{}\t{}\t{}\t{}\t{}".format( - 'NAME'.rjust(20), - 'ADDRESS'.rjust(15), - 'BOX'.rjust(35), - 'VERSION'.rjust(12), - 'PATH', - )) - for instance_name, instance in utils.instances().items(): - path = instance.get('path') - if path and os.path.exists(path): - self.activate(instance_name) - mech_path = os.path.join(path, '.mech') - if os.path.exists(mech_path): - vmx = self.get_vmx(silent=True) - if vmx: - vmrun = VMrun(vmx, user=self.user, password=self.password) - lookup = self.get("enable_ip_lookup", False) - ip = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) - else: - ip = colored.red("invalid") - if ip is None: - ip = colored.yellow("poweroff") - elif not ip: - ip = colored.green("running") - else: - ip = colored.green(ip) + if detail: + print('Instance Details') + print() + else: + print("{}\t{}\t{}\t{}".format( + 'NAME'.rjust(20), + 'ADDRESS'.rjust(15), + 'BOX'.rjust(35), + 'VERSION'.rjust(12) + )) + + for name in self.mechfile: + inst = MechInstance(name, self.mechfile) + if inst.created: + vmrun = VMrun(inst.vmx) + lookup = inst.enable_ip_lookup + ip_address = vmrun.get_guest_ip_address(wait=False, quiet=True, lookup=lookup) + if ip_address is None: + ip_address = colored.yellow("poweroff") + elif not ip_address: + ip_address = colored.green("running") else: - ip = "" - box_name = self.box_name or "" - box_version = self.box_version or "" - print("{}\t{}\t{}\t{}\t{}".format( - colored.green(instance_name.rjust(20)), - ip.rjust(15), - box_name.rjust(35), - box_version.rjust(12), - path, + ip_address = colored.green(ip_address) + else: + ip_address = "notcreated" + + if detail: + print(inst) + print() + else: + # deal with box_version being none + box_version = inst.box_version + if inst.box_version is None: + box_version = '' + print("{}\t{}\t{}\t{}".format( + colored.green(name.rjust(20)), + ip_address.rjust(15), + inst.box.rjust(35), + box_version.rjust(12) )) + + # allow 'mech ls' as alias to 'mech list' ls = list diff --git a/mech/mech_box.py b/mech/mech_box.py new file mode 100644 index 0000000..3a2cda3 --- /dev/null +++ b/mech/mech_box.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016-2017 Kevin Chung +# Copyright (c) 2018 German Mendez Bravo (Kronuz) +# Copyright (c) 2020 Mike Kinney +# +# 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. +# +"""MechBox class""" + +from __future__ import print_function, absolute_import + +import os +import fnmatch +import logging +import shutil + +from . import utils +from .mech_command import MechCommand + +LOGGER = logging.getLogger(__name__) + + +class MechBox(MechCommand): + """ + Usage: mech box [...] + + Available subcommands: + add add a box to the catalog of available boxes + (list|ls) list available boxes in the catalog + (remove|delete) removes a box that matches the given name + + For help on any individual subcommand run `mech box -h` + """ + + def add(self, arguments): # pylint: disable=no-self-use + """ + Add a box to the catalog of available boxes. + + Usage: mech box add [options] + + Notes: + The location can be a: + URL (ex: 'http://example.com/foo.box'), + box file (ex: 'file:/mnt/boxen/foo.box'), + json file (ex: 'file:/tmp/foo.json'), or + HashiCorp account/box (ex: 'bento/ubuntu-18.04'). + + Options: + --box-version VERSION Constrain version of the added box + -f, --force Overwrite an existing box if it exists + -h, --help Print this help + """ + + location = arguments[''] + box_version = arguments['--box-version'] + + force = arguments['--force'] + utils.add_box(name=None, box=None, location=location, box_version=box_version, + force=force) + + def list(self, arguments): # pylint: disable=no-self-use,unused-argument + """ + List all available boxes in the catalog. + + Usage: mech box list [options] + + Options: + -h, --help Print this help + """ + + print("{}\t{}".format( + 'BOX'.rjust(35), + 'VERSION'.rjust(12), + )) + path = os.path.abspath(os.path.join(utils.mech_dir(), 'boxes')) + for root, _, filenames in os.walk(path): + for filename in fnmatch.filter(filenames, '*.box'): + directory = os.path.dirname(os.path.join(root, filename))[len(path) + 1:] + account, box, version = (directory.split('/', 2) + ['', ''])[:3] + print("{}\t{}".format( + "{}/{}".format(account, box).rjust(35), + version.rjust(12), + )) + + # add alias for 'mech box ls' + ls = list + + def remove(self, arguments): # pylint: disable=no-self-use + """ + Remove a box from mech that matches the given name and version. + + Usage: mech box remove [options] + + Options: + -h, --help Print this help + """ + name = arguments[''] + box_version = arguments[''] + path = os.path.abspath(os.path.join(utils.mech_dir(), 'boxes', name, box_version)) + if os.path.exists(path): + shutil.rmtree(path) + print("Removed {} {}".format(name, box_version)) + else: + print("No boxes were removed.") + + # add alias for 'mech box delete' + delete = remove diff --git a/mech/mech_command.py b/mech/mech_command.py new file mode 100644 index 0000000..8312d27 --- /dev/null +++ b/mech/mech_command.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016-2017 Kevin Chung +# Copyright (c) 2018 German Mendez Bravo (Kronuz) +# Copyright (c) 2020 Mike Kinney +# +# 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. +# +"""MechCommand class""" + +from __future__ import print_function, absolute_import + +import logging + +from . import utils +from .command import Command + +LOGGER = logging.getLogger(__name__) + + +class MechCommand(Command): + """Class for the mech commands from help doc (as python object).""" + mechfile = None + + def activate_mechfile(self): + """Load the Mechfile.""" + self.mechfile = utils.load_mechfile() + LOGGER.debug("loaded mechfile:%s", self.mechfile) + + def instances(self): + """Returns a list of the instances from the Mechfile.""" + if not self.mechfile: + self.activate_mechfile() + return list(self.mechfile) diff --git a/mech/mech_instance.py b/mech/mech_instance.py new file mode 100644 index 0000000..f4f133b --- /dev/null +++ b/mech/mech_instance.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016-2017 Kevin Chung +# Copyright (c) 2018 German Mendez Bravo (Kronuz) +# Copyright (c) 2020 Mike Kinney +# +# 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. +# +"""MechInstance class""" + +from __future__ import print_function, absolute_import + +import os +import re +import sys +import logging +import textwrap + +from clint.textui import colored + +from . import utils +from .vmrun import VMrun + +LOGGER = logging.getLogger(__name__) + +DEFAULT_USER = 'vagrant' +DEFAULT_PASSWORD = 'vagrant' +INSECURE_PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzI +w+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoP +kcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2 +hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NO +Td0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcW +yLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQIBIwKCAQEA4iqWPJXtzZA68mKd +ELs4jJsdyky+ewdZeNds5tjcnHU5zUYE25K+ffJED9qUWICcLZDc81TGWjHyAqD1 +Bw7XpgUwFgeUJwUlzQurAv+/ySnxiwuaGJfhFM1CaQHzfXphgVml+fZUvnJUTvzf +TK2Lg6EdbUE9TarUlBf/xPfuEhMSlIE5keb/Zz3/LUlRg8yDqz5w+QWVJ4utnKnK +iqwZN0mwpwU7YSyJhlT4YV1F3n4YjLswM5wJs2oqm0jssQu/BT0tyEXNDYBLEF4A +sClaWuSJ2kjq7KhrrYXzagqhnSei9ODYFShJu8UWVec3Ihb5ZXlzO6vdNQ1J9Xsf +4m+2ywKBgQD6qFxx/Rv9CNN96l/4rb14HKirC2o/orApiHmHDsURs5rUKDx0f9iP +cXN7S1uePXuJRK/5hsubaOCx3Owd2u9gD6Oq0CsMkE4CUSiJcYrMANtx54cGH7Rk +EjFZxK8xAv1ldELEyxrFqkbE4BKd8QOt414qjvTGyAK+OLD3M2QdCQKBgQDtx8pN +CAxR7yhHbIWT1AH66+XWN8bXq7l3RO/ukeaci98JfkbkxURZhtxV/HHuvUhnPLdX +3TwygPBYZFNo4pzVEhzWoTtnEtrFueKxyc3+LjZpuo+mBlQ6ORtfgkr9gBVphXZG +YEzkCD3lVdl8L4cw9BVpKrJCs1c5taGjDgdInQKBgHm/fVvv96bJxc9x1tffXAcj +3OVdUN0UgXNCSaf/3A/phbeBQe9xS+3mpc4r6qvx+iy69mNBeNZ0xOitIjpjBo2+ +dBEjSBwLk5q5tJqHmy/jKMJL4n9ROlx93XS+njxgibTvU6Fp9w+NOFD/HvxB3Tcz +6+jJF85D5BNAG3DBMKBjAoGBAOAxZvgsKN+JuENXsST7F89Tck2iTcQIT8g5rwWC +P9Vt74yboe2kDT531w8+egz7nAmRBKNM751U/95P9t88EDacDI/Z2OwnuFQHCPDF +llYOUI+SpLJ6/vURRbHSnnn8a/XG+nzedGH5JGqEJNQsz+xT2axM0/W/CRknmGaJ +kda/AoGANWrLCz708y7VYgAtW2Uf1DPOIYMdvo6fxIB5i9ZfISgcJ/bbCUkFrhoH ++vq/5CIWxCPp0f85R4qxxQ5ihxJ0YDQT9Jpx4TMss4PSavPaBH3RXow5Ohe+bYoQ +NE5OgEXk2wVfZczCZpigBKbKZHNYcelXtTt/nP3rsCuGcM4h53s= +-----END RSA PRIVATE KEY----- +""" + + +class MechInstance(): + """Class to hold a mech instance (aka virtual machine).""" + + def __init__(self, name, mechfile=None): + """Constructor for the mech instance.""" + if not name or name == "": + raise AttributeError("Must provide a name for the instance.") + if not mechfile: + mechfile = utils.load_mechfile() + LOGGER.debug("loaded mechfile:%s", mechfile) + if mechfile.get(name, None): + self.name = name + else: + sys.exit(colored.red("Instance ({}) was not found in the " + "Mechfile".format(name))) + self.box = mechfile[name].get('box', None) + self.box_version = mechfile[name].get('box_version', None) + self.url = mechfile[name].get('url', None) + self.box_file = mechfile[name].get('file', None) + self.provision = mechfile[name].get('provision', None) + self.enable_ip_lookup = False + self.config = {} + self.auth = mechfile[name].get('auth', None) + self.shared_folders = mechfile[name].get('shared_folders', []) + self.user = DEFAULT_USER + self.password = DEFAULT_PASSWORD + self.use_psk = False + self.path = os.path.join(utils.mech_dir(), name) + vmx = utils.locate(self.path, '*.vmx') + # Note: If vm has not been started vmx will be None + if vmx: + self.vmx = vmx + self.created = True + else: + self.vmx = None + self.created = False + + # If vmx exists, then the VM has already been created. + # See if we need to switch to preshared key authentication + # for interactions with this guest. + if self.created: + self.switch_to_psk() + + def switch_to_psk(self): + """Switch to using preshared key, instead of using user/password.""" + if self.auth: + mech_use = self.auth.get('mech_use', False) + username = self.auth.get('username') + if username and username != '' and mech_use: + self.user = username + self.password = None + self.use_psk = True + + def __repr__(self): + """Return a representation of a Mech instance.""" + sep = '\n' + return ('name:{name}{sep}created:{created}{sep}box:{box}{sep}' + 'box_version:{box_version}{sep}' + 'url:{url}{sep}box_file:{box_file}{sep}provision:{provision}{sep}' + 'vmx:{vmx}{sep}user:{user}{sep}' + 'password:{password}{sep}enable_ip_lookup:{enable_ip_lookup}' + '{sep}config:{config}{sep}shared_folders:{shared_folders}' + '{sep}auth:{auth}'.format(name=self.name, created=self.created, + box=self.box, box_version=self.box_version, + url=self.url, box_file=self.box_file, + provision=self.provision, vmx=self.vmx, + user=self.user, password=self.password, + enable_ip_lookup=self.enable_ip_lookup, + config=self.config, + shared_folders=self.shared_folders, + auth=self.auth, sep=sep)) + + def config_ssh(self): + """Configure ssh to work. If needed, create an insecure private key file for ssh/scp.""" + vmrun = VMrun(self.vmx) + lookup = self.enable_ip_lookup + ip_address = vmrun.get_guest_ip_address(wait=False, + lookup=lookup) if vmrun.installed_tools() else None + if not ip_address: + sys.exit(colored.red(textwrap.fill( + "This Mech machine is reporting that it is not yet ready for SSH. " + "Make sure your machine is created and running and try again. " + "Additionally, check the output of `mech status` to verify " + "that the machine is in the state that you expect."))) + + if not self.use_psk: + key = os.path.abspath(os.path.join( + utils.mech_dir(), "insecure_private_key")) + if not os.path.exists(key): + with open(key, 'w') as the_file: + the_file.write(INSECURE_PRIVATE_KEY) + os.chmod(key, 0o400) + else: + key = '~/.ssh/id_rsa' + + self.config = { + "Host": self.name, + "User": self.user, + "Port": "22", + "UserKnownHostsFile": "/dev/null", + "StrictHostKeyChecking": "no", + "PasswordAuthentication": "no", + "IdentityFile": key, + "IdentitiesOnly": "yes", + "LogLevel": "FATAL", + } + for key, value in self.config.items(): + key = re.sub(r'[ _]+', r' ', key) + key = re.sub(r'(?<=[^_])([A-Z])', r' \1', key).lower() + key = re.sub(r'^( *)(.*?)( *)$', r'\2', key) + + def callback(pat): + return pat.group(1).upper() + + key = re.sub(r' (\w)', callback, key) + if key[0].islower(): + key = key[0].upper() + key[1:] + self.config[key] = value + self.config.update({ + "HostName": ip_address, + }) + return self.config diff --git a/mech/mech_snapshot.py b/mech/mech_snapshot.py new file mode 100644 index 0000000..09aa5ea --- /dev/null +++ b/mech/mech_snapshot.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016-2017 Kevin Chung +# Copyright (c) 2018 German Mendez Bravo (Kronuz) +# Copyright (c) 2020 Mike Kinney +# +# 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. +# +"""MechSnapshot class""" + +from __future__ import print_function, absolute_import + +import sys +import logging + +from clint.textui import colored + +from .vmrun import VMrun +from .mech_instance import MechInstance +from .mech_command import MechCommand + +LOGGER = logging.getLogger(__name__) + + +class MechSnapshot(MechCommand): + """ + Usage: mech snapshot [...] + + Available subcommands: + (delete|remove) delete a snapshot taken previously with snapshot save + (list|ls) list all snapshots taken for a machine + save take a snapshot of the current state of the machine + + For help on any individual subcommand run `mech snapshot -h` + """ + + def delete(self, arguments): # pylint: disable=no-self-use + """ + Delete a snapshot taken previously with snapshot save. + + Usage: mech snapshot delete [options] + + Options: + -h, --help Print this help + """ + name = arguments[''] + + instance = arguments[''] + inst = MechInstance(instance) + + vmrun = VMrun(inst.vmx) + if vmrun.delete_snapshot(name) is None: + print(colored.red("Cannot delete name")) + else: + print(colored.green("Snapshot {} deleted".format(name))) + + # add alias for 'mech snapshot remove' + remove = delete + + def list(self, arguments): + """ + List all snapshots taken for a machine. + + Usage: mech snapshot list [options] [] + + Options: + -h, --help Print this help + """ + instance_name = arguments[''] + + if instance_name: + # single instance + instances = [instance_name] + else: + # multiple instances + instances = self.instances() + + for instance in instances: + inst = MechInstance(instance) + print('Snapshots for instance:{}'.format(instance)) + if inst.created: + vmrun = VMrun(inst.vmx) + print(vmrun.list_snapshots()) + else: + print(colored.red('Instance ({}) is not created.'.format(instance))) + + # add alias for 'mech snapshot ls' + ls = list + + def save(self, arguments): # pylint: disable=no-self-use + """ + Take a snapshot of the current state of the machine. + + Usage: mech snapshot save [options] + + Notes: + Take a snapshot of the current state of the machine. + + Snapshots are useful for experimenting in a machine and being able + to rollback quickly. + + Options: + -h, --help Print this help + """ + name = arguments[''] + instance = arguments[''] + + inst = MechInstance(instance) + if inst.created: + vmrun = VMrun(inst.vmx) + if vmrun.snapshot(name) is None: + sys.exit(colored.red("Warning: Could not take snapshot.")) + else: + print(colored.green("Snapshot ({}) on VM ({}) taken".format(name, instance))) + else: + print(colored.red('Instance ({}) is not created.'.format(instance))) diff --git a/mech/test_int_add_and_remove_instances.py b/mech/test_int_add_and_remove_instances.py new file mode 100644 index 0000000..f4804de --- /dev/null +++ b/mech/test_int_add_and_remove_instances.py @@ -0,0 +1,176 @@ +# Copyright (c) 2020 Mike Kinney + +"""Mech integration tests: add and remove instances""" +import re +import subprocess + + +import pytest + + +@pytest.mark.int +def test_int_add_and_remove_instances_using_add_first(helpers): + """Test adding/removing of instances from Mechfile starting off with add.""" + + test_dir = "tests/int/add_and_remove_instances_add_first" + helpers.cleanup_dir_and_vms_from_dir(test_dir) + + command = "mech add apple bento/ubuntu-18.04" + expected_lines = ["Adding", "Added"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + command = "mech list" + expected_lines = ["apple"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + command = "mech add banana bento/ubuntu-18.04" + expected_lines = ["Adding", "Added"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + command = "mech list" + expected_lines = ["apple", "banana"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + command = "mech remove banana" + expected_lines = ["Removed"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + command = "mech list" + expected_lines = ["apple"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # should be able to destroy + command = "mech destroy -f" + expected = "not created" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) + + +@pytest.mark.int +def test_int_add_and_remove_instances_using_init_first(helpers): + """Test adding/removing of instances from Mechfile starting off with init.""" + + test_dir = "tests/int/add_and_remove_instances_init_first" + helpers.cleanup_dir_and_vms_from_dir(test_dir) + + command = "mech init --name apple bento/ubuntu-18.04" + expected_lines = ["Initializing", "has been init"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + command = "mech list" + expected_lines = ["apple"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + command = "mech add banana bento/ubuntu-18.04" + expected_lines = ["Adding", "Added"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + command = "mech list" + expected_lines = ["apple", "banana"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + command = "mech remove banana" + expected_lines = ["Removing", "Removed"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + command = "mech list" + expected_lines = ["apple"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # should be able to destroy + command = "mech destroy -f" + expected = "not created" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) diff --git a/mech/test_int_auth.py b/mech/test_int_auth.py new file mode 100644 index 0000000..9eb8bcd --- /dev/null +++ b/mech/test_int_auth.py @@ -0,0 +1,79 @@ +# Copyright (c) 2020 Mike Kinney + +"""Mech integration tests: auth tests""" +import re +import subprocess + +import pytest + + +@pytest.mark.int +def test_int_auth(helpers): + """Auth testing.""" + + test_dir = "tests/int/auth" + helpers.cleanup_dir_and_vms_from_dir(test_dir) + + # "up" with "add-me" and "use-me" options + command = "mech init -a -u bento/ubuntu-18.04" + expected_lines = [r"init"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # "list" detailed view shows psk + command = "mech ls -d" + expected_lines = [r"id_rsa.pub"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # "up" with 'remove-vagrant' option + command = "mech up -r" + expected_lines = [r"started", "Removing"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # make sure we can run a command using our psk (but do not + # add entry to our known hosts file) + command = """first_ip=`mech ip first`; + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ +-o LogLevel=QUIET ${first_ip} -C uptime""" + expected_lines = [r"load average"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # destroy + command = "mech destroy -f" + expected = "Deleting" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) + + # clean up at the end + helpers.cleanup_dir_and_vms_from_dir(test_dir) diff --git a/mech/test_int_init_from_boxfile.py b/mech/test_int_init_from_boxfile.py new file mode 100644 index 0000000..082ab51 --- /dev/null +++ b/mech/test_int_init_from_boxfile.py @@ -0,0 +1,68 @@ +# Copyright (c) 2020 Mike Kinney + +"""Mech integration tests: init from box file""" +import re +import subprocess + + +import pytest + + +@pytest.mark.int +def test_int_init_from_boxfile(helpers): + """Test mech init from .box file.""" + + test_dir = "tests/int/init_from_boxfile" + helpers.cleanup_dir_and_vms_from_dir(test_dir) + + ubuntu = "ubuntu-18.04" + box_file = "/tmp/{}.box".format(ubuntu) + + # download the file if we don't have it already + # that way we "cache" the file + commands = """ + if ! [ -f "{box_file}" ]; then + wget -O "{box_file}" "https://vagrantcloud.com/bento/\ +boxes/{ubuntu}/versions/201912.04.0/providers/vmware_desktop.box" + fi + """.format(box_file=box_file, ubuntu=ubuntu) + results = subprocess.run(commands, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert results.returncode == 0 + + # init from boxfile + command = "mech init --box bento/{} file:{}".format(ubuntu, box_file) + expected_lines = ["Initializing", "has been init"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # should start + command = "mech up" + expected_lines = ["Extracting", "Added network", + "Bringing machine", "Getting IP", "Sharing folders", + "started", "Provisioning"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # should be able to destroy + command = "mech destroy -f" + expected = "Deleting" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) diff --git a/mech/test_int_init_from_jsonfile.py b/mech/test_int_init_from_jsonfile.py new file mode 100644 index 0000000..a371c1f --- /dev/null +++ b/mech/test_int_init_from_jsonfile.py @@ -0,0 +1,87 @@ +# Copyright (c) 2020 Mike Kinney + +"""Mech integration tests: init from json file""" +import re +import subprocess + + +import pytest + + +@pytest.mark.int +def test_int_init_from_jsonfile(helpers): + """Test mech init from .json file.""" + + test_dir = "tests/int/init_from_jsonfile" + helpers.cleanup_dir_and_vms_from_dir(test_dir) + + ubuntu = "ubuntu-18.04" + + jsonfile_contents = """{ + "description": "Bento Ubuntu box", + "short_description": "ubuntu", + "name": "bento/ubuntu-18.04", + "versions": [ + { + "version": "201912.04.0", + "status": "active", + "description_html": "Some html description", + "description_markdown": "Some markdown description", + "providers": [ + { + "name": "vmware_desktop", + "url": "https://vagrantcloud.com/bento/boxes/ubuntu-18.04/\ +versions/201912.04.0/providers/vmware_desktop.box", + "checksum": null, + "checksum_type": null + } + ] + } + ] +}""" + + jsonfile_name = "bento_1804.json" + jsonfile_path = "{}/{}".format(test_dir, jsonfile_name) + jsonfile_file = open(jsonfile_path, "w") + jsonfile_file.write(jsonfile_contents) + jsonfile_file.close() + + # init from jsonfile + command = "mech init --box bento/{} file:{}".format(ubuntu, jsonfile_name) + expected_lines = ["Initializing", "has been init"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # should start + command = "mech up" + expected_lines = ["could not be found", "vmware_desktop", + "integrity", "Extracting", "Added network", + "Bringing machine", "Getting IP", "Sharing folders", + "started", "Provisioning"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # should be able to destroy + command = "mech destroy -f" + expected = "Deleting" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) + + # clean up file + helpers.cleanup_dir_and_vms_from_dir(test_dir) diff --git a/mech/test_int_multiple_instances.py b/mech/test_int_multiple_instances.py new file mode 100644 index 0000000..8bde298 --- /dev/null +++ b/mech/test_int_multiple_instances.py @@ -0,0 +1,112 @@ +# Copyright (c) 2020 Mike Kinney + +"""Mech integration tests: multiple instances""" +import re +import subprocess + + +import pytest + + +@pytest.mark.int +def test_int_multiple_instances(helpers): + """Test with multiple instances.""" + + test_dir = "tests/int/multiple_instances" + helpers.cleanup_dir_and_vms_from_dir(test_dir) + + mechfile_contents = """{ + "first": { + "box": "bento/ubuntu-18.04", + "box_version": "201912.04.0", + "name": "first", + "url": "https://vagrantcloud.com/bento/boxes/ubuntu-18.04/\ +versions/201912.04.0/providers/vmware_desktop.box" + }, + "second": { + "box": "bento/ubuntu-18.04", + "box_version": "201912.04.0", + "name": "second", + "url": "https://vagrantcloud.com/bento/boxes/ubuntu-18.04/\ +versions/201912.04.0/providers/vmware_desktop.box" + } +} + """ + mechfile_path = test_dir + '/' + 'Mechfile' + mechfile_file = open(mechfile_path, "w") + mechfile_file.write(mechfile_contents) + mechfile_file.close() + + # list two + command = "mech ls" + expected_lines = ["first", "second"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # should start + command = "mech up" + expected_lines = ["could not be found", "vmware_desktop", + "integrity", "Extracting", "Added network", + "Bringing machine", "Getting IP", "Sharing folders", + "started", "Provisioning"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + command = "mech add third bento/ubuntu-18.04" + expected_lines = ["Adding", "Loading", "Added"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # should stop one + command = "mech stop first" + expected_lines = ["Stopped"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # should only have one instance running (second) + command = "mech ls" + expected_lines = [r"first.*poweroff", + r"second.*[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}", + r"third.*notcreated"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # should be able to destroy + command = "mech destroy -f" + expected = "Deleting" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) diff --git a/mech/test_int_provision.py b/mech/test_int_provision.py new file mode 100644 index 0000000..d88c156 --- /dev/null +++ b/mech/test_int_provision.py @@ -0,0 +1,111 @@ +# Copyright (c) 2020 Mike Kinney + +"""Mech integration tests: provisioning tests""" +import re +import subprocess + +import pytest + + +@pytest.mark.int +def test_int_provision(helpers): + """Provision testing.""" + + test_dir = "tests/int/provision/tmp" + helpers.cleanup_dir_and_vms_from_dir(test_dir) + + # copy files from parent dir + command = "cp ../file* .; cp ../Mechfile ." + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stdout == '' + assert stderr == '' + assert results.returncode == 0 + + # up without provisioning + command = "mech up --disable-provisioning" + expected_lines = [r".first.*started", + r".second.*started", + r".third.*started"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # show provisioning + command = "mech provision -s" + expected_lines = [r"first.*Provision 2 entries", + r"second.*Provision 3 entries", + r"Nothing"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # ensure there is no file on first + command = 'mech ssh -c "ls -al /tmp/file1.txt" first' + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert re.search("No such file or directory", stdout) + assert results.returncode == 1 + + # provision + command = "mech provision" + expected_lines = [r"first.*Provision 2 entries", + r"second.*Provision 3 entries", + r"Nothing"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # ensure file exists now + command = 'mech ssh -c "ls -al /tmp/file1.txt" first' + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert re.search("/tmp/file1.txt", stdout) + assert results.returncode == 0 + + # ensure provisioning runs during "up" + command = "mech up" + expected_lines = [r"first.*Provision 2 entries", + r"second.*Provision 3 entries", + r"Nothing"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # destroy + command = "mech destroy -f" + expected = "Deleting" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) + + # clean up at the end + helpers.cleanup_dir_and_vms_from_dir(test_dir) diff --git a/mech/test_int_provision_using_psk.py b/mech/test_int_provision_using_psk.py new file mode 100644 index 0000000..eed0fd9 --- /dev/null +++ b/mech/test_int_provision_using_psk.py @@ -0,0 +1,65 @@ +# Copyright (c) 2020 Mike Kinney + +"""Mech integration tests: provisioning tests using psk""" +import re +import subprocess + +import pytest + + +@pytest.mark.int +def test_int_provision_using_psk(helpers): + """Provision testing using psk.""" + + test_dir = "tests/int/provision_using_psk/tmp" + helpers.cleanup_dir_and_vms_from_dir(test_dir) + + # copy files from parent dir + command = "cp ../file* .; cp ../Mechfile ." + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stdout == '' + assert stderr == '' + assert results.returncode == 0 + + # up (and remove vagrant user) + command = "mech up -r" + expected_lines = [r".first.*started", + r".second.*started", + r".third.*started", + r"Added auth", + r"first.*Provision 2 entries", + r"second.*Provision 3 entries", + r"Nothing", + r"Removing username"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # ensure file exists + command = 'mech ssh -c "ls -al /tmp/file1.txt" first' + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert re.search("/tmp/file1.txt", stdout) + assert results.returncode == 0 + + # destroy + command = "mech destroy -f" + expected = "Deleting" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) + + # clean up at the end + helpers.cleanup_dir_and_vms_from_dir(test_dir) diff --git a/mech/test_int_shared_folders.py b/mech/test_int_shared_folders.py new file mode 100644 index 0000000..73f2967 --- /dev/null +++ b/mech/test_int_shared_folders.py @@ -0,0 +1,142 @@ +# Copyright (c) 2020 Mike Kinney + +"""Mech integration tests: shared folders tests""" +import re +import subprocess + +import pytest + + +@pytest.mark.int +def test_int_shared_folders(helpers): + """Shared folders testing.""" + + test_dir = "tests/int/shared_folders/tmp" + helpers.cleanup_dir_and_vms_from_dir(test_dir) + + # copy file from parent dir + command = "cp ../Mechfile ." + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stdout == '' + assert stderr == '' + assert results.returncode == 0 + + # up + command = "mech up" + expected_lines = [r"started"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # ensure the Mechfile is present from the guest + command = 'mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first' + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert re.search("Mechfile", stdout) + assert results.returncode == 0 + + # create a simple file to see if visible on another share + command = 'date > /tmp/now' + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stdout == '' + assert stderr == '' + assert results.returncode == 0 + + # ensure we can see the file on the other share + command = 'mech ssh -c "ls -al /mnt/hgfs/mech2/now" first' + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert re.search("/mnt/hgfs/mech2/now", stdout) + assert results.returncode == 0 + + # stop + command = "mech stop" + expected_lines = [r"Stopped"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # "up" but do not have shared folders + command = "mech up --disable-shared-folders" + expected_lines = [r"started"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # ensure the Mechfile is *NOT* present from the guest + command = 'mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first' + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert re.search("No such file or directory", stdout) + assert results.returncode == 2 + + # pause + command = "mech pause" + expected_lines = [r"Paused"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # "resume" but do not have shared folders + command = "mech resume --disable-shared-folders" + expected_lines = ["resumed"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # ensure the Mechfile is *NOT* present from the guest + command = 'mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first' + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert re.search("No such file or directory", stdout) + assert results.returncode == 2 + + # destroy + command = "mech destroy -f" + expected = "Deleting" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) + + # clean up at the end + helpers.cleanup_dir_and_vms_from_dir(test_dir) diff --git a/mech/test_int_simple.py b/mech/test_int_simple.py new file mode 100644 index 0000000..f974a54 --- /dev/null +++ b/mech/test_int_simple.py @@ -0,0 +1,371 @@ +# Copyright (c) 2020 Mike Kinney + +"""Mech integration tests: simple ones (including smoke)""" +import re +import subprocess + +import pytest + + +@pytest.mark.int +def test_int_no_args(): + """Test without any args""" + return_value, out = subprocess.getstatusoutput('mech') + assert re.match(r'Usage: mech ', out) + assert return_value == 1 + + +@pytest.mark.int +def test_int_version(): + """Test '--version'.""" + return_value, out = subprocess.getstatusoutput('mech --version') + assert re.match(r'mech v[0-9]+\.[0-9]+\.[0-9]', out) + assert return_value == 0 + + +@pytest.mark.int +def test_int_help(): + """Test '--help'.""" + return_value, out = subprocess.getstatusoutput('mech --help') + assert re.match(r'Usage: mech ', out) + assert return_value == 0 + + +@pytest.mark.int +def test_int_no_mechfile(helpers): + """Test when no Mechfile.""" + test_dir = "tests/int/no_mechfile" + helpers.cleanup_dir_and_vms_from_dir(test_dir) + command = "mech ls" + return_value, out = subprocess.getstatusoutput(command) + assert re.search(r'Could not find a Mechfile', out) + assert return_value == 1 + + +@pytest.mark.int +def test_int_smoke(helpers): + """Smoke test most options.""" + + test_dir = "tests/int/simple" + helpers.cleanup_dir_and_vms_from_dir(test_dir) + + # ensure we need to provide more args + commands = ["mech box", "mech init", "mech ip", "mech ps", "mech scp", "mech snapshot", + "mech snapshot save", "mech snapshot save snap1", "mech snapshot delete", + "mech snapshot remove", "mech ssh"] + expected = "Usage: mech " + for command in commands: + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stdout == '' + assert results.returncode == 1 + assert re.search(expected, stderr) + + # should init + command = "mech init mrlesmithjr/alpine311" + expected_lines = ["Initializing", "Loading metadata", "has been initialized", "mech up"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # should start + command = "mech up" + expected_lines = ["could not be found", "vmware_desktop", + "integrity", "Extracting", "Added network", + "Bringing machine", "Getting IP", "Sharing folders", + "started", "Provisioning"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # ensure we get proper response from bad instance name + commands = ["mech status first2", "mech ip first2"] + expected = "was not found in the Mechfile" + for command in commands: + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stdout == '' + assert results.returncode == 1 + assert re.search(expected, stderr) + + # should be able to re-up, verify 'start' alias works, too + commands = ["mech up", "mech start"] + expected_lines = ["Bringing machine", "Getting IP", "Sharing folders", + "was already started", "Provisioning"] + for command in commands: + results = subprocess.run(commands, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech ps' + command = "mech ps first" + expected_lines = ["/sbin/init"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech status' + command = "mech status" + expected_lines = ["first", "Tools running"] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech global-status' + command = "mech global-status" + expected_lines = [test_dir + '/.mech/first/'] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech list' + commands = ["mech ls", "mech list"] + expected_lines = ['first', 'alpine'] + for command in commands: + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech stop' + command = "mech stop" + expected_lines = ['Stopped'] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech stop' again + command = "mech stop" + expected_lines = ['Not stopped'] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert re.search('The virtual machine is not powered on', stderr, re.MULTILINE) + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech start' + command = "mech start" + expected_lines = ['started'] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech pause' + command = "mech pause" + expected_lines = ['Paused'] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech resume' + command = "mech resume" + expected_lines = ['resumed'] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech suspend' + command = "mech suspend" + expected_lines = ['Suspended'] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech resume' after suspend + command = "mech resume" + expected_lines = ['started'] + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech ssh' (different forms) + commands = ["mech ssh -c 'uptime' first", "mech ssh --command 'uptime' first"] + expected_lines = ['load average'] + for command in commands: + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + for line in expected_lines: + print(line) + assert re.search(line, stdout, re.MULTILINE) + + # test 'mech scp' to guest + command = "date > now; mech scp now first:/tmp" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stdout == '' + assert stderr == '' + assert results.returncode == 0 + + # test 'mech scp' from guest + command = "mech scp first:/tmp/now ." + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stdout == '' + assert stderr == '' + assert results.returncode == 0 + + # test 'mech ip first' + command = "mech ip first" + expected = r"[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) + + # test "mech port" + command = "mech port" + expected = "Total port forwardings: 0" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout, re.MULTILINE) + + # test "mech box list" (and alias) + commands = ["mech box list", "mech box ls"] + expected = r"alpine" + for command in commands: + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout, re.MULTILINE) + + # test "mech snapshot list" (and alias) + commands = ["mech snapshot list", "mech snapshot ls"] + expected = "Total snapshots: 0" + for command in commands: + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout, re.MULTILINE) + + # test "mech snapshot save" + command = "mech snapshot save snap1 first" + expected = "taken" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) + + # test "mech snapshot save" with same args again + command = "mech snapshot save snap1 first" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stdout == '' + assert re.search('A snapshot with the name already exists', stderr) + assert results.returncode == 1 + + # test "mech snapshot list" (and alias) again (now that we have one) + commands = ["mech snapshot list", "mech snapshot ls"] + expected = "Total snapshots: 1" + for command in commands: + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout, re.MULTILINE) + + # test "mech snapshot delete" + command = "mech snapshot delete snap1 first" + expected = "deleted" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) + + # should be able to destroy + command = "mech destroy -f" + expected = "Deleting" + results = subprocess.run(command, cwd=test_dir, shell=True, capture_output=True) + stdout = results.stdout.decode('utf-8') + stderr = results.stderr.decode('utf-8') + assert stderr == '' + assert results.returncode == 0 + assert re.search(expected, stdout) diff --git a/mech/test_mech.py b/mech/test_mech.py new file mode 100644 index 0000000..00d3c57 --- /dev/null +++ b/mech/test_mech.py @@ -0,0 +1,1645 @@ +# Copyright (c) 2020 Mike Kinney + +"""mech tests""" +import os +import re + +from unittest.mock import patch, mock_open +from pytest import raises + +import mech.command +import mech.mech +import mech.vmrun + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_list_with_one(mock_locate, mock_load_mechfile, capfd, + mechfile_one_entry): + """Test 'mech list' with one entry.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + list_arguments = {'--detail': False} + a_mech.list(list_arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'first\s+notcreated', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_list_with_one_and_debug(mock_locate, mock_load_mechfile, capfd, + mechfile_one_entry): + """Test 'mech list' with one entry.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': True} + a_mech = mech.mech.Mech(arguments=global_arguments) + list_arguments = {'--detail': True} + a_mech.list(list_arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'created:False', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_list_with_two_not_created(mock_locate, mock_load_mechfile, capfd, + mechfile_two_entries): + """Test 'mech list' with two entries.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + list_arguments = {'--detail': False} + a_mech.list(list_arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'first\s+notcreated', out, re.MULTILINE) + assert re.search(r'second\s+notcreated', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_list_powered_on(mock_locate, mock_load_mechfile, + mock_get_ip, capfd, + mechfile_two_entries): + """Test 'mech list' powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = {'': 'first', '--detail': None} + a_mech.list(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_get_ip.assert_called() + assert re.search(r'192.168.', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value=None) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_list_powered_off(mock_locate, mock_load_mechfile, + mock_get_ip, capfd, + mechfile_two_entries): + """Test 'mech list' powered off.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = {'': 'first', '--detail': None} + a_mech.list(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_get_ip.assert_called() + assert re.search(r'poweroff', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_status_with_two_not_created(mock_locate, mock_load_mechfile, capfd, + mechfile_two_entries): + """Test 'mech status' with two entries, neither created.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = {'': None} + a_mech.status(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'first.*has not been created', out, re.MULTILINE) + assert re.search(r'second.*has not been created', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.check_tools_state', return_value="running") +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_status_powered_on(mock_locate, mock_load_mechfile, + mock_get_ip, mock_check_tools_state, capfd, + mechfile_two_entries): + """Test 'mech status' powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = {'': 'first'} + a_mech.status(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_get_ip.assert_called() + mock_check_tools_state.assert_called() + assert re.search(r'VM is ready', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.check_tools_state', return_value="running") +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value=None) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_status_powered_off(mock_locate, mock_load_mechfile, + mock_get_ip, mock_check_tools_state, + capfd, mechfile_two_entries): + """Test 'mech status' powered off.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = {'': 'first'} + a_mech.status(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_get_ip.assert_called() + mock_check_tools_state.assert_called() + assert re.search(r'VM is powered off', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.check_tools_state', return_value=False) +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value=False) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_status_could_not_get_ip(mock_locate, mock_load_mechfile, + mock_get_ip, mock_check_tools_state, capfd, + mechfile_two_entries): + """Test 'mech status' powered off.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = {'': 'first'} + a_mech.status(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_get_ip.assert_called() + mock_check_tools_state.assert_called() + assert re.search(r'VM is on.*no IP to connect', out, re.MULTILINE) + + +@patch('mech.utils.get_provider', return_value=None) +@patch('os.path.exists', return_value=True) +@patch('shutil.rmtree') +@patch('mech.vmrun.VMrun.delete_vm') +@patch('mech.vmrun.VMrun.stop', return_value=True) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_destroy(mock_locate, mock_load_mechfile, + mock_vmrun_stop, mock_vmrun_delete_vm, + mock_rmtree, mock_path_exists, mock_get_provider, + capfd, mechfile_two_entries): + """Test 'mech destroy' powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries + mock_rmtree.return_value = True + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--force': True, + } + a_mech.destroy(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_stop.assert_called() + mock_get_provider.assert_called() + mock_vmrun_delete_vm.assert_called() + mock_rmtree.assert_called() + mock_path_exists.assert_called() + assert re.search(r'Deleting', out, re.MULTILINE) + assert re.search(r'Deleted', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_destroy_not_created(mock_locate, mock_load_mechfile, + capfd, mechfile_two_entries): + """Test 'mech destroy' not created.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + '--force': True, + } + a_mech.destroy(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'not created', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.installed_tools', return_value='running') +@patch('mech.vmrun.VMrun.stop', return_value=True) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_down(mock_locate, mock_load_mechfile, + mock_vmrun_stop, mock_installed_tools, + capfd, mechfile_two_entries): + """Test 'mech down' powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--force': None, + } + a_mech.down(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_stop.assert_called() + mock_installed_tools.assert_called() + assert re.search(r'Stopped', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.installed_tools', return_value=False) +@patch('mech.vmrun.VMrun.stop', return_value=None) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_down_no_vmware_tools_and_stopped_fails(mock_locate, mock_load_mechfile, + mock_vmrun_stop, mock_installed_tools, + capfd, mechfile_two_entries): + """Test 'mech down' powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--force': None, + } + a_mech.down(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_stop.assert_called() + mock_installed_tools.assert_called() + assert re.search(r'Not stopped', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_down_not_created(mock_locate, mock_load_mechfile, + capfd, mechfile_two_entries): + """Test 'mech down' not created.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + '--force': None, + } + a_mech.down(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r' not created', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.145") +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_ip(mock_locate, mock_load_mechfile, + mock_get_ip, capfd, mechfile_two_entries): + """Test 'mech ip' powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + } + a_mech.ip(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_get_ip.assert_called() + assert re.search(r'192.168', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value=None) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_ip_unknown(mock_locate, mock_load_mechfile, + mock_get_ip, capfd, mechfile_two_entries): + """Test 'mech ip' but cannot get ip address.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + } + a_mech.ip(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_get_ip.assert_called() + assert re.search(r'Unknown', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_ip_not_created(mock_locate, mock_load_mechfile, + capfd, mechfile_two_entries): + """Test 'mech ip' not created.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + } + a_mech.ip(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'VM not created', out, re.MULTILINE) + + +MECHFILE_WITH_PROVISIONING = { + "first": { + "box": "mrlesmithjr/alpine311", + "box_version": "1578437753", + "name": "first", + "url": "https://vagrantcloud.com/mrlesmithjr/boxes/alpine311/\ +versions/1578437753/providers/vmware_desktop.box", + "provision": [ + { + "type": "file", + "source": "file1.txt", + "destination": "/tmp/file1.txt" + }, + { + "type": "file", + "source": "file2.txt", + "destination": "/tmp/file2.txt" + } + ] + }, + "second": { + "box": "mrlesmithjr/alpine311", + "box_version": "1578437753", + "name": "second", + "url": "https://vagrantcloud.com/mrlesmithjr/boxes/alpine311/\ +versions/1578437753/providers/vmware_desktop.box", + "provision": [ + { + "type": "shell", + "path": "file1.sh", + "args": [ + "a=1", + "b=true" + ] + }, + { + "type": "shell", + "path": "file2.sh", + "args": [] + }, + { + "type": "shell", + "inline": "echo hello from inline" + } + ] + }, + "third": { + "box": "mrlesmithjr/alpine311", + "box_version": "1578437753", + "name": "third", + "url": "https://vagrantcloud.com/mrlesmithjr/boxes/alpine311/\ + versions/1578437753/providers/vmware_desktop.box", + "provision": [] + } +} +@patch('mech.utils.provision_file', return_value=True) +@patch('mech.vmrun.VMrun.installed_tools', return_value='running') +@patch('mech.utils.load_mechfile', return_value=MECHFILE_WITH_PROVISIONING) +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_provision_file(mock_locate, mock_load_mechfile, + mock_installed_tools, mock_provision_file, capfd): + """Test 'mech provision' (using file provisioning).""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--show-only': None, + } + a_mech.provision(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_installed_tools.assert_called() + mock_provision_file.assert_called() + assert re.search(r' Provision ', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile', return_value=MECHFILE_WITH_PROVISIONING) +@patch('mech.utils.locate', return_value=None) +def test_mech_provision_not_started(mock_locate, mock_load_mechfile, capfd): + """Test 'mech provision' (using file provisioning).""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + '--show-only': None, + } + a_mech.provision(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'VM not created', out, re.MULTILINE) + + +@patch('mech.utils.provision_shell', return_value=True) +@patch('mech.vmrun.VMrun.installed_tools', return_value='running') +@patch('mech.utils.load_mechfile', return_value=MECHFILE_WITH_PROVISIONING) +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_provision_shell(mock_locate, mock_load_mechfile, + mock_installed_tools, mock_provision_shell, capfd): + """Test 'mech provision' (using shell provisioning).""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'second', + '--show-only': None, + } + a_mech.provision(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_installed_tools.assert_called() + mock_provision_shell.assert_called() + assert re.search(r' Provision ', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.suspend', return_value=True) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_suspend(mock_locate, mock_load_mechfile, + mock_vmrun_suspend, capfd, mechfile_two_entries): + """Test 'mech suspend' powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + } + a_mech.suspend(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_suspend.assert_called() + assert re.search(r'Suspended', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.suspend', return_value=None) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_suspend_fails(mock_locate, mock_load_mechfile, + mock_vmrun_suspend, capfd, mechfile_two_entries): + """Test 'mech suspend' powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + } + a_mech.suspend(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_suspend.assert_called() + assert re.search(r'Not suspended', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_suspend_not_created(mock_locate, mock_load_mechfile, + capfd, mechfile_two_entries): + """Test 'mech suspend' not created.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + } + a_mech.suspend(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'VM has not been created', out, re.MULTILINE) + + +@patch('os.chmod', return_value=True) +@patch('mech.vmrun.VMrun.installed_tools', return_value='running') +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.4.130") +@patch('subprocess.run') +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_ssh(mock_locate, mock_load_mechfile, + mock_subprocess_run, mock_get_ip, mock_installed_tools, + mock_chmod, mechfile_two_entries): + """Test 'mech ssh'""" + mock_load_mechfile.return_value = mechfile_two_entries + mock_subprocess_run.return_value.returncode = 0 + mock_subprocess_run.stdout = b'00:03:30 up 2 min, load average: 0.00, 0.00, 0.00\n' + mock_subprocess_run.stderr = b'' + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--plain': None, + '--command': 'uptime', + '': 'blah', + } + filename = os.path.join(mech.utils.mech_dir(), 'insecure_private_key') + a_mock = mock_open() + with raises(SystemExit): + with patch('builtins.open', a_mock, create=True): + a_mech.ssh(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_subprocess_run.assert_called() + mock_installed_tools.assert_called() + mock_get_ip.assert_called() + mock_chmod.assert_called() + a_mock.assert_called_once_with(filename, 'w') + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_ssh_not_created(mock_locate, mock_load_mechfile, + mechfile_two_entries, capfd): + """Test 'mech ssh'""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--plain': None, + '--command': 'uptime', + '': None, + } + a_mech.ssh(arguments) + # Note: Could not figure out how to capture output from subprocess.call. + mock_locate.assert_called() + mock_load_mechfile.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'VM not created', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.pause', return_value=True) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_pause(mock_locate, mock_load_mechfile, + mock_vmrun_pause, capfd, mechfile_two_entries): + """Test 'mech pause' powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--force': None, + } + a_mech.pause(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_pause.assert_called() + assert re.search(r'Paused', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_pause_not_created(mock_locate, mock_load_mechfile, + capfd, mechfile_two_entries): + """Test 'mech pause' not created.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + '--force': None, + } + a_mech.pause(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r' not created', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.upgradevm', return_value=None) +@patch('mech.vmrun.VMrun.check_tools_state', return_value=False) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_upgrade_created_powered_off_upgrade_fails(mock_locate, mock_load_mechfile, + mock_check_tools_state, + mock_vmrun_upgradevm, + capfd, mechfile_two_entries): + """Test 'mech upgrade' with vm created and powered off.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--force': None, + } + a_mech.upgrade(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_check_tools_state.assert_called() + mock_vmrun_upgradevm.assert_called() + assert re.search(r'Not upgraded', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.upgradevm', return_value='') +@patch('mech.vmrun.VMrun.check_tools_state', return_value=False) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_upgrade_created_powered_off_upgrade_works(mock_locate, mock_load_mechfile, + mock_check_tools_state, + mock_vmrun_upgradevm, + capfd, mechfile_two_entries): + """Test 'mech upgrade' with vm created and powered off.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--force': None, + } + a_mech.upgrade(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_check_tools_state.assert_called() + mock_vmrun_upgradevm.assert_called() + assert re.search(r'Upgraded', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.check_tools_state', return_value="running") +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_upgrade_created__powered_on(mock_locate, mock_load_mechfile, + mock_check_tools_state, + capfd, mechfile_two_entries): + """Test 'mech upgrade' with vm created and powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--force': None, + } + a_mech.upgrade(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_check_tools_state.assert_called() + assert re.search(r'VM must be stopped', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_upgrade_not_created(mock_locate, mock_load_mechfile, + capfd, mechfile_two_entries): + """Test 'mech upgrade' not created.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + '--force': None, + } + a_mech.upgrade(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r' not created', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value='192.168.1.101') +@patch('mech.vmrun.VMrun.reset', return_value=True) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_reload(mock_locate, mock_load_mechfile, + mock_vmrun_reset, mock_get_ip, + capfd, mechfile_two_entries): + """Test 'mech reload' powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + } + a_mech.reload(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_reset.assert_called() + mock_get_ip.assert_called() + assert re.search(r'started', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_reload_not_created(mock_locate, mock_load_mechfile, + capfd, mechfile_two_entries): + """Test 'mech reload' not created.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + } + a_mech.reload(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'VM not created', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.disable_shared_folders', return_value=True) +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value='192.168.1.101') +@patch('mech.vmrun.VMrun.unpause', return_value=True) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_resume(mock_locate, mock_load_mechfile, + mock_vmrun_unpause, mock_vmrun_get_ip, + mock_vmrun_disable_shared_folders, capfd, mechfile_two_entries): + """Test 'mech resume'.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--disable-shared-folders': True, + '--force': True, + } + a_mech.resume(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_unpause.assert_called() + mock_vmrun_disable_shared_folders.assert_called() + mock_vmrun_get_ip.assert_called() + assert re.search(r'resumed', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_resume_not_created(mock_locate, mock_load_mechfile, + capfd, mechfile_two_entries): + """Test 'mech resume'.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--disable-shared-folders': True, + '--force': True, + } + a_mech.resume(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'VM not created', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.start', return_value=None) +@patch('mech.vmrun.VMrun.unpause', return_value=None) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_resume_unpause_unsuccessful_start_fails(mock_locate, mock_load_mechfile, + mock_vmrun_unpause, + mock_vmrun_start, + capfd, mechfile_two_entries): + """Test 'mech resume'.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--disable-shared-folders': True, + '--force': True, + } + a_mech.resume(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_unpause.assert_called() + mock_vmrun_start.assert_called() + assert re.search(r'VM not started', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.add_shared_folder', return_value=True) +@patch('mech.vmrun.VMrun.enable_shared_folders', return_value=True) +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value='192.168.1.101') +@patch('mech.vmrun.VMrun.unpause', return_value=True) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_resume_shared_folders(mock_locate, mock_load_mechfile, + mock_vmrun_unpause, mock_vmrun_get_ip, + mock_enable_shared_folders, mock_add_shared_folder, + capfd, mechfile_two_entries): + """Test 'mech resume'.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + '--disable-shared-folders': False, + '--force': True, + } + a_mech.resume(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_unpause.assert_called() + mock_enable_shared_folders.assert_called() + mock_add_shared_folder.assert_called() + mock_vmrun_get_ip.assert_called() + assert re.search(r'Sharing folders', out, re.MULTILINE) + assert re.search(r'resumed', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.add_shared_folder', return_value=True) +@patch('mech.vmrun.VMrun.enable_shared_folders', return_value=True) +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value='192.168.1.101') +@patch('mech.vmrun.VMrun.start', return_value=True) +@patch('mech.vmrun.VMrun.unpause', return_value=None) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_resume_unpause_fails_starts_successfully_with_shared_folders( + mock_locate, mock_load_mechfile, mock_vmrun_unpause, mock_vmrun_start, + mock_vmrun_get_ip, mock_enable_shared_folders, mock_add_shared_folder, + capfd, mechfile_two_entries): + + """Test 'mech resume'.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + '--disable-shared-folders': False, + '--force': True, + } + a_mech.resume(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_unpause.assert_called() + mock_vmrun_start.assert_called() + mock_enable_shared_folders.assert_called() + mock_add_shared_folder.assert_called() + mock_vmrun_get_ip.assert_called() + assert re.search(r'Sharing folders', out, re.MULTILINE) + assert re.search(r'started', out, re.MULTILINE) + + +MECHFILE_BAD_ENTRY = { + '': { + 'name': + '', + 'box': + 'bento/ubuntu-18.04', + 'box_version': + '201912.04.0' + } +} +@patch('mech.utils.load_mechfile') +def test_mech_up_without_name(mock_load_mechfile): + """Test 'mech up' (overriding name to be '') to test exception.""" + mock_load_mechfile.return_value = MECHFILE_BAD_ENTRY + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--detail': False, + '--gui': False, + '--disable-shared-folders': False, + '--disable-provisioning': False, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '--no-nat': None, + '--remove-vagrant': None, + '': '', + } + with raises(AttributeError, match=r"Must provide a name for the instance."): + a_mech.up(arguments) + + +@patch('mech.utils.load_mechfile') +def test_mech_up_with_name_not_in_mechfile(mock_load_mechfile, + mechfile_one_entry): + """Test 'mech up' with a name that is not in the Mechfile.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--detail': False, + '--gui': False, + '--disable-shared-folders': False, + '--disable-provisioning': False, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '--no-nat': None, + '--remove-vagrant': None, + '': 'notfirst', + } + with raises(SystemExit, match=r" was not found in the Mechfile"): + a_mech.up(arguments) + + +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") +@patch('mech.vmrun.VMrun.start', return_value='') +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/one.vmx') +def test_mech_up_already_started(mock_locate, mock_load_mechfile, + mock_vmrun_start, mock_vmrun_get_ip, capfd, + mechfile_one_entry): + """Test 'mech up'.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--gui': False, + '--disable-shared-folders': True, + '--disable-provisioning': True, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '--no-nat': None, + '--remove-vagrant': None, + '': None, + } + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_start.assert_called() + mock_vmrun_get_ip.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'was already started on', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.run_script_in_guest', return_value='') +@patch('mech.vmrun.VMrun.installed_tools', return_value='running') +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") +@patch('mech.vmrun.VMrun.start', return_value='') +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/one.vmx') +def test_mech_up_already_started_with_add_me(mock_locate, mock_load_mechfile, + mock_vmrun_start, mock_vmrun_get_ip, + mock_installed_tools, + mock_run_script_in_guest, capfd, + mechfile_one_entry_with_auth): + """Test 'mech up'.""" + mock_load_mechfile.return_value = mechfile_one_entry_with_auth + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--gui': False, + '--disable-shared-folders': True, + '--disable-provisioning': True, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '--no-nat': None, + '--remove-vagrant': None, + '': None, + } + mock_file = mock_open(read_data='some_pub_key_data') + with patch('builtins.open', mock_file, create=True): + a_mech.up(arguments) + mock_file.assert_called() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_start.assert_called() + mock_installed_tools.assert_called() + mock_run_script_in_guest.assert_called() + mock_vmrun_get_ip.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'was already started on', out, re.MULTILINE) + assert re.search(r'Added auth', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value='') +@patch('mech.vmrun.VMrun.start', return_value='') +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/one.vmx') +def test_mech_up_already_started_but_could_not_get_ip(mock_locate, mock_load_mechfile, + mock_vmrun_start, mock_vmrun_get_ip, + capfd, mechfile_one_entry): + """Test 'mech up'.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--gui': False, + '--disable-shared-folders': True, + '--disable-provisioning': True, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '--no-nat': None, + '--remove-vagrant': None, + '': None, + } + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_start.assert_called() + mock_vmrun_get_ip.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'started on an unknown', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value=False) +@patch('mech.vmrun.VMrun.start', return_value=True) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/one.vmx') +def test_mech_up_already_started_but_on_unknnown_ip(mock_locate, mock_load_mechfile, + mock_vmrun_start, mock_vmrun_get_ip, capfd, + mechfile_one_entry): + """Test 'mech up'.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--gui': False, + '--disable-shared-folders': True, + '--disable-provisioning': True, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '--no-nat': None, + '--remove-vagrant': None, + '': None, + } + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_start.assert_called() + mock_vmrun_get_ip.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'started on an unknown', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.start', return_value=None) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/one.vmx') +def test_mech_up_problem(mock_locate, mock_load_mechfile, + mock_vmrun_start, capfd, + mechfile_one_entry): + """Test 'mech up' when issue with starting VM""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--gui': False, + '--disable-shared-folders': True, + '--disable-provisioning': True, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '--no-nat': None, + '--remove-vagrant': None, + '': None, + } + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_start.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'not started', out, re.MULTILINE) + + +@patch('mech.utils.provision') +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") +@patch('mech.vmrun.VMrun.start', return_value=True) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/one.vmx') +def test_mech_up_with_provisioning(mock_locate, mock_load_mechfile, + mock_vmrun_start, mock_vmrun_get_ip, + mock_provision, capfd, mechfile_one_entry): + """Test 'mech up'.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--gui': False, + '--disable-shared-folders': True, + '--disable-provisioning': False, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '--no-nat': None, + '--remove-vagrant': None, + '': None, + } + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_start.assert_called() + mock_vmrun_get_ip.assert_called() + mock_provision.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'started', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.enable_shared_folders') +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") +@patch('mech.vmrun.VMrun.start', return_value=True) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/one.vmx') +def test_mech_up_wth_shared_folders(mock_locate, mock_load_mechfile, + mock_vmrun_start, mock_vmrun_get_ip, + mock_vmrun_enable_shared_folders, + capfd, mechfile_one_entry): + """Test 'mech up'.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--gui': False, + '--disable-shared-folders': False, + '--disable-provisioning': True, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '--no-nat': None, + '--remove-vagrant': None, + '': None, + } + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_vmrun_start.assert_called() + mock_vmrun_get_ip.assert_called() + mock_vmrun_enable_shared_folders.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'started', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_ssh_config_not_created(mock_locate, mock_load_mechfile, capfd, + mechfile_one_entry): + """Test 'mech ssh-config' when vm is not created.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + } + a_mech.ssh_config(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'not created', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.check_tools_state', return_value=True) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate') +@patch('os.getcwd') +def test_mech_ssh_config_not_started(mock_getcwd, mock_locate, mock_load_mechfile, + mock_check_tools_state, mechfile_one_entry): + """Test 'mech ssh-config' when vm is created but not started.""" + mock_locate.return_value = '/tmp/first/some.vmx' + mock_load_mechfile.return_value = mechfile_one_entry + mock_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + } + with raises(SystemExit, match=r".*not yet ready for SSH.*"): + a_mech.ssh_config(arguments) + + +@patch('os.chmod') +@patch('mech.vmrun.VMrun.installed_tools', return_value='running') +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value='192.168.2.120') +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +@patch('os.getcwd') +def test_mech_ssh_config(mock_getcwd, mock_locate, # pylint: disable=too-many-arguments + mock_load_mechfile, mock_get_guest_ip_address, + mock_installed_tools, mock_chmod, capfd, mechfile_one_entry): + """Test 'mech ssh-config'.""" + mock_load_mechfile.return_value = mechfile_one_entry + mock_getcwd.return_value = '/tmp' + mock_chmod.return_value = 0 + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + } + mock_file = mock_open() + with patch('builtins.open', mock_file, create=True): + a_mech.ssh_config(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_get_guest_ip_address.assert_called() + mock_installed_tools.assert_called() + mock_file.assert_called() + mock_chmod.assert_called() + assert re.search(r'Host first', out, re.MULTILINE) + assert re.search(r' User vagrant', out, re.MULTILINE) + assert re.search(r' Port 22', out, re.MULTILINE) + + +HOST_NETWORKS = """Total host networks: 3 +INDEX NAME TYPE DHCP SUBNET MASK +0 vmnet0 bridged false empty empty +1 vmnet1 hostOnly true 172.16.11.0 255.255.255.0 +8 vmnet8 nat true 192.168.3.0 255.255.255.0""" +@patch('mech.vmrun.VMrun.list_port_forwardings', return_value='Total port forwardings: 0') +@patch('mech.vmrun.VMrun.list_host_networks', return_value=HOST_NETWORKS) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_port_with_nat(mock_locate, mock_load_mechfile, mock_list_host_networks, + mock_list_port_forwardings, capfd, + mechfile_one_entry): + """Test 'mech port' with nat networking.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + port_arguments = {} + port_arguments = {'': None} + a_mech.port(port_arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_list_host_networks.assert_called() + mock_list_port_forwardings.assert_called() + assert re.search(r'Total port forwardings: 0', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.list_port_forwardings', return_value='Total port forwardings: 0') +@patch('mech.vmrun.VMrun.list_host_networks', return_value=HOST_NETWORKS) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_port_with_nat_and_instance(mock_locate, mock_load_mechfile, mock_list_host_networks, + mock_list_port_forwardings, capfd, mechfile_one_entry): + """Test 'mech port first' with nat networking.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + port_arguments = {} + port_arguments = {'': 'first'} + a_mech.port(port_arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_list_host_networks.assert_called() + mock_list_port_forwardings.assert_called() + assert re.search(r'Total port forwardings: 0', out, re.MULTILINE) + + +HOST_NETWORKS = """Total host networks: 3 +INDEX NAME TYPE DHCP SUBNET MASK +0 vmnet0 bridged false empty empty +1 vmnet1 hostOnly true 172.16.11.0 255.255.255.0 +8 vmnet8 nat true 192.168.3.0 255.255.255.0""" +@patch('mech.vmrun.VMrun.list_port_forwardings', return_value='Total port forwardings: 0') +@patch('mech.vmrun.VMrun.list_host_networks', return_value=HOST_NETWORKS) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_port_with_nat_two_hosts(mock_locate, mock_load_mechfile, mock_list_host_networks, + mock_list_port_forwardings, capfd, mechfile_two_entries): + """Test 'mech port' with nat networking and two instances.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + port_arguments = {} + port_arguments = {'': None} + a_mech.port(port_arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_list_host_networks.assert_called() + mock_list_port_forwardings.assert_called() + assert re.search(r'Total port forwardings: 0', out, re.MULTILINE) + + +HOST_NETWORKS_WITHOUT_NAT = """Total host networks: 2 +INDEX NAME TYPE DHCP SUBNET MASK +0 vmnet0 bridged false empty empty +1 vmnet1 hostOnly true 172.16.11.0 255.255.255.0""" +@patch('mech.vmrun.VMrun.list_host_networks', return_value=HOST_NETWORKS_WITHOUT_NAT) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_port_without_nat(mock_locate, mock_load_mechfile, mock_list_host_networks, + capfd, mechfile_one_entry): + """Test 'mech port' without nat.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + port_arguments = {} + port_arguments = {'': None} + a_mech.port(port_arguments) + _, err = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_list_host_networks.assert_called() + assert re.search(r'Cannot find a nat network', err, re.MULTILINE) + + +@patch('requests.get') +@patch('os.path.exists') +@patch('os.getcwd') +def test_mech_init(mock_os_getcwd, mock_os_path_exists, + mock_requests_get, capfd, catalog_as_json, + mech_init_arguments): + """Test 'mech init' from Hashicorp'.""" + mock_os_getcwd.return_value = '/tmp' + mock_os_path_exists.return_value = False + global_arguments = {'--debug': False} + mock_requests_get.return_value.status_code = 200 + mock_requests_get.return_value.json.return_value = catalog_as_json + + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = mech_init_arguments + arguments[''] = 'bento/ubuntu-18.04' + arguments['-add-me'] = None + a_mech.init(arguments) + out, _ = capfd.readouterr() + assert re.search(r'Loading metadata', out, re.MULTILINE) + + +@patch('os.path.exists') +@patch('os.getcwd') +def test_mech_init_mechfile_exists(mock_os_getcwd, mock_os_path_exists, + mech_init_arguments): + """Test 'mech init' when Mechfile exists'.""" + mock_os_getcwd.return_value = '/tmp' + mock_os_path_exists.return_value = True + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = mech_init_arguments + arguments[''] = 'bento/ubuntu-18.04' + with raises(SystemExit, match=r".*already exists in this directory.*"): + a_mech.init(arguments) + + +@patch('os.path.exists') +@patch('os.getcwd') +def test_mech_init_with_invalid_location(mock_os_getcwd, mock_os_path_exists, mech_add_arguments): + """Test if we do not have a valid location. (must be in form of 'hashiaccount/boxname').""" + mock_os_getcwd.return_value = '/tmp' + mock_os_path_exists.return_value = False + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = mech_add_arguments + arguments[''] = 'bento' + with raises(SystemExit, match=r"Provided box name is not valid"): + a_mech.init(arguments) + + +@patch('requests.get') +@patch('os.getcwd') +def test_mech_add_mechfile_exists(mock_os_getcwd, + mock_requests_get, capfd, + catalog_as_json, mech_add_arguments): + """Test 'mech add' when Mechfile exists'.""" + mock_os_getcwd.return_value = '/tmp' + mock_requests_get.return_value.status_code = 200 + mock_requests_get.return_value.json.return_value = catalog_as_json + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = mech_add_arguments + arguments[''] = 'bento/ubuntu-18.04' + arguments[''] = 'second' + a_mech.add(arguments) + out, _ = capfd.readouterr() + mock_os_getcwd.assert_called() + assert re.search(r'Loading metadata', out, re.MULTILINE) + + +def test_mech_add_mechfile_exists_no_name(mech_add_arguments): + """Test 'mech add' when Mechfile exists but no name provided'.""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = mech_add_arguments + arguments[''] = 'bento/ubuntu-18.04' + arguments[''] = None + with raises(SystemExit, match=r".*Need to provide a name.*"): + a_mech.add(arguments) + + +@patch('mech.utils.load_mechfile') +@patch('os.getcwd') +def test_mech_remove(mock_os_getcwd, mock_load_mechfile, capfd, + mechfile_one_entry): + """Test 'mech remove'.""" + mock_load_mechfile.return_value = mechfile_one_entry + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + } + a_mech.remove(arguments) + out, _ = capfd.readouterr() + mock_os_getcwd.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'Removed', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('os.getcwd') +def test_mech_remove_a_nonexisting_entry(mock_os_getcwd, mock_load_mechfile, + mechfile_one_entry): + """Test 'mech remove'.""" + mock_load_mechfile.return_value = mechfile_one_entry + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'second', + } + with raises(SystemExit, match=r".*There is no instance.*"): + a_mech.remove(arguments) + + +def test_mech_remove_no_name(): + """Test 'mech remove' no name provided'.""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + } + with raises(SystemExit, match=r".*Need to provide a name.*"): + a_mech.remove(arguments) + + +@patch('mech.vmrun.VMrun.list', return_value="Total running VMs: 0") +def test_mech_global_status(mock_list, capfd): + """Test 'mech global-status'.""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = {} + a_mech.global_status(arguments) + out, _ = capfd.readouterr() + mock_list.assert_called() + assert re.search(r'Total running VMs', out, re.MULTILINE) + + +PROCESSES = """Process list: 99 +pid=1, owner=root, cmd=/sbin/init +pid=2, owner=root, cmd=kthreadd +pid=3, owner=root, cmd=rcu_gp +pid=4, owner=root, cmd=rcu_par_gp +pid=5, owner=root, cmd=kworker/0:0-events +pid=6, owner=root, cmd=kworker/0:0H-kblockd +""" +@patch('mech.vmrun.VMrun.list_processes_in_guest', return_value=PROCESSES) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/one.vmx') +@patch('os.getcwd') +def test_mech_ps(mock_getcwd, mock_locate, mock_load_mechfile, mock_list_processes, capfd, + mechfile_two_entries): + """Test 'mech ps'.""" + mock_load_mechfile.return_value = mechfile_two_entries + mock_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + } + a_mech.ps(arguments) + out, _ = capfd.readouterr() + mock_getcwd.assert_called() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_list_processes.assert_called() + assert re.search(r'kworker', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='') +@patch('os.getcwd') +def test_mech_ps_not_started_vm(mock_getcwd, mock_locate, + mock_load_mechfile, capfd, + mechfile_two_entries): + """Test 'mech ps'.""" + mock_load_mechfile.return_value = mechfile_two_entries + mock_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'second', + } + a_mech.ps(arguments) + out, _ = capfd.readouterr() + mock_getcwd.assert_called() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'not created', out, re.MULTILINE) + + +@patch('subprocess.run', return_value=True) +@patch('os.chmod', return_value=True) +@patch('mech.vmrun.VMrun.installed_tools', return_value='running') +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_scp_host_to_guest(mock_locate, + mock_load_mechfile, mock_get_ip, + mock_installed_tools, mock_chmod, + mock_subprocess_run, + mechfile_two_entries): + """Test 'mech scp'.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'foo', + '': 'now', + '': 'first:/tmp/now', + } + filename = os.path.join(mech.utils.mech_dir(), 'insecure_private_key') + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): + a_mech.scp(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_subprocess_run.assert_called() + mock_installed_tools.assert_called() + mock_get_ip.assert_called() + mock_chmod.assert_called() + a_mock.assert_called_once_with(filename, 'w') + + +@patch('subprocess.run', return_value=True) +@patch('os.chmod', return_value=True) +@patch('mech.vmrun.VMrun.installed_tools', return_value='running') +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_scp_guest_to_host(mock_locate, + mock_load_mechfile, mock_get_ip, + mock_installed_tools, mock_chmod, + mock_subprocess_run, + mechfile_two_entries): + """Test 'mech scp'.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + '': 'first:/tmp/now', + '': '.', + } + filename = os.path.join(mech.utils.mech_dir(), 'insecure_private_key') + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): + a_mech.scp(arguments) + # Note: Could not figure out how to capture output from subprocess.call. + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_subprocess_run.assert_called() + mock_installed_tools.assert_called() + mock_get_ip.assert_called() + mock_chmod.assert_called() + a_mock.assert_called_once_with(filename, 'w') + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_scp_guest_to_host_not_created(mock_locate, + mock_load_mechfile, + mechfile_two_entries): + """Test 'mech scp'.""" + mock_load_mechfile.return_value = mechfile_two_entries + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + '': 'first:/tmp/now', + '': '.', + } + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): + a_mech.scp(arguments) + # Note: Could not figure out how to capture output from subprocess.call. + mock_locate.assert_called() + mock_load_mechfile.assert_called() + + +def test_mech_scp_invalid_args(): + """Test 'mech scp'.""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': None, + '': 'first:/tmp/now', + '': 'first:/tmp/now2', + } + with raises(SystemExit, match=r"Both src and dst are host destinations"): + a_mech.scp(arguments) diff --git a/mech/test_mech_box.py b/mech/test_mech_box.py new file mode 100644 index 0000000..e7cde03 --- /dev/null +++ b/mech/test_mech_box.py @@ -0,0 +1,140 @@ +# Copyright (c) 2020 Mike Kinney + +"""Unit tests for 'mech box'.""" +import re + +from unittest.mock import patch + +import mech.command +import mech.mech +import mech.vmrun + + +@patch('os.getcwd') +def test_mech_box_list_no_mechdir(mock_os_getcwd, capfd): + """Test 'mech box list' with no '.mech' directory.""" + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + a_mech = mech.mech.MechBox(arguments=global_arguments) + with patch('os.walk') as mock_walk: + # root, dirs, files + mock_walk.return_value = [('./tmp', [], []), ] + a_mech.list({}) + mock_walk.assert_called() + out, _ = capfd.readouterr() + # ensure a header prints out + assert re.search(r'BOX', out, re.MULTILINE) + + +@patch('os.getcwd') +def test_mech_box_list_empty_boxes_dir(mock_os_getcwd, capfd): + """Test 'mech box list' with no directories in '.mech/boxes' directory.""" + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + a_mech = mech.mech.MechBox(arguments=global_arguments) + with patch('os.walk') as mock_walk: + # root, dirs, files + mock_walk.return_value = [('/tmp', ['boxes', ], []), ] + a_mech.list({}) + mock_walk.assert_called() + out, _ = capfd.readouterr() + # ensure a header prints out + assert re.search(r'BOX', out, re.MULTILINE) + + +@patch('os.getcwd') +def test_mech_box_list_one_box(mock_os_getcwd, capfd): + """Test 'mech box list' with one box present.""" + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + a_mech = mech.mech.MechBox(arguments=global_arguments) + with patch('os.walk') as mock_walk: + # simulate: bento/ubuntu-18.04/201912.04.0/vmware_desktop.box + mock_walk.return_value = [ + ('/tmp', ['boxes'], []), + ('/tmp/boxes', ['bento'], []), + ('/tmp/boxes/bento', ['ubuntu-18.04'], []), + ('/tmp/boxes/bento/ubuntu-18.04', ['201912.04.0'], []), + ('/tmp/boxes/bento/ubuntu-18.04/201912.04.0', [], ['vmware_desktop.box']), + ] + a_mech.list({}) + mock_walk.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'ubuntu-18.04', out, re.MULTILINE) + + +@patch('requests.get') +@patch('os.path.exists') +@patch('os.getcwd') +def test_mech_box_add_new(mock_os_getcwd, mock_os_path_exists, + mock_requests_get, capfd, catalog_as_json, + mech_box_arguments): + """Test 'mech box add' from Hashicorp'.""" + mock_os_path_exists.return_value = False + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + mock_requests_get.return_value.status_code = 200 + mock_requests_get.return_value.json.return_value = catalog_as_json + + a_mech = mech.mech.MechBox(arguments=global_arguments) + arguments = mech_box_arguments + arguments[''] = 'bento/ubuntu-19.10' + a_mech.add(arguments) + out, _ = capfd.readouterr() + assert re.search(r'Checking box', out, re.MULTILINE) + + +@patch('requests.get') +@patch('os.path.exists') +@patch('os.getcwd') +def test_mech_box_add_existing(mock_os_getcwd, mock_os_path_exists, + mock_requests_get, capfd, catalog_as_json, + mech_box_arguments): + """Test 'mech box add' from Hashicorp'.""" + mock_os_getcwd.return_value = '/tmp' + mock_os_path_exists.return_value = True + global_arguments = {'--debug': False} + mock_requests_get.return_value.status_code = 200 + mock_requests_get.return_value.json.return_value = catalog_as_json + + a_mech = mech.mech.MechBox(arguments=global_arguments) + arguments = mech_box_arguments + arguments[''] = 'bento/ubuntu-19.10' + a_mech.add(arguments) + out, _ = capfd.readouterr() + assert re.search(r'Loading metadata', out, re.MULTILINE) + + +@patch('shutil.rmtree') +@patch('os.path.exists') +def test_mech_box_remove_exists(mock_os_path_exists, mock_rmtree, capfd): + """Test 'mech box remove'.""" + mock_os_path_exists.return_value = True + mock_rmtree.return_value = True + global_arguments = {'--debug': False} + a_mech = mech.mech.MechBox(arguments=global_arguments) + arguments = { + '': 'bento/ubuntu-18.04', + '': 'somever', + } + a_mech.remove(arguments) + out, _ = capfd.readouterr() + mock_os_path_exists.assert_called() + mock_rmtree.assert_called() + assert re.search(r'Removed ', out, re.MULTILINE) + + +@patch('os.path.exists') +def test_mech_box_remove_does_not_exists(mock_os_path_exists, capfd): + """Test 'mech box remove'.""" + mock_os_path_exists.return_value = False + global_arguments = {'--debug': False} + a_mech = mech.mech.MechBox(arguments=global_arguments) + arguments = { + '': 'bento/ubuntu-18.04', + '': 'somever', + } + a_mech.remove(arguments) + out, _ = capfd.readouterr() + mock_os_path_exists.assert_called() + assert re.search(r'No boxes were removed', out, re.MULTILINE) diff --git a/mech/test_mech_snapshot.py b/mech/test_mech_snapshot.py new file mode 100644 index 0000000..918d157 --- /dev/null +++ b/mech/test_mech_snapshot.py @@ -0,0 +1,240 @@ +# Copyright (c) 2020 Mike Kinney + +"""Unit tests for 'mech snapshot'.""" +import re + +from unittest.mock import patch +from pytest import raises + +import mech.command +import mech.mech +import mech.vmrun + + +@patch('mech.utils.load_mechfile') +@patch('os.getcwd') +def test_mech_snapshot_list_no_mechdir(mock_os_getcwd, mock_load_mechfile, capfd, + mechfile_two_entries): + """Test 'mech snapshot list' with no '.mech' directory.""" + mock_load_mechfile.return_value = mechfile_two_entries + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + a_mech = mech.mech.MechSnapshot(arguments=global_arguments) + with patch('os.walk') as mock_walk: + # root, dirs, files + mock_walk.return_value = [('./tmp', [], []), ] + arguments = {'': None} + a_mech.list(arguments) + mock_walk.assert_called() + mock_load_mechfile.assert_called() + out, _ = capfd.readouterr() + # ensure a header prints out + assert re.search(r'Snapshots', out, re.MULTILINE) + + +SNAPSHOT_LIST_WITHOUT_SNAPSHOTS = """Snapshots for instance:first +Total snapshots: 0 +Snapshots for instance:second +Instance (second) is not created.""" +@patch('mech.vmrun.VMrun.list_snapshots', return_value=SNAPSHOT_LIST_WITHOUT_SNAPSHOTS) +@patch('mech.utils.load_mechfile') +@patch('os.getcwd') +def test_mech_snapshot_list_no_snapshots(mock_os_getcwd, mock_load_mechfile, + mock_list_snapshots, capfd, + mechfile_two_entries): + """Test 'mech snapshot list' without any snapshots.""" + mock_load_mechfile.return_value = mechfile_two_entries + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + a_mech = mech.mech.MechSnapshot(arguments=global_arguments) + with patch('os.walk') as mock_walk: + mock_walk.return_value = [ + ('/tmp', ['first'], []), + ('/tmp/first', [], ['some.vmx']), + ] + + # with no args + arguments = {'': None} + a_mech.list(arguments) + mock_walk.assert_called() + mock_load_mechfile.assert_called() + mock_list_snapshots.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'Total snapshots: 0', out, re.MULTILINE) + assert re.search(r'Instance \(second\) is not created.', out, re.MULTILINE) + + # single instance + arguments = {'': 'first'} + a_mech.list(arguments) + out, _ = capfd.readouterr() + mock_load_mechfile.assert_called() + mock_list_snapshots.assert_called() + assert re.search(r'Total snapshots: 0', out, re.MULTILINE) + + +SNAPSHOT_LIST_WITH_SNAPSHOT = """Snapshots for instance:first +Total snapshots: 1 +snap1 +Snapshots for instance:second +Instance (second) is not created.""" +@patch('mech.vmrun.VMrun.list_snapshots', return_value=SNAPSHOT_LIST_WITH_SNAPSHOT) +@patch('mech.utils.load_mechfile') +@patch('os.getcwd') +def test_mech_snapshot_list_with_snapshot(mock_os_getcwd, mock_load_mechfile, + mock_list_snapshots, capfd, + mechfile_two_entries): + """Test 'mech snapshot list' with a snapshots.""" + mock_load_mechfile.return_value = mechfile_two_entries + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + a_mech = mech.mech.MechSnapshot(arguments=global_arguments) + with patch('os.walk') as mock_walk: + mock_walk.return_value = [ + ('/tmp', ['first'], []), + ('/tmp/first', [], ['some.vmx']), + ] + + # with no args + arguments = {'': None} + a_mech.list(arguments) + mock_walk.assert_called() + mock_load_mechfile.assert_called() + mock_list_snapshots.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'Total snapshots: 1', out, re.MULTILINE) + assert re.search(r'Instance \(second\) is not created.', out, re.MULTILINE) + + # single instance + arguments = {'': 'first'} + a_mech.list(arguments) + out, _ = capfd.readouterr() + mock_load_mechfile.assert_called() + mock_list_snapshots.assert_called() + assert re.search(r'Total snapshots: 1', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.delete_snapshot') +@patch('mech.vmrun.VMrun.list_snapshots', return_value=SNAPSHOT_LIST_WITH_SNAPSHOT) +@patch('mech.utils.load_mechfile') +@patch('os.getcwd') +def test_mech_snapshot_delete_snapshot(mock_os_getcwd, mock_load_mechfile, + mock_list_snapshots, mock_delete_snapshot, capfd, + mechfile_two_entries): + """Test 'mech snapshot delete'.""" + mock_load_mechfile.return_value = mechfile_two_entries + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + a_mech = mech.mech.MechSnapshot(arguments=global_arguments) + with patch('os.walk') as mock_walk: + mock_walk.return_value = [ + ('/tmp', ['first'], []), + ('/tmp/first', [], ['some.vmx']), + ] + + arguments = {'': 'first'} + a_mech.list(arguments) + out, _ = capfd.readouterr() + mock_load_mechfile.assert_called() + mock_list_snapshots.assert_called() + assert re.search(r'Total snapshots: 1', out, re.MULTILINE) + + arguments = {'': 'first', '': 'snap2'} + mock_delete_snapshot.return_value = None + a_mech.delete(arguments) + out, _ = capfd.readouterr() + mock_delete_snapshot.assert_called() + mock_list_snapshots.assert_called() + assert re.search(r'Cannot delete', out, re.MULTILINE) + + arguments = {'': 'first', '': 'snap1'} + # Note: delete_snapshots return None if could not delete, or '' if it could + mock_delete_snapshot.return_value = '' + a_mech.delete(arguments) + out, _ = capfd.readouterr() + mock_delete_snapshot.assert_called() + assert re.search(r' deleted', out, re.MULTILINE) + + arguments = {'': 'first'} + mock_list_snapshots.return_value = SNAPSHOT_LIST_WITHOUT_SNAPSHOTS + a_mech.list(arguments) + out, _ = capfd.readouterr() + mock_list_snapshots.assert_called() + mock_delete_snapshot.assert_called() + assert re.search(r'Total snapshots: 0', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_snapshot_list_not_created(mock_locate, mock_load_mechfile, capfd, + mechfile_one_entry): + """Test 'mech snapshot list' when vm is not created.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + arguments = { + '': 'first', + } + a_mech = mech.mech.MechSnapshot(arguments=global_arguments) + arguments = {'': 'first'} + a_mech.list(arguments) + out, _ = capfd.readouterr() + mock_load_mechfile.assert_called() + mock_locate.assert_called() + assert re.search(r'not created', out, re.MULTILINE) + + +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value=None) +def test_mech_snapshot_save_not_created(mock_locate, mock_load_mechfile, capfd, + mechfile_one_entry): + """Test 'mech snapshot save' when vm is not created.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + arguments = { + '': 'first', + } + a_mech = mech.mech.MechSnapshot(arguments=global_arguments) + arguments = {'': 'first', '': 'snap1'} + a_mech.save(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + assert re.search(r'not created', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.snapshot', return_value='Snapshot (snap1) on VM (first) taken') +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_snapshot_save_success(mock_locate, mock_load_mechfile, + mock_snapshot, capfd, mechfile_one_entry): + """Test 'mech snapshot save' successful.""" + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + arguments = { + '': 'first', + } + a_mech = mech.mech.MechSnapshot(arguments=global_arguments) + arguments = {'': 'first', '': 'snap1'} + a_mech.save(arguments) + out, _ = capfd.readouterr() + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_snapshot.assert_called() + assert re.search(r' taken', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.snapshot', return_value=None) +@patch('mech.utils.load_mechfile') +@patch('mech.utils.locate') +def test_mech_snapshot_save_failure(mock_locate, mock_load_mechfile, + mock_vmrun_snapshot, mechfile_one_entry): + """Test 'mech snapshot save' failure.""" + mock_locate.return_value = '/tmp/first/some.vmx' + mock_load_mechfile.return_value = mechfile_one_entry + global_arguments = {'--debug': False} + arguments = { + '': 'first', + } + a_mech = mech.mech.MechSnapshot(arguments=global_arguments) + arguments = {'': 'first', '': 'snap1'} + with raises(SystemExit, match=r"Warning: Could not take snapshot."): + a_mech.save(arguments) diff --git a/mech/test_utils.py b/mech/test_utils.py new file mode 100644 index 0000000..e33858c --- /dev/null +++ b/mech/test_utils.py @@ -0,0 +1,768 @@ +# Copyright (c) 2020 Mike Kinney + +"""Test mech utils.""" +import os +import re + +from unittest.mock import patch, mock_open, MagicMock +from collections import OrderedDict +from pytest import raises + +import mech.utils + + +@patch('os.getcwd') +def test_main_dir(mock_os_getcwd): + """Test main_dir().""" + mock_os_getcwd.return_value = '/tmp' + main = mech.utils.main_dir() + mock_os_getcwd.assert_called() + assert main == '/tmp' + + +@patch('os.getcwd') +def test_mech_dir(mock_os_getcwd): + """Test mech_dir().""" + mock_os_getcwd.return_value = '/tmp' + mechdir = mech.utils.mech_dir() + mock_os_getcwd.assert_called() + assert mechdir == '/tmp/.mech' + + +@patch('json.loads') +@patch('os.path.isfile') +@patch('os.getcwd') +def test_load_mechfile(mock_os_getcwd, mock_os_path_isfile, mock_json_loads): + """Test mech_load_mechfile().""" + mock_os_getcwd.return_value = '/tmp' + expected = {} + mock_json_loads.return_value = expected + mock_os_path_isfile.return_value = True + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): + assert mech.utils.load_mechfile() == expected + a_mock.assert_called() + mock_os_getcwd.assert_called() + + +@patch('json.loads') +@patch('os.path.isfile') +@patch('os.getcwd') +def test_load_mechfile_no_mechfile(mock_os_getcwd, mock_os_path_isfile, mock_json_loads): + """Test mech_load_mechfile().""" + mock_os_getcwd.return_value = '/tmp' + expected = {} + mock_json_loads.return_value = expected + mock_os_path_isfile.return_value = False + with raises(SystemExit): + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): + mech.utils.load_mechfile() + + +@patch('json.loads') +@patch('os.path.isfile') +@patch('os.getcwd') +def test_load_mechfile_no_mechfile_should_not_exist(mock_os_getcwd, mock_os_path_isfile, + mock_json_loads): + """Test mech_load_mechfile().""" + mock_os_getcwd.return_value = '/tmp' + expected = {} + mock_json_loads.return_value = expected + mock_os_path_isfile.return_value = False + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): + got = mech.utils.load_mechfile(should_exist=False) + assert got == expected + mock_os_getcwd.assert_called() + + +@patch('os.path.isfile') +@patch('os.getcwd') +def test_load_mechfile_invalid_json(mock_os_getcwd, mock_os_path_isfile): + """Test mech_load_mechfile().""" + mock_os_getcwd.return_value = '/tmp' + expected = {} + # bad_json below is missing a comma between elements + bad_json = '''{"foo": "bar" "foo2": 1}''' + mock_os_path_isfile.return_value = True + a_mock = mock_open(read_data=bad_json) + with patch('builtins.open', a_mock, create=True): + got = mech.utils.load_mechfile() + assert got == expected + a_mock.assert_called() + mock_os_getcwd.assert_called() + + +def test_save_mechfile_empty_config(): + """Test save_mechfile with empty configuration.""" + filename = os.path.join(mech.utils.main_dir(), 'Mechfile') + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): + assert mech.utils.save_mechfile({}) + a_mock.assert_called_once_with(filename, 'w+') + a_mock.return_value.write.assert_called_once_with('{}') + + +def test_save_mechfile_one(helpers): + """Test save_mechfile with one entry.""" + first_dict = { + 'first': { + 'name': + 'first', + 'box': + 'bento/ubuntu-18.04', + 'box_version': + '201912.04.0', + 'url': + 'https://vagrantcloud.com/bento/boxes/ubuntu-18.04/' + 'versions/201912.04.0/providers/vmware_desktop.box' + } + } + first_json = '''{ + "first": { + "box": "bento/ubuntu-18.04", + "box_version": "201912.04.0", + "name": "first", + "url": "https://vagrantcloud.com/bento/boxes/ubuntu-18.04/versions/201912.04.0/providers/vmware_desktop.box" + } +}''' # noqa: 501 + filename = os.path.join(mech.utils.main_dir(), 'Mechfile') + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): + assert mech.utils.save_mechfile(first_dict) + a_mock.assert_called_once_with(filename, 'w+') + assert first_json == helpers.get_mock_data_written(a_mock) + + +def test_save_mechfile_two(helpers): + """Test save_mechfile with two entries.""" + two_dict = { + 'first': { + 'name': + 'first', + 'box': + 'bento/ubuntu-18.04', + 'box_version': + '201912.04.0', + 'url': + 'https://vagrantcloud.com/bento/boxes/ubuntu-18.04/' + 'versions/201912.04.0/providers/vmware_desktop.box' + }, + 'second': { + 'name': + 'second', + 'box': + 'bento/ubuntu-18.04', + 'box_version': + '201912.04.0', + 'url': + 'https://vagrantcloud.com/bento/boxes/ubuntu-18.04/' + 'versions/201912.04.0/providers/vmware_desktop.box' + } + } + two_json = '''{ + "first": { + "box": "bento/ubuntu-18.04", + "box_version": "201912.04.0", + "name": "first", + "url": "https://vagrantcloud.com/bento/boxes/ubuntu-18.04/versions/201912.04.0/providers/vmware_desktop.box" + }, + "second": { + "box": "bento/ubuntu-18.04", + "box_version": "201912.04.0", + "name": "second", + "url": "https://vagrantcloud.com/bento/boxes/ubuntu-18.04/versions/201912.04.0/providers/vmware_desktop.box" + } +}''' # noqa: 501 + filename = os.path.join(mech.utils.main_dir(), 'Mechfile') + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): + assert mech.utils.save_mechfile(two_dict) + a_mock.assert_called_once_with(filename, 'w+') + assert two_json == helpers.get_mock_data_written(a_mock) + + +def test_tar_cmd(): + """Test tar cmd. + Note: not really a unit test per se, as it calls out. + """ + assert ["tar"] == mech.utils.tar_cmd() + + +def test_tar_cmd_when_tar_not_found(): + """Test tar cmd.""" + a_mock = MagicMock() + a_mock.return_value = None + a_mock.returncode = None + a_mock.side_effect = OSError() + with patch('subprocess.Popen', a_mock): + tar = mech.utils.tar_cmd() + assert tar is None + + +def test_config_ssh_string_empty(): + """Test config_ssh_string with empty configuration.""" + ssh_string = mech.utils.config_ssh_string({}) + assert ssh_string == "Host \n" + + +def test_config_ssh_string_simple(): + """Test config_ssh_string with a simple configuration.""" + config = { + "Host": "first", + "User": "foo", + "Port": "22", + "UserKnownHostsFile": "/dev/null", + "StrictHostKeyChecking": "no", + "PasswordAuthentication": "no", + "IdentityFile": 'blah', + "IdentitiesOnly": "yes", + "LogLevel": "FATAL", + } + ssh_string = mech.utils.config_ssh_string(config) + assert ssh_string == 'Host first\n User foo\n Port 22\n UserKnownHostsFile /dev/null\n StrictHostKeyChecking no\n PasswordAuthentication no\n IdentityFile blah\n IdentitiesOnly yes\n LogLevel FATAL\n' # noqa: E501 pylint: disable=line-too-long + + +@patch('mech.utils.load_mechfile', return_value={}) +@patch('mech.utils.save_mechfile', return_value=True) +def test_save_mechfile_entry_with_empty_mechfile(load_mock, save_mock): + """Test save_mechfile_entry with no entries in the mechfile.""" + entry = {'first': {'name': 'first'}} + assert mech.utils.save_mechfile_entry(entry, 'first', True) + load_mock.assert_called_once() + save_mock.assert_called_once() + + +@patch('mech.utils.load_mechfile', return_value={}) +@patch('mech.utils.save_mechfile', return_value=True) +def test_save_mechfile_entry_with_blank_name(load_mock, save_mock): + """Test save_mechfile_entry with a blank name.""" + entry = {'first': {'name': 'first'}} + assert mech.utils.save_mechfile_entry(entry, '', True) + load_mock.assert_called_once() + save_mock.assert_called_once() + + +@patch('mech.utils.load_mechfile', return_value={}) +@patch('mech.utils.save_mechfile', return_value=True) +def test_save_mechfile_entry_with_name_as_none(load_mock, save_mock): + """Test save_mechfile_entry with name as None.""" + entry = {'first': {'name': 'first'}} + assert mech.utils.save_mechfile_entry(entry, None, True) + load_mock.assert_called_once() + save_mock.assert_called_once() + + +@patch('mech.utils.load_mechfile', return_value={}) +@patch('mech.utils.save_mechfile', return_value=True) +def test_save_mechfile_entry_twice(load_mock, save_mock): + """Test save_mechfile_entry multiple times.""" + entry = {'first': {'name': 'first'}} + assert mech.utils.save_mechfile_entry(entry, 'first', True) + load_mock.assert_called_once() + save_mock.assert_called_once() + assert mech.utils.save_mechfile_entry(entry, 'first', True) + + +@patch('mech.utils.load_mechfile', return_value={}) +@patch('mech.utils.save_mechfile', return_value=True) +def test_remove_mechfile_entry_with_empty_mechfile(load_mock, save_mock): + """Test remove_mechfile_entry with no entries in the mechfile.""" + assert mech.utils.remove_mechfile_entry('first', True) + load_mock.assert_called_once() + save_mock.assert_called_once() + + +@patch('mech.utils.load_mechfile', return_value={'first': {'name': 'first'}}) +@patch('mech.utils.save_mechfile', return_value=True) +def test_remove_mechfile_entry(load_mock, save_mock): + """Test remove_mechfile_entry.""" + assert mech.utils.remove_mechfile_entry('first', True) + load_mock.assert_called_once() + save_mock.assert_called_once() + + +def test_parse_vmx(): + """Test parse_vmx.""" + partial_vmx = '''.encoding = "UTF-8" +bios.bootorder = "hdd,cdrom" +checkpoint.vmstate = "" + +cleanshutdown = "FALSE" +config.version = "8"''' + expected_vmx = OrderedDict([ + ('.encoding', '"UTF-8"'), + ('bios.bootorder', '"hdd,cdrom"'), + ('checkpoint.vmstate', '""'), + ('cleanshutdown', '"FALSE"'), + ('config.version', '"8"') + ]) + a_mock = mock_open(read_data=partial_vmx) + with patch('builtins.open', a_mock): + assert mech.utils.parse_vmx(partial_vmx) == expected_vmx + a_mock.assert_called() + + +@patch('mech.utils.parse_vmx') +def test_update_vmx_empty(mock_parse_vmx, helpers, capfd): + """Test update_vmx.""" + expected_vmx = """ethernet0.addresstype = generated +ethernet0.bsdname = en0 +ethernet0.connectiontype = nat +ethernet0.displayname = Ethernet +ethernet0.linkstatepropagation.enable = FALSE +ethernet0.pcislotnumber = 32 +ethernet0.present = TRUE +ethernet0.virtualdev = e1000 +ethernet0.wakeonpcktrcv = FALSE +""" + mock_parse_vmx.return_value = {} + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): + mech.utils.update_vmx('/tmp/first/one.vmx') + a_mock.assert_called() + got = helpers.get_mock_data_written(a_mock) + assert expected_vmx == got + out, _ = capfd.readouterr() + assert re.search(r'Added network interface to vmx file', out, re.MULTILINE) + + +@patch('mech.utils.parse_vmx') +def test_update_vmx_with_a_network_entry(mock_parse_vmx, capfd): + """Test update_vmx.""" + mock_parse_vmx.return_value = {'ethernet0.present': 'true'} + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): + mech.utils.update_vmx('/tmp/first/one.vmx') + assert not a_mock.called, 'should not have written anything to the vmx file' + out, _ = capfd.readouterr() + assert out == '' + + +@patch('mech.utils.parse_vmx') +def test_update_vmx_with_cpu_and_memory(mock_parse_vmx, helpers, capfd): + """Test update_vmx.""" + mock_parse_vmx.return_value = {'ethernet0.present': 'true'} + expected_vmx = '''ethernet0.present = true +numvcpus = "3" +memsize = "1025" +''' + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): + mech.utils.update_vmx('/tmp/first/one.vmx', numvcpus=3, memsize=1025) + a_mock.assert_called() + got = helpers.get_mock_data_written(a_mock) + assert expected_vmx == got + out, _ = capfd.readouterr() + assert out == '' + + +def test_build_mechfile_entry_no_location(): + """Test if None is used for location.""" + assert mech.utils.build_mechfile_entry(location=None) == {} + + +def test_build_mechfile_entry_https_location(): + """Test if location starts with 'https://'.""" + assert mech.utils.build_mechfile_entry(location='https://foo') == { + 'box': None, + 'box_version': None, + 'name': None, + 'shared_folders': [{'host_path': '../..', 'share_name': 'mech'}], + 'url': 'https://foo' + } + + +def test_build_mechfile_entry_http_location(): + """Test if location starts with 'http://'.""" + assert mech.utils.build_mechfile_entry(location='http://foo') == { + 'box': None, + 'box_version': None, + 'name': None, + 'shared_folders': [{'host_path': '../..', 'share_name': 'mech'}], + 'url': + 'http://foo' + } + + +def test_build_mechfile_entry_ftp_location(): + """Test if location starts with 'ftp://'.""" + assert mech.utils.build_mechfile_entry(location='ftp://foo') == { + 'box': None, + 'box_version': None, + 'name': None, + 'shared_folders': [{'host_path': '../..', 'share_name': 'mech'}], + 'url': 'ftp://foo' + } + + +def test_build_mechfile_entry_ftp_location_with_other_values(): + """Test if mechfile_entry is filled out.""" + expected = { + 'box': 'bbb', + 'box_version': 'ccc', + 'name': 'aaa', + 'shared_folders': [{'host_path': '../..', 'share_name': 'mech'}], + 'url': 'ftp://foo' + } + assert mech.utils.build_mechfile_entry(location='ftp://foo', name='aaa', + box='bbb', box_version='ccc') == expected + + +def test_build_mechfile_entry_file_location_json(catalog): + """Test if location starts with 'file:' and contains valid json.""" + + # Note: Download/format json like this: + # curl --header 'Accept:application/json' \ + # 'https://app.vagrantup.com/bento/boxes/ubuntu-18.04' | python3 -m json.tool + expected = { + 'box': 'bento/ubuntu-18.04', + 'box_version': 'aaa', + 'name': 'first', + 'shared_folders': [{'host_path': '../..', 'share_name': 'mech'}], + 'url': + 'https://vagrantcloud.com/bento/boxes/ubuntu-18.04/\ +versions/aaa/providers/vmware_desktop.box' + } + a_mock = mock_open(read_data=catalog) + with patch('builtins.open', a_mock): + actual = mech.utils.build_mechfile_entry(location='file:/tmp/one.json') + assert expected == actual + a_mock.assert_called() + + +def test_build_mechfile_entry_file_location_but_file_not_found(): + """Test if location starts with 'file:' and file does not exist.""" + with patch('builtins.open', mock_open()) as mock_file: + mock_file.side_effect = SystemExit() + with raises(SystemExit): + mech.utils.build_mechfile_entry(location='file:/tmp/one.box') + + +@patch('requests.get') +def test_build_mechfile_entry_file_location_external_good(mock_requests_get, + catalog_as_json): + """Test if location talks to Hashicorp.""" + expected = { + 'box': 'bento/ubuntu-18.04', + 'box_version': 'aaa', + 'name': None, + 'shared_folders': [{'host_path': '../..', 'share_name': 'mech'}], + 'url': + 'https://vagrantcloud.com/bento/boxes/ubuntu-18.04/\ +versions/aaa/providers/vmware_desktop.box' + } + mock_requests_get.return_value.status_code = 200 + mock_requests_get.return_value.json.return_value = catalog_as_json + actual = mech.utils.build_mechfile_entry(location='bento/ubuntu-18.04') + mock_requests_get.assert_called() + assert expected == actual + + +def test_build_mechfile_entry_file_location_external_bad_location(): + """Test if we do not have a valid location. (must be in form of 'hashiaccount/boxname').""" + with raises(SystemExit, match=r"Provided box name is not valid"): + mech.utils.build_mechfile_entry(location='bento') + + +def test_provision_no_instance(): + """Test provisioning.""" + with raises(SystemExit, match=r"Need to provide an instance to provision"): + mech.utils.provision(instance=None, show=None) + + +def test_provision_no_vmx(): + """Test provisioning.""" + mock_inst = MagicMock() + mock_inst.vmx = None + with raises(SystemExit, match=r"Need to provide vmx.*"): + mech.utils.provision(instance=mock_inst, show=None) + + +@patch('mech.vmrun.VMrun.installed_tools') +def test_provision_no_vmare_tools(mock_installed_tools): + """Test provisioning.""" + mock_inst = MagicMock() + mock_installed_tools.return_value = None + with raises(SystemExit, match=r"Cannot provision if VMware Tools are not installed"): + mech.utils.provision(instance=mock_inst, show=None) + + +@patch('mech.utils.provision_file') +@patch('mech.vmrun.VMrun.installed_tools') +def test_provision_file_no_provisioning(mock_installed_tools, mock_provision_file, capfd): + """Test provisioning.""" + mock_installed_tools.return_value = "running" + mock_provision_file.return_value = None + mock_inst = MagicMock() + mock_inst.provision = [] + mock_inst.created = True + mock_inst.use_psk = False + mech.utils.provision(instance=mock_inst, show=None) + out, _ = capfd.readouterr() + assert re.search(r'Nothing to provision', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.copy_file_from_host_to_guest') +@patch('mech.vmrun.VMrun.installed_tools') +def test_provision_file(mock_installed_tools, mock_copy_file, capfd): + """Test provisioning.""" + mock_installed_tools.return_value = "running" + mock_copy_file.return_value = True + config = [ + { + "type": "file", + "source": "file1.txt", + "destination": "/tmp/file1.txt", + }, + ] + mock_inst = MagicMock() + mock_inst.name = 'first' + mock_inst.vmx = '/tmp/first/some.vmx' + mock_inst.provision = config + mock_inst.use_psk = False + mech.utils.provision(instance=mock_inst, show=None) + out, _ = capfd.readouterr() + assert re.search(r'Copying ', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.copy_file_from_host_to_guest') +@patch('mech.vmrun.VMrun.installed_tools') +def test_provision_file_could_not_copy_file_to_guest(mock_installed_tools, + mock_copy_file, capfd): + """Test provisioning.""" + mock_installed_tools.return_value = "running" + mock_copy_file.return_value = None + config = [ + { + "type": "file", + "source": "file1.txt", + "destination": "/tmp/file1.txt", + }, + ] + mock_inst = MagicMock() + mock_inst.name = 'first' + mock_inst.vmx = '/tmp/first/some.vmx' + mock_inst.provision = config + mock_inst.use_psk = False + mech.utils.provision(instance=mock_inst, show=None) + out, _ = capfd.readouterr() + assert re.search(r'Not Provisioned', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.installed_tools') +def test_provision_file_show(mock_installed_tools, capfd): + """Test provisioning.""" + mock_installed_tools.return_value = "running" + config = [ + { + "type": "file", + "source": "file1.txt", + "destination": "/tmp/file1.txt", + }, + ] + mock_inst = MagicMock() + mock_inst.name = 'first' + mock_inst.vmx = '/tmp/first/some.vmx' + mock_inst.provision = config + mech.utils.provision(instance=mock_inst, show=True) + out, _ = capfd.readouterr() + assert re.search(r'instance:', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.delete_file_in_guest', return_value=True) +@patch('mech.vmrun.VMrun.run_program_in_guest', return_value=True) +@patch('mech.vmrun.VMrun.run_script_in_guest', return_value=True) +@patch('os.path.isfile', return_value=True) +@patch('mech.vmrun.VMrun.create_tempfile_in_guest', return_value='/tmp/foo') +@patch('mech.vmrun.VMrun.copy_file_from_host_to_guest', return_value=True) +@patch('mech.vmrun.VMrun.installed_tools', return_value="running") +def test_provision_shell(mock_installed_tools, mock_copy_file, + mock_create_tempfile, mock_isfile, + mock_run_script_in_guest, mock_run_program_in_guest, + mock_delete_file_in_guest, capfd): + """Test provisioning.""" + config = [ + { + "type": "shell", + "path": "file1.sh", + "args": [ + "a=1", + "b=true", + ], + }, + { + "type": "shell", + "inline": "echo hello from inline" + }, + ] + mock_inst = MagicMock() + mock_inst.name = 'first' + mock_inst.vmx = '/tmp/first/some.vmx' + mock_inst.provision = config + mock_inst.use_psk = False + mech.utils.provision(instance=mock_inst, show=None) + out, _ = capfd.readouterr() + mock_installed_tools.assert_called() + mock_copy_file.assert_called() + mock_create_tempfile.assert_called() + mock_isfile.assert_called() + mock_run_script_in_guest.assert_called() + mock_run_program_in_guest.assert_called() + mock_delete_file_in_guest.assert_called() + assert re.search(r'Configuring script', out, re.MULTILINE) + assert re.search(r'Configuring environment', out, re.MULTILINE) + assert re.search(r'Configuring script to run inline', out, re.MULTILINE) + assert re.search(r'Executing program', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.installed_tools', return_value="running") +def test_provision_shell_show_only(mock_installed_tools, capfd): + """Test provisioning.""" + config = [ + { + "type": "shell", + "path": "file1.sh", + "args": [ + "a=1", + "b=true", + ], + }, + ] + mock_inst = MagicMock() + mock_inst.name = 'first' + mock_inst.vmx = '/tmp/first/some.vmx' + mock_inst.provision = config + mech.utils.provision(instance=mock_inst, show=True) + out, _ = capfd.readouterr() + mock_installed_tools.assert_called() + assert re.search(r' instance:', out, re.MULTILINE) + + +@patch('mech.utils.provision_shell', return_value=None) +@patch('mech.vmrun.VMrun.installed_tools', return_value="running") +def test_provision_shell_with_issue(mock_installed_tools, mock_provision_shell, + capfd): + """Test provisioning.""" + config = [ + { + "type": "shell", + "path": "file1.sh", + "args": [ + "a=1", + "b=true", + ], + }, + ] + mock_inst = MagicMock() + mock_inst.name = 'first' + mock_inst.vmx = '/tmp/first/some.vmx' + mock_inst.provision = config + mech.utils.provision(instance=mock_inst, show=None) + out, _ = capfd.readouterr() + mock_installed_tools.assert_called() + mock_provision_shell.assert_called() + assert re.search(r'Not Provisioned', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.installed_tools', return_value="running") +def test_provision_with_unknown_type(mock_installed_tools, capfd): + """Test provisioning.""" + config = [ + { + "type": "foo", + }, + ] + mock_inst = MagicMock() + mock_inst.name = 'first' + mock_inst.vmx = '/tmp/first/some.vmx' + mock_inst.provision = config + mech.utils.provision(instance=mock_inst, show=None) + out, _ = capfd.readouterr() + mock_installed_tools.assert_called() + assert re.search(r'Not Provisioned', out, re.MULTILINE) + + +@patch('os.environ') +def test_get_fallback_executable_no_path_in_environ(mock_os_environ): + """Weird case where PATH is is not in the environment.""" + mock_os_environ.return_value = '' + assert mech.utils.get_fallback_executable() is None + + +@patch('os.path.exists') +def test_get_fallback_executable(mock_os_path_exists): + """Find vmrun in PATH.""" + mock_os_path_exists.return_value = True + with patch.dict('os.environ', {'PATH': '/tmp:/tmp2'}): + got = mech.utils.get_fallback_executable() + expected = '/tmp/vmrun' + assert got == expected + mock_os_path_exists.assert_called() + + +@patch('os.path.exists') +def test_darwin_executable_when_installed(mock_os_path_exists): + """Find vmrun in PATH.""" + expected = '/Applications/VMware Fusion.app/Contents/Library/vmrun' + mock_os_path_exists.return_value = True + got = mech.utils.get_darwin_executable() + assert expected == got + mock_os_path_exists.assert_called() + + +@patch('os.path.exists') +def test_darwin_executable_when_not_installed(mock_os_path_exists): + """Find vmrun in PATH.""" + # deal with a different file returns a different mocked value + def side_effect(filename): + if filename == '/Applications/VMware Fusion.app/Contents/Library/vmrun': + return False + else: + return True + mock_os_path_exists.side_effect = side_effect + expected = '/tmp/vmrun' + with patch.dict('os.environ', {'PATH': '/tmp:/tmp2'}): + got = mech.utils.get_darwin_executable() + assert expected == got + + +def test_catalog_to_mechfile_when_empty_catalog(): + """Test catalog_to_mechfile.""" + catalog = {} + with raises(SystemExit): + mech.utils.catalog_to_mechfile(catalog) + + +@patch('mech.utils.locate') +def test_init_box_cannot_find_valid_box(mock_locate): + """Test init_box.""" + mock_locate.return_value = None + with raises(SystemExit): + mech.utils.init_box(name='first') + + +def test_add_mechfile_with_empty_mechfile(): + """Test add_mechfile.""" + mech.utils.add_mechfile(mechfile_entry={}) + + +@patch('requests.get') +@patch('mech.utils.locate') +def test_add_box_url(mock_locate, mock_requests_get, catalog_as_json): + """Test init_box.""" + mock_locate.return_value = False + mock_requests_get.return_value.status_code = 200 + mock_requests_get.return_value.json.return_value = catalog_as_json + got = mech.utils.add_box_url(name='first', box='abox', box_version='aver', url='') + assert got is None + + +@patch('os.getlogin', return_value='bob') +@patch('os.path.expanduser', return_value='/home/bob/id_rsa.pub') +def test_get_info_for_auth(mock_path_expanduser, mock_getlogin): + """Test get_info_for_auth.""" + expected = {'auth': {'username': 'bob', 'pub_key': '/home/bob/id_rsa.pub', 'mech_use': False}} + got = mech.utils.get_info_for_auth() + assert got == expected diff --git a/mech/test_vmrun.py b/mech/test_vmrun.py new file mode 100644 index 0000000..c2c1df1 --- /dev/null +++ b/mech/test_vmrun.py @@ -0,0 +1,633 @@ +# Copyright (c) 2020 Mike Kinney + +"""Tests for VMrun class.""" + +from unittest.mock import patch, MagicMock, mock_open + +import mech.vmrun + + +@patch('subprocess.Popen') +def test_vmrun_vmrun(mock_popen): + """Test vmrun method.""" + process_mock = MagicMock() + attrs = {'communicate.return_value': ('output', 'error')} + process_mock.configure_mock(**attrs) + mock_popen.return_value = process_mock + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', + executable='/tmp/vmrun', provider='ws') + got = vmrun.vmrun('list', vmrun.vmx_file) + assert got is None + mock_popen.assert_called() + + +def test_vmrun_start(): + """Test start method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'start', '/tmp/first/some.vmx', 'nogui'] + got = vmrun.start() + assert got == expected + + +def test_vmrun_stop(): + """Test stop method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'stop', '/tmp/first/some.vmx', 'soft'] + got = vmrun.stop() + assert got == expected + + +def test_vmrun_reset(): + """Test reset method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'reset', '/tmp/first/some.vmx', 'soft'] + got = vmrun.reset() + assert got == expected + + +def test_vmrun_suspend(): + """Test suspend method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'suspend', '/tmp/first/some.vmx', 'soft'] + got = vmrun.suspend() + assert got == expected + + +def test_vmrun_pause(): + """Test pause method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'pause', '/tmp/first/some.vmx'] + got = vmrun.pause() + assert got == expected + + +def test_vmrun_unpause(): + """Test unpause method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'unpause', '/tmp/first/some.vmx'] + got = vmrun.unpause() + assert got == expected + + +def test_vmrun_list_snapshots(): + """Test list_snapshots method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'listSnapshots', '/tmp/first/some.vmx'] + got = vmrun.list_snapshots() + assert got == expected + + +def test_vmrun_snapshot(): + """Test snapshot method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'snapshot', '/tmp/first/some.vmx', 'snap1'] + got = vmrun.snapshot('snap1') + assert got == expected + + +def test_vmrun_delete_snapshot(): + """Test delete_snapshot method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'deleteSnapshot', '/tmp/first/some.vmx', 'snap1'] + got = vmrun.delete_snapshot('snap1') + assert got == expected + + +def test_vmrun_revert_to_snapshot(): + """Test revert_to_snapshot method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'revertToSnapshot', '/tmp/first/some.vmx', 'snap2'] + got = vmrun.revert_to_snapshot('snap2') + assert got == expected + + +def test_vmrun_list_network_adapters(): + """Test list_network_adapters method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'listNetworkAdapters', '/tmp/first/some.vmx'] + got = vmrun.list_network_adapters('snap2') + assert got == expected + + +def test_vmrun_add_network_adapter(): + """Test add_network_adapter method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'addNetworkAdapter', '/tmp/first/some.vmx', 'a_type'] + got = vmrun.add_network_adapter('a_type') + assert got == expected + + +def test_vmrun_set_network_adapter(): + """Test set_network_adapter method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'setNetworkAdapter', + '/tmp/first/some.vmx', 'some_index', 'a_type'] + got = vmrun.set_network_adapter('some_index', 'a_type') + assert got == expected + + +def test_vmrun_delete_network_adapter(): + """Test delete_network_adapter method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'deleteNetworkAdapter', '/tmp/first/some.vmx', 'a_type'] + got = vmrun.delete_network_adapter('a_type') + assert got == expected + + +def test_vmrun_list_networks(): + """Test list_host_networks method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'listHostNetworks'] + got = vmrun.list_host_networks() + assert got == expected + + +def test_vmrun_list_port_forwardings(): + """Test list_port_forwardings method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'listPortForwardings', 'a_host_network'] + got = vmrun.list_port_forwardings('a_host_network') + assert got == expected + + +def test_vmrun_set_port_forwarding(): + """Test set_port_forwarding method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'setPortForwarding', + 'a_host_network', 'a_protocol', 'a_host_port', 'a_guest_ip', 'a_guest_port'] + got = vmrun.set_port_forwarding('a_host_network', 'a_protocol', + 'a_host_port', 'a_guest_ip', 'a_guest_port') + assert got == expected + + +def test_vmrun_delete_port_forwarding(): + """Test delete_port_forwarding method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'deletePortForwarding', + 'a_host_network', 'a_protocol', 'a_host_port'] + got = vmrun.delete_port_forwarding('a_host_network', 'a_protocol', 'a_host_port') + assert got == expected + + +def test_vmrun_run_program_in_guest(): + """Test run_program_in_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'runProgramInGuest', '/tmp/first/some.vmx', + 'program_path', 'one_cmd'] + got = vmrun.run_program_in_guest('program_path', ['one_cmd']) + assert got == expected + + +def test_vmrun_run_program_in_guest_non_defaults(): + """Test run_program_in_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'runProgramInGuest', '/tmp/first/some.vmx', + '-noWait', '-activateWindow', '-interactive', 'program_path', 'one_cmd'] + got = vmrun.run_program_in_guest('program_path', ['one_cmd'], wait=False, + activate_window=True, interactive=True) + assert got == expected + + +def test_vmrun_set_shared_folder_state(): + """Test set_shared_folder_state method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'setSharedFolderState', '/tmp/first/some.vmx', + 'a_share_name', 'a_new_path', 'readonly'] + got = vmrun.set_shared_folder_state('a_share_name', 'a_new_path') + assert got == expected + + +def test_vmrun_add_shared_folder(): + """Test add_shared_folder method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'addSharedFolder', '/tmp/first/some.vmx', + 'a_share_name', 'a_path'] + got = vmrun.add_shared_folder('a_share_name', 'a_path') + assert got == expected + + +def test_vmrun_remove_shared_folder(): + """Test remove_shared_folder method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'removeSharedFolder', '/tmp/first/some.vmx', + 'a_share_name'] + got = vmrun.remove_shared_folder('a_share_name') + assert got == expected + + +def test_vmrun_enable_shared_folders(): + """Test enable_shared_folders method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'enableSharedFolders', '/tmp/first/some.vmx'] + got = vmrun.enable_shared_folders() + assert got == expected + + +def test_vmrun_disable_shared_folders(): + """Test disable_shared_folders method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'disableSharedFolders', '/tmp/first/some.vmx'] + got = vmrun.disable_shared_folders() + assert got == expected + + +def test_vmrun_list_processes_in_guest(): + """Test list_processes_in_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'listProcessesInGuest', '/tmp/first/some.vmx'] + got = vmrun.list_processes_in_guest() + assert got == expected + + +def test_vmrun_kill_process_in_guest(): + """Test kill_process_in_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'killProcessInGuest', '/tmp/first/some.vmx', 'a_pid'] + got = vmrun.kill_process_in_guest('a_pid') + assert got == expected + + +def test_vmrun_run_script_in_guest(): + """Test run_script_in_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'runScriptInGuest', '/tmp/first/some.vmx', + 'a_path', 'a_script'] + got = vmrun.run_script_in_guest('a_path', 'a_script') + assert got == expected + + +def test_vmrun_delete_file_in_guest(): + """Test delete_file_in_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'deleteFileInGuest', '/tmp/first/some.vmx', 'a_file'] + got = vmrun.delete_file_in_guest('a_file') + assert got == expected + + +def test_vmrun_create_directory_in_guest(): + """Test create_directory_in_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'createDirectoryInGuest', '/tmp/first/some.vmx', 'a_dir'] + got = vmrun.create_directory_in_guest('a_dir') + assert got == expected + + +def test_vmrun_delete_directory_in_guest(): + """Test delete_directory_in_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'deleteDirectoryInGuest', '/tmp/first/some.vmx', 'a_dir'] + got = vmrun.delete_directory_in_guest('a_dir') + assert got == expected + + +def test_vmrun_create_tempfile_in_guest(): + """Test create_tempfile_in_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'createTempfileInGuest', '/tmp/first/some.vmx'] + got = vmrun.create_tempfile_in_guest() + assert got == expected + + +def test_vmrun_list_directory_in_guest(): + """Test list_directory_in_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'listDirectoryInGuest', '/tmp/first/some.vmx', 'a_path'] + got = vmrun.list_directory_in_guest('a_path') + assert got == expected + + +def test_vmrun_copy_file_from_host_to_guest(): + """Test copy_file_from_host_to_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'copyFileFromHostToGuest', '/tmp/first/some.vmx', + 'a_host_path', 'a_guest_path'] + got = vmrun.copy_file_from_host_to_guest('a_host_path', 'a_guest_path') + assert got == expected + + +def test_vmrun_copy_file_from_guest_to_host(): + """Test copy_file_from_guest_to_host method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'copyFileFromGuestToHost', '/tmp/first/some.vmx', + 'a_guest_path', 'a_host_path'] + got = vmrun.copy_file_from_guest_to_host('a_guest_path', 'a_host_path') + assert got == expected + + +def test_vmrun_rename_file_in_guest(): + """Test rename_file_in_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'renameFileInGuest', '/tmp/first/some.vmx', + 'orig', 'new'] + got = vmrun.rename_file_in_guest('orig', 'new') + assert got == expected + + +def test_vmrun_type_keystrokes_in_guest(): + """Test type_keystrokes_in_guest method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'typeKeystrokesInGuest', '/tmp/first/some.vmx', + 'some_keys'] + got = vmrun.type_keystrokes_in_guest('some_keys') + assert got == expected + + +def test_vmrun_connect_named_device(): + """Test connect_named_device method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'connectNamedDevice', '/tmp/first/some.vmx', + 'a_device_name'] + got = vmrun.connect_named_device('a_device_name') + assert got == expected + + +def test_vmrun_capture_screen(): + """Test capture_screen method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'captureScreen', '/tmp/first/some.vmx', + 'a_path_on_host'] + got = vmrun.capture_screen('a_path_on_host') + assert got == expected + + +def test_vmrun_write_variable(): + """Test write_variable method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'writeVariable', '/tmp/first/some.vmx', + 'a_name', 'a_value'] + got = vmrun.write_variable('a_name', 'a_value') + assert got == expected + + +def test_vmrun_read_variable(): + """Test read_variable method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'readVariable', '/tmp/first/some.vmx', 'a_name'] + got = vmrun.read_variable('a_name') + assert got == expected + + +@patch('mech.vmrun.VMrun.vmrun', return_value='192.168.1.200') +def test_vmrun_get_guest_ip_address_no_lookup(mock_vmrun): + """Test get_guest_ip_address method without lookup.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', + executable='/tmp/vmrun', provider='ws') + got = vmrun.get_guest_ip_address() + assert got == '192.168.1.200' + mock_vmrun.assert_called() + + +@patch('mech.vmrun.VMrun.vmrun', return_value='unknown') +def test_vmrun_get_guest_ip_address_no_lookup_unknown(mock_vmrun): + """Test get_guest_ip_address method without lookup.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', + executable='/tmp/vmrun', provider='ws') + got = vmrun.get_guest_ip_address() + assert got == '' + mock_vmrun.assert_called() + + +@patch('mech.vmrun.VMrun.copy_file_from_guest_to_host', return_value='') +@patch('mech.vmrun.VMrun.run_script_in_guest', return_value='') +def test_vmrun_get_guest_ip_address_lookup(mock_run_script_in_guest, + mock_copy_file_from_guest_to_host): + """Test get_guest_ip_address method with lookup.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', user='auser', password='apass', + executable='/tmp/vmrun', provider='ws') + a_mock = mock_open() + a_mock = mock_open(read_data='192.168.1.201') + with patch('builtins.open', a_mock, create=True): + got = vmrun.get_guest_ip_address(lookup=True) + assert got == '192.168.1.201' + a_mock.assert_called() + mock_run_script_in_guest.assert_called() + mock_copy_file_from_guest_to_host.assert_called() + + +def test_vmrun_list(): + """Test list method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'list', '/tmp/first/some.vmx'] + got = vmrun.list() + assert got == expected + + +def test_vmrun_upgradevm(): + """Test upgradevm method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'upgradevm', '/tmp/first/some.vmx'] + got = vmrun.upgradevm() + assert got == expected + + +def test_vmrun_install_tools(): + """Test install_tools method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'installTools', '/tmp/first/some.vmx'] + got = vmrun.install_tools() + assert got == expected + + +def test_vmrun_check_tools_state(): + """Test check_tools_state method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'checkToolsState', '/tmp/first/some.vmx'] + got = vmrun.check_tools_state() + assert got == expected + + +def test_vmrun_register(): + """Test register method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'register', '/tmp/first/some.vmx'] + got = vmrun.register() + assert got == expected + + +def test_vmrun_unregister(): + """Test unregister method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'unregister', '/tmp/first/some.vmx'] + got = vmrun.unregister() + assert got == expected + + +def test_vmrun_list_registered_vm(): + """Test list_registered_vm method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'listRegisteredVM', '/tmp/first/some.vmx'] + got = vmrun.list_registered_vm() + assert got == expected + + +def test_vmrun_delete_vm(): + """Test delete_vm method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'deleteVM', '/tmp/first/some.vmx'] + got = vmrun.delete_vm() + assert got == expected + + +def test_vmrun_clone(): + """Test clone method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'clone', '/tmp/first/some.vmx', + 'a_dest_vmx', 'a_mode'] + got = vmrun.clone('a_dest_vmx', 'a_mode') + assert got == expected + + +def test_vmrun_begin_recording(): + """Test begin_recording method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'beginRecording', '/tmp/first/some.vmx', 'a_name'] + got = vmrun.begin_recording('a_name') + assert got == expected + + +def test_vmrun_end_recording(): + """Test end_recording method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'endRecording', '/tmp/first/some.vmx'] + got = vmrun.end_recording() + assert got == expected + + +def test_vmrun_begin_replay(): + """Test begin_replay method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'beginReplay', '/tmp/first/some.vmx', 'a_name'] + got = vmrun.begin_replay('a_name') + assert got == expected + + +def test_vmrun_end_replay(): + """Test end_replay method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'endReplay', '/tmp/first/some.vmx'] + got = vmrun.end_replay() + assert got == expected + + +def test_vmrun_vprobe_version(): + """Test vprobe_version method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'vprobeVersion', '/tmp/first/some.vmx'] + got = vmrun.vprobe_version() + assert got == expected + + +def test_vmrun_vprobe_load(): + """Test vprobe_load method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'vprobeLoad', '/tmp/first/some.vmx', 'a_script'] + got = vmrun.vprobe_load('a_script') + assert got == expected + + +def test_vmrun_vprobe_load_file(): + """Test vprobe_load_file method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'vprobeLoadFile', '/tmp/first/some.vmx', 'a_script'] + got = vmrun.vprobe_load_file('a_script') + assert got == expected + + +def test_vmrun_vprobe_reset(): + """Test vprobe_reset method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'vprobeReset', '/tmp/first/some.vmx'] + got = vmrun.vprobe_reset() + assert got == expected + + +def test_vmrun_vprobe_list_probes(): + """Test vprobe_list_probes method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'vprobeListProbes', '/tmp/first/some.vmx'] + got = vmrun.vprobe_list_probes() + assert got == expected + + +def test_vmrun_vprobe_list_globals(): + """Test vprobe_list_globals method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + expected = ['/tmp/vmrun', '-T', 'ws', 'vprobeListGlobals', '/tmp/first/some.vmx'] + got = vmrun.vprobe_list_globals() + assert got == expected + + +@patch('mech.vmrun.VMrun.check_tools_state', return_value='') +def test_vmrun_installed_tools_not_running(mock_tools_state): + """Test installed_tools method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + assert not vmrun.check_tools_state() + + +@patch('mech.vmrun.VMrun.check_tools_state', return_value='running') +def test_vmrun_installed_tools_running(mock_tools_state): + """Test installed_tools method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws', test_mode=True) + assert vmrun.check_tools_state() diff --git a/mech/utils.py b/mech/utils.py index 613a969..0bca057 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -2,6 +2,7 @@ # # Copyright (c) 2016-2017 Kevin Chung # Copyright (c) 2018 German Mendez Bravo (Kronuz) +# Copyright (c) 2020 Mike Kinney # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -22,10 +23,14 @@ # IN THE SOFTWARE. # +"""Mech utility functions.""" + from __future__ import division, absolute_import import os import re +import random +import string import sys import json import tarfile @@ -38,48 +43,35 @@ from shutil import copyfile import requests -from filelock import Timeout, FileLock -from clint.textui import colored, puts_err +from clint.textui import colored from clint.textui import progress -from .compat import raw_input, b2s +from .vmrun import VMrun +from .compat import b2s, PY3, raw_input + +LOGGER = logging.getLogger(__name__) -logger = logging.getLogger(__name__) +def main_dir(): + """Return the main directory.""" + return os.getcwd() -HOME = os.path.expanduser('~/.mech') -DATA_DIR = os.path.join(HOME, 'data') + +def mech_dir(): + """Return the mech directory.""" + return os.path.join(main_dir(), '.mech') def makedirs(name, mode=0o777): + """Make directories with mode supplied.""" try: os.makedirs(name, mode) except OSError: pass -def uncomment(text): - def e(m): - return '\x00%02x' % ord(m.group(1)) - e.re = re.compile(r'\\(.)', re.DOTALL | re.MULTILINE) - - def r(m): - s = m.group(0) - if s.startswith('/'): - return '' - if s.startswith(','): - return s[1:] - return s - r.re = re.compile(r'//.*?$|/\*.*?\*/|\'.*?\'|".*?"|,\s*?(?:}|])', re.DOTALL | re.MULTILINE) - - def u(m): - return '\\%s' % chr(int(m.group(1), 16)) - u.re = re.compile(r'\x00(..)', re.DOTALL | re.MULTILINE) - - return u.re.sub(u, r.re.sub(r, e.re.sub(e, text))) - - def confirm(prompt, default='y'): + """Confirmation prompt.""" default = default.lower() if default not in ['y', 'n']: default = 'y' @@ -87,44 +79,79 @@ def confirm(prompt, default='y'): prompt = prompt + ' ' + choicebox + ' ' while True: - input = raw_input(prompt).strip() - if input == '': + some_input = raw_input(prompt).strip() + if some_input == '': if default == 'y': return True else: return False - if re.match('y(?:es)?', input, re.IGNORECASE): + if re.match('y(?:es)?', some_input, re.IGNORECASE): return True - elif re.match('n(?:o)?', input, re.IGNORECASE): + elif re.match('n(?:o)?', some_input, re.IGNORECASE): return False -def save_mechfile(mechfile, path): - with open(os.path.join(path, 'Mechfile'), 'w+') as fp: - json.dump(mechfile, fp, sort_keys=True, indent=2, separators=(',', ': ')) +def save_mechfile_entry(mechfile_entry, name, mechfile_should_exist=False): + """Save the entry to the Mechfile.""" + LOGGER.debug('mechfile_entry:%s name:%s mechfile_should_exist:%s', + mechfile_entry, name, mechfile_should_exist) + mechfile = load_mechfile(mechfile_should_exist) + + mechfile[name] = mechfile_entry + + LOGGER.debug("after adding name:%s mechfile:%s", name, mechfile) + return save_mechfile(mechfile) + + +def remove_mechfile_entry(name, mechfile_should_exist=True): + """Removed the entry from the Mechfile.""" + LOGGER.debug('name:%s mechfile_should_exist:%s', name, mechfile_should_exist) + mechfile = load_mechfile(mechfile_should_exist) + + if mechfile.get(name): + del mechfile[name] + + LOGGER.debug("after removing name:%s mechfile:%s", name, mechfile) + return save_mechfile(mechfile) + + +def save_mechfile(mechfile): + """Save the mechfile object (which is a dict) to a file called 'Mechfile'. + Return True if save was successful. + """ + LOGGER.debug('mechfile:%s', mechfile) + with open(os.path.join(main_dir(), 'Mechfile'), 'w+') as the_file: + json.dump(mechfile, the_file, sort_keys=True, indent=2, separators=(',', ': ')) return True def locate(path, glob): - for root, dirnames, filenames in os.walk(path): + """Locate a file in the path provided.""" + for root, _, filenames in os.walk(path): for filename in filenames: if fnmatch.fnmatch(filename, glob): return os.path.abspath(os.path.join(root, filename)) def parse_vmx(path): + """Parse the virtual machine configuration (.vmx) file and return an + ordered dictionary. + """ vmx = collections.OrderedDict() - with open(path) as fp: - for line in fp: + with open(path) as the_file: + for line in the_file: line = line.strip().split('=', 1) if len(line) > 1: vmx[line[0].rstrip()] = line[1].lstrip() return vmx -def update_vmx(path): +def update_vmx(path, numvcpus=None, memsize=None, no_nat=False): + """Update the virtual machine configuration (.vmx) + file with desired settings. + """ updated = False vmx = parse_vmx(path) @@ -139,14 +166,24 @@ def update_vmx(path): if not has_network: vmx["ethernet0.addresstype"] = "generated" vmx["ethernet0.bsdname"] = "en0" - vmx["ethernet0.connectiontype"] = "nat" + if not no_nat: + vmx["ethernet0.connectiontype"] = "nat" vmx["ethernet0.displayname"] = "Ethernet" vmx["ethernet0.linkstatepropagation.enable"] = "FALSE" vmx["ethernet0.pcislotnumber"] = "32" vmx["ethernet0.present"] = "TRUE" vmx["ethernet0.virtualdev"] = "e1000" vmx["ethernet0.wakeonpcktrcv"] = "FALSE" - puts_err(colored.yellow("Added network interface to vmx file")) + print(colored.yellow("Added network interface to vmx file")) + updated = True + + # write out vmx file if memsize or numvcpus was specified + if numvcpus is not None: + vmx["numvcpus"] = '"{}"'.format(numvcpus) + updated = True + + if memsize is not None: + vmx["memsize"] = '"{}"'.format(memsize) updated = True if updated: @@ -156,170 +193,140 @@ def update_vmx(path): row = "{} = {}".format(key, value) new_vmx.write(row + os.linesep) - # puts_err(colored.yellow("Upgrading VM...")) - # vmrun = VMrun(path) - # vmrun.upgradevm() +def load_mechfile(should_exist=True): + """Load the Mechfile from disk and return the mechfile as a dictionary.""" + mechfile_fullpath = os.path.join(main_dir(), 'Mechfile') + LOGGER.debug("mechfile_fullpath:%s", mechfile_fullpath) + if os.path.isfile(mechfile_fullpath): + with open(mechfile_fullpath) as the_file: + try: + mechfile = json.loads(the_file.read()) + LOGGER.debug('mechfile:%s', mechfile) + return mechfile + except ValueError: + print(colored.red("Invalid Mechfile." + os.linesep)) + return {} + else: + if should_exist: + sys.exit(colored.red(textwrap.fill( + "Could not find a Mechfile in the current directory. " + "A Mech environment is required to run this command. Run `mech init` " + "to create a new Mech environment. Or specify the name of the VM you would " + "like to start with `mech up `. A final option is to change to a " + "directory with a Mechfile and to try again."))) + else: + return {} -def instances(): - makedirs(DATA_DIR) - index_path = os.path.join(DATA_DIR, 'index') - index_lock = os.path.join(DATA_DIR, 'index.lock') - try: - with FileLock(index_lock, timeout=3): - updated = False - if os.path.exists(index_path): - with open(index_path) as fp: - instances = json.loads(uncomment(fp.read())) - # prune unexistent Mechfiles - for k in list(instances): - instance_data = instances[k] - path = instance_data and instance_data.get('path') - if not path or not os.path.exists(os.path.join(path, 'Mechfile')): - del instances[k] - updated = True - else: - instances = {} - if updated: - with open(index_path, 'w') as fp: - json.dump(instances, fp, sort_keys=True, indent=2, separators=(',', ': ')) - return instances - except Timeout: - puts_err(colored.red(textwrap.fill("Couldn't access index, it seems locked."))) - sys.exit(1) - - -def settle_instance(instance_name, obj=None, force=False): - makedirs(DATA_DIR) - index_path = os.path.join(DATA_DIR, 'index') - index_lock = os.path.join(DATA_DIR, 'index.lock') - try: - with FileLock(index_lock, timeout=3): - updated = False - if os.path.exists(index_path): - with open(index_path) as fp: - instances = json.loads(uncomment(fp.read())) - # prune unexistent Mechfiles - for k in list(instances): - instance_data = instances[k] - path = instance_data and instance_data.get('path') - if not path or not os.path.exists(os.path.join(path, 'Mechfile')): - del instances[k] - updated = True - else: - instances = {} - instance_data = instances.get(instance_name) - if not instance_data or force: - if obj: - instance_data = instances[instance_name] = obj - updated = True - else: - instance_data = {} - if updated: - with open(index_path, 'w') as fp: - json.dump(instances, fp, sort_keys=True, indent=2, separators=(',', ': ')) - return instance_data - except Timeout: - puts_err(colored.red(textwrap.fill("Couldn't access index, it seems locked."))) - sys.exit(1) - - -def load_mechfile(pwd): - while pwd: - mechfile = os.path.join(pwd, 'Mechfile') - if os.path.isfile(mechfile): - with open(mechfile) as fp: - try: - return json.loads(uncomment(fp.read())) - except ValueError: - puts_err(colored.red("Invalid Mechfile." + os.linesep)) - break - new_pwd = os.path.basename(pwd) - pwd = None if new_pwd == pwd else new_pwd - puts_err(colored.red(textwrap.fill( - "Couldn't find a Mechfile in the current directory any deeper directories. " - "A Mech environment is required to run this command. Run `mech init` " - "to create a new Mech environment. Or specify the name of the VM you'd " - "like to start with `mech up `. A final option is to change to a " - "directory with a Mechfile and to try again." - ))) - sys.exit(1) - - -def build_mechfile(descriptor, name=None, version=None, requests_kwargs={}): - mechfile = {} - if descriptor is None: - return mechfile - if any(descriptor.startswith(s) for s in ('https://', 'http://', 'ftp://')): - mechfile['url'] = descriptor + +def default_shared_folders(): + """Return the default shared folders config. + The host_path value of "../.." is because it is relative to the vmx file. + """ + return [{'share_name': 'mech', 'host_path': '../..'}] + + +def build_mechfile_entry(location, box=None, name=None, box_version=None, + shared_folders=None): + """Build the Mechfile from the inputs.""" + LOGGER.debug("location:%s name:%s box:%s box_version:%s", location, name, box, box_version) + mechfile_entry = {} + + if location is None: + return mechfile_entry + + mechfile_entry['name'] = name + mechfile_entry['box'] = box + mechfile_entry['box_version'] = box_version + + if shared_folders is None: + shared_folders = default_shared_folders() + mechfile_entry['shared_folders'] = shared_folders + + if any(location.startswith(s) for s in ('https://', 'http://', 'ftp://')): if not name: - name = os.path.splitext(os.path.basename(descriptor))[0] - mechfile['box'] = name - if version: - mechfile['box_version'] = version - return mechfile - elif descriptor.startswith('file:') or os.path.isfile(re.sub(r'^file:(?://)?', '', descriptor)): - descriptor = re.sub(r'^file:(?://)?', '', descriptor) + name = 'first' + mechfile_entry['url'] = location + return mechfile_entry + + elif location.startswith('file:') or os.path.isfile(re.sub(r'^file:(?://)?', '', location)): + if not name: + name = 'first' + location = re.sub(r'^file:(?://)?', '', location) + LOGGER.debug('location:%s', location) + mechfile_entry['file'] = location try: - with open(descriptor) as fp: - catalog = json.loads(uncomment(fp.read())) - except Exception: - mechfile['file'] = descriptor - if not name: - name = os.path.splitext(os.path.basename(descriptor))[0] - mechfile['box'] = name - if version: - mechfile['box_version'] = version - return mechfile + # see if the location/file is a json file + with open(location) as the_file: + # if an exception is not thrown, then set values and continue + # to the end of the function + catalog = json.loads(the_file.read()) + LOGGER.debug('catalog:%s', catalog) + except (json.decoder.JSONDecodeError, ValueError) as e: + # this means the location/file is probably a .box file + # or the json is invalid + LOGGER.debug('mechfile_entry:%s', mechfile_entry) + LOGGER.debug(e) + return mechfile_entry + except IOError: + # cannot open file + sys.exit('Error: Cannot open file:({})'.format(location)) else: try: - account, box, v = (descriptor.split('/', 2) + ['', ''])[:3] + account, box, ver = (location.split('/', 2) + ['', ''])[:3] if not account or not box: - puts_err(colored.red("Provided box name is not valid")) - if v: - version = v - puts_err(colored.blue("Loading metadata for box '{}'{}".format(descriptor, " ({})".format(version) if version else ""))) + sys.exit(colored.red("Provided box name is not valid")) + if ver: + box_version = ver + print( + colored.blue("Loading metadata for box '{}'{}".format( + location, " ({})".format(box_version) if box_version else ""))) url = 'https://app.vagrantup.com/{}/boxes/{}'.format(account, box) - r = requests.get(url, **requests_kwargs) - r.raise_for_status() - catalog = r.json() + response = requests.get(url) + response.raise_for_status() + catalog = response.json() except (requests.HTTPError, ValueError) as exc: - puts_err(colored.red("Bad response from HashiCorp's Vagrant Cloud API: %s" % exc)) - sys.exit(1) + sys.exit(colored.red("Bad response from HashiCorp's Vagrant Cloud API: %s" % exc)) except requests.ConnectionError: - puts_err(colored.red("Couldn't connect to HashiCorp's Vagrant Cloud API")) - sys.exit(1) - return catalog_to_mechfile(catalog, name, version) + sys.exit(colored.red("Couldn't connect to HashiCorp's Vagrant Cloud API")) + LOGGER.debug("catalog:%s name:%s box_version:%s", catalog, name, box_version) + return catalog_to_mechfile(catalog, name=name, box=box, box_version=box_version) -def catalog_to_mechfile(catalog, name=None, version=None): + +def catalog_to_mechfile(catalog, name=None, box=None, box_version=None): + """Convert the Hashicorp cloud catalog entry to Mechfile entry.""" + LOGGER.debug('catalog:%s name:%s box:%s box_version:%s', catalog, name, box, box_version) mechfile = {} versions = catalog.get('versions', []) - for v in versions: - current_version = v['version'] - if not version or current_version == version: - for provider in v['providers']: + for ver in versions: + current_version = ver['version'] + if not box_version or current_version == box_version: + for provider in ver['providers']: if 'vmware' in provider['name']: + mechfile['name'] = name mechfile['box'] = catalog['name'] mechfile['box_version'] = current_version mechfile['url'] = provider['url'] + mechfile['shared_folders'] = default_shared_folders() return mechfile - puts_err(colored.red("Couldn't find a VMWare compatible VM for '{}'{}".format(name, " ({})".format(version) if version else ""))) - sys.exit(1) + sys.exit(colored.red("Couldn't find a VMWare compatible VM using catalog:{}".format(catalog))) def tar_cmd(*args, **kwargs): + """Build the tar command to be used to extract the box.""" try: startupinfo = None if os.name == "nt": startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.SW_HIDE | subprocess.STARTF_USESHOWWINDOW - proc = subprocess.Popen(['tar', '--help'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo) + proc = subprocess.Popen(['tar', '--help'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, startupinfo=startupinfo) except OSError: return None if proc.returncode: return None - stdoutdata, stderrdata = map(b2s, proc.communicate()) + stdoutdata, _ = map(b2s, proc.communicate()) tar = ['tar'] if kwargs.get('wildcards') and re.search(r'--wildcards\b', stdoutdata): tar.append('--wildcards') @@ -331,108 +338,172 @@ def tar_cmd(*args, **kwargs): return tar -def init_box(name, version, force=False, save=True, requests_kwargs={}): - if not locate('.mech', '*.vmx'): - name_version_box = add_box(name, name=name, version=version, force=force, save=save, requests_kwargs=requests_kwargs) +def init_box(name, box=None, box_version=None, location=None, force=False, save=True, + instance_path=None, numvcpus=None, memsize=None, no_nat=False): + """Initialize the box. This includes uncompressing the files + from the box file and updating the vmx file with + desired settings. Return the full path to the vmx file. + """ + LOGGER.debug("name:%s box:%s box_version:%s location:%s", name, box, box_version, location) + if not locate(instance_path, '*.vmx'): + name_version_box = add_box( + name=name, + box=box, + box_version=box_version, + location=location, + force=force, + save=save) if not name_version_box: - puts_err(colored.red("Cannot find a valid box with a VMX file in it")) - sys.exit(1) - name, version, box = name_version_box - # box = locate(os.path.join(*filter(None, (HOME, 'boxes', name, version))), '*.box') + sys.exit(colored.red("Cannot find a valid box with a VMX file in it")) + + box_parts = box.split('/') + box_dir = os.path.join(*filter(None, (mech_dir(), 'boxes', + box_parts[0], box_parts[1], box_version))) + box_file = locate(box_dir, '*.box') - puts_err(colored.blue("Extracting box '{}'...".format(name))) - makedirs('.mech') + print(colored.blue("Extracting box '{}'...".format(box_file))) + makedirs(instance_path) if sys.platform == 'win32': - cmd = tar_cmd('-xf', box, force_local=True) + cmd = tar_cmd('-xf', box_file, force_local=True) else: - cmd = tar_cmd('-xf', box) + cmd = tar_cmd('-xf', box_file) if cmd: startupinfo = None if os.name == "nt": startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.SW_HIDE | subprocess.STARTF_USESHOWWINDOW - proc = subprocess.Popen(cmd, cwd='.mech', startupinfo=startupinfo) + proc = subprocess.Popen(cmd, cwd=instance_path, startupinfo=startupinfo) if proc.wait(): - puts_err(colored.red("Cannot extract box")) - sys.exit(1) + sys.exit(colored.red("Cannot extract box")) else: - tar = tarfile.open(box, 'r') - tar.extractall('.mech') + tar = tarfile.open(box_file, 'r') + tar.extractall(instance_path) if not save and box.startswith(tempfile.gettempdir()): os.unlink(box) - vmx = get_vmx() + vmx_path = locate(instance_path, '*.vmx') + if not vmx_path: + sys.exit(colored.red("Cannot locate a VMX file")) + + update_vmx(vmx_path, numvcpus=numvcpus, memsize=memsize, no_nat=no_nat) + return vmx_path + + +def add_box(name=None, box=None, box_version=None, location=None, + force=False, save=True): + """Add a box.""" + # build the dict + LOGGER.debug('name:%s box:%s box_version:%s location:%s', name, + box, box_version, location) + mechfile_entry = build_mechfile_entry( + box=box, + name=name, + location=location, + box_version=box_version) + + return add_mechfile( + mechfile_entry, + name=name, + box=box, + location=location, + box_version=box_version, + force=force, + save=save) + + +def add_mechfile(mechfile_entry, name=None, box=None, box_version=None, + location=None, force=False, save=True): + """Add a mechfile entry.""" + LOGGER.debug('mechfile_entry:%s name:%s box:%s box_version:%s location:%s', + mechfile_entry, name, box, box_version, location) + + box = mechfile_entry.get('box') + name = mechfile_entry.get('name') + box_version = mechfile_entry.get('box_version') + + url = mechfile_entry.get('url') + box_file = mechfile_entry.get('file') + + if box_file: + return add_box_file(box=box, box_version=box_version, filename=box_file, + force=force, save=save) - update_vmx(vmx) - - return vmx - - -def add_box(descriptor, name=None, version=None, force=False, save=True, requests_kwargs={}): - mechfile = build_mechfile(descriptor, name=name, version=version, requests_kwargs=requests_kwargs) - return add_mechfile(mechfile, name=name, version=version, force=force, save=save, requests_kwargs=requests_kwargs) - - -def add_mechfile(mechfile, name=None, version=None, force=False, save=True, requests_kwargs={}): - url = mechfile.get('url') - file = mechfile.get('file') - name = mechfile.get('box') - version = mechfile.get('box_version') - if file: - return add_box_file(name, version, file, force=force, save=save) if url: - return add_box_url(name, version, url, force=force, save=save, requests_kwargs=requests_kwargs) - puts_err(colored.red("Couldn't find a VMWare compatible VM for '{}'{}".format(name, " ({})".format(version) if version else ""))) + return add_box_url(name=name, box=box, box_version=box_version, + url=url, force=force, save=save) + print( + colored.red( + "Could not find a VMWare compatible VM for '{}'{}".format( + name, " ({})".format(box_version) if box_version else ""))) -def add_box_url(name, version, url, force=False, save=True, requests_kwargs={}): +def add_box_url(name, box, box_version, url, force=False, save=True): + """Add a box using the URL.""" + LOGGER.debug('name:%s box:%s box_version:%s url:%s', name, box, box_version, url) boxname = os.path.basename(url) - box = os.path.join(*filter(None, (HOME, 'boxes', name, version, boxname))) - exists = os.path.exists(box) + box_parts = box.split('/') + first_box_part = box_parts[0] + second_box_part = '' + if len(box_parts) > 1: + second_box_part = box_parts[1] + box_dir = os.path.join(*filter(None, (mech_dir(), 'boxes', + first_box_part, second_box_part, box_version))) + exists = os.path.exists(box_dir) if not exists or force: if exists: - puts_err(colored.blue("Attempting to download box '{}'...".format(name))) + print(colored.blue("Attempting to download box '{}'...".format(box))) else: - puts_err(colored.blue("Box '{}' could not be found. Attempting to download...".format(name))) + print(colored.blue("Box '{}' could not be found. " + "Attempting to download...".format(box))) try: - puts_err(colored.blue("URL: {}".format(url))) - r = requests.get(url, stream=True, **requests_kwargs) - r.raise_for_status() + print(colored.blue("URL: {}".format(url))) + response = requests.get(url, stream=True) + response.raise_for_status() try: - length = int(r.headers['content-length']) + length = int(response.headers['content-length']) progress_args = dict(expected_size=length // 1024 + 1) progress_type = progress.bar except KeyError: progress_args = dict(every=1024 * 100) progress_type = progress.dots - fp = tempfile.NamedTemporaryFile(delete=False) + the_file = tempfile.NamedTemporaryFile(delete=False) try: - for chunk in progress_type(r.iter_content(chunk_size=1024), label="{} ".format(boxname), **progress_args): + for chunk in progress_type( + response.iter_content( + chunk_size=1024), + label="{} ".format(boxname), + **progress_args): if chunk: - fp.write(chunk) - fp.close() - if r.headers.get('content-type') == 'application/json': + the_file.write(chunk) + the_file.close() + if response.headers.get('content-type') == 'application/json': # Downloaded URL might be a Vagrant catalog if it's json: - catalog = json.load(fp.name) - mechfile = catalog_to_mechfile(catalog, name, version) - return add_mechfile(mechfile, name=name, version=version, force=force, save=save, requests_kwargs=requests_kwargs) + catalog = json.load(the_file.name) + mechfile = catalog_to_mechfile(catalog, name, box, box_version) + return add_mechfile( + mechfile, + name=name, + box_version=box_version, + force=force, + save=save) else: # Otherwise it must be a valid box: - return add_box_file(name, version, fp.name, url=url, force=force, save=save) + return add_box_file(box=box, box_version=box_version, + filename=the_file.name, url=url, force=force, + save=save) finally: - os.unlink(fp.name) + os.unlink(the_file.name) except requests.HTTPError as exc: - puts_err(colored.red("Bad response: %s" % exc)) - sys.exit(1) + sys.exit(colored.red("Bad response: %s" % exc)) except requests.ConnectionError: - puts_err(colored.red("Couldn't connect to '%s'" % url)) - sys.exit(1) - return name, version, box + sys.exit(colored.red("Couldn't connect to '%s'" % url)) + return name, box_version, box -def add_box_file(name, version, filename, url=None, force=False, save=True): - puts_err(colored.blue("Checking box '{}' integrity...".format(name))) +def add_box_file(box=None, box_version=None, filename=None, url=None, force=False, save=True): + """Add a box using a file as the source. Returns box and box_version.""" + print(colored.blue("Checking box '{}' integrity filename:{}...".format(box, filename))) if sys.platform == 'win32': cmd = tar_cmd('-tf', filename, '*.vmx', wildcards=True, fast_read=True, force_local=True) @@ -454,126 +525,532 @@ def add_box_file(name, version, filename, url=None, force=False, save=True): valid_tar = True break if i.startswith('/') or i.startswith('..'): - puts_err(colored.red(textwrap.fill( - "This box is comprised of filenames starting with '/' or '..' " - "Exiting for the safety of your files." - ))) - sys.exit(1) + sys.exit(colored.red(textwrap.fill( + "This box is comprised of filenames starting with '/' or '..' " + "Exiting for the safety of your files."))) if valid_tar: if save: boxname = os.path.basename(url if url else filename) - box = os.path.join(*filter(None, (HOME, 'boxes', name, version, boxname))) + box = os.path.join(*filter(None, (mech_dir(), 'boxes', box, box_version, boxname))) path = os.path.dirname(box) makedirs(path) if not os.path.exists(box) or force: copyfile(filename, box) else: box = filename - return name, version, box - - -def index_active_instance(instance_name): - path = os.getcwd() - instance = settle_instance(instance_name, { - 'path': path, - }) - if instance.get('path') != path: - puts_err(colored.red(textwrap.fill(( - "There is already a Mech box with the name '{}' at {}" - ).format(instance_name, instance.get('path'))))) - sys.exit(1) - return path - - -def init_mechfile(instance_name, descriptor, name=None, version=None, requests_kwargs={}): - if not instance_name: - instance_name = os.path.basename(os.getcwd()) - path = index_active_instance(instance_name) - mechfile = build_mechfile(descriptor, name=name, version=version, requests_kwargs=requests_kwargs) - mechfile['name'] = instance_name - return save_mechfile(mechfile, path) - - -def get_requests_kwargs(arguments): - requests_kwargs = {} - if arguments['--insecure']: - requests_kwargs['verify'] = False - elif arguments['--capath']: - requests_kwargs['verify'] = arguments['--capath'] - elif arguments['--cacert']: - requests_kwargs['verify'] = arguments['--cacert'] - elif arguments['--cert']: - requests_kwargs['cert'] = arguments['--cert'] - return requests_kwargs - - -def get_vmx(silent=False): - vmx = locate('.mech', '*.vmx') - if not vmx and not silent: - puts_err(colored.red("Cannot locate a VMX file")) - sys.exit(1) - return vmx + return box, box_version + + +def get_info_for_auth(mech_use=False): + """Get information (username/pub_key) for authentication.""" + username = os.getlogin() + pub_key = os.path.expanduser('~/.ssh/id_rsa.pub') + return {'auth': {'username': username, 'pub_key': pub_key, 'mech_use': mech_use}} + + +def init_mechfile(location=None, box=None, name=None, box_version=None, add_me=None, + use_me=None): + """Initialize the Mechfile.""" + LOGGER.debug("name:%s box:%s box_version:%s location:%s add_me:%s use_me:%s", + name, box, box_version, location, add_me, use_me) + mechfile_entry = build_mechfile_entry( + location=location, + box=box, + name=name, + box_version=box_version) + if add_me: + mechfile_entry.update(get_info_for_auth(use_me)) + LOGGER.debug('mechfile_entry:%s', mechfile_entry) + return save_mechfile_entry(mechfile_entry, name, mechfile_should_exist=False) + + +def add_to_mechfile(location=None, box=None, name=None, box_version=None, add_me=None, + use_me=None): + """Add entry to the Mechfile.""" + LOGGER.debug("name:%s box:%s box_version:%s location:%s add_me:%s use_me:%s", + name, box, box_version, location, add_me, use_me) + this_mech_entry = build_mechfile_entry( + location=location, + box=box, + name=name, + box_version=box_version) + if add_me: + this_mech_entry.update(get_info_for_auth(use_me)) + LOGGER.debug('this_mech_entry:%s', this_mech_entry) + return save_mechfile_entry(this_mech_entry, name, mechfile_should_exist=False) + + +def random_string(string_len=15): + """Generate a random string of fixed length.""" + letters = string.ascii_lowercase + return ''.join(random.choice(letters) for i in range(string_len)) + + +def add_auth(instance): + """Add authentication to VM.""" + + if not instance: + sys.exit(colored.red("Need to provide an instance to add_auth().")) + + if instance.vmx is None or instance.user is None or instance.password is None: + sys.exit(colored.red("Need to provide vmx/user/password to add_auth().")) + + print(colored.green('Adding auth to instance:{}'.format(instance.name))) + + vmrun = VMrun(instance.vmx, instance.user, instance.password) + # cannot run if vmware tools are not installed + if not vmrun.installed_tools(): + sys.exit(colored.red("Cannot add auth if VMware Tools are not installed.")) + + if instance.auth: + username = instance.auth.get('username', None) + pub_key = instance.auth.get('pub_key', None) + if username and pub_key: + with open(pub_key, 'r') as the_file: + pub_key_contents = the_file.read().strip() + if pub_key_contents: + # set the password to some random string + # user should never need it (sudo should not prompt for a + # password) + password = random_string() + cmd = ('sudo useradd -m -s /bin/bash -p "{password}" {username};' + 'sudo mkdir /home/{username}/.ssh;' + 'sudo usermod -aG sudo {username};' + 'echo "{username} ALL=(ALL) NOPASSWD: ALL" | ' + 'sudo tee -a /etc/sudoers;' + 'echo "{pub_key_contents}" | ' + 'sudo tee -a /home/{username}/.ssh/authorized_keys;' + 'sudo chmod 700 /home/{username}/.ssh;' + 'sudo chown {username}:{username} /home/{username}/.ssh;' + 'sudo chmod 600 /home/{username}/.ssh/authorized_keys;' + 'sudo chown {username}:{username} /home/{username}/.ssh/authorized_keys' + ).format(username=username, pub_key_contents=pub_key_contents, + password=password) + LOGGER.debug('cmd:', cmd) + results = vmrun.run_script_in_guest('/bin/sh', cmd, quiet=True) + LOGGER.debug('results:%s', results) + if results is None: + print(colored.red("Did not add auth")) + else: + print(colored.green("Added auth.")) + else: + print(colored.green("Could not read contents of the pub_key" + " file:{}".format(pub_key))) + else: + print(colored.blue("Warning: Need a username and pub_key in auth.")) + else: + print(colored.blue("No auth to add.")) + + +def ssh(instance, command, plain=None, extra=None): + """Run ssh command. + Note: May not really need the tempfile if self.use_psk==True. + Using the tempfile, there are options to not add host to the known_hosts files + which is useful, but could be MITM attacks. Not likely locally, but still + could be an issue. + """ + LOGGER.debug('command:%s plain:%s extra:%s', command, plain, extra) + if instance.created: + config_ssh = instance.config_ssh() + temp_file = tempfile.NamedTemporaryFile(delete=False) + try: + temp_file.write(config_ssh_string(config_ssh).encode('utf-8')) + temp_file.close() + + cmds = ['ssh'] + if not plain: + cmds.extend(('-F', temp_file.name)) + if extra: + cmds.extend(extra) + if not plain: + cmds.append(config_ssh['Host']) + if command: + cmds.extend(('--', command)) + + LOGGER.debug( + " ".join( + "'{}'".format( + c.replace( + "'", + "\\'")) if ' ' in c else c for c in cmds)) + + # if running a script + if command: + result = subprocess.run(cmds, capture_output=True) + stdout = result.stdout.decode('utf-8').strip() + stderr = result.stderr.decode('utf-8').strip() + return result.returncode, stdout, stderr + else: + # interactive + return subprocess.call(cmds), None, None + finally: + os.unlink(temp_file.name) + + +def scp(instance, src, dst, dst_is_host, extra=None): + """Run scp command. + Note: May not really need the tempfile if self.use_psk==True. + Using the tempfile, there are options to not add host to the known_hosts files + which is useful, but could be MITM attacks. Not likely locally, but still + could be an issue. + """ + if instance.created: + + config_ssh = instance.config_ssh() + temp_file = tempfile.NamedTemporaryFile(delete=False) + + try: + temp_file.write(config_ssh_string(config_ssh).encode()) + temp_file.close() + + cmds = ['scp'] + cmds.extend(('-F', temp_file.name)) + if extra: + cmds.extend(extra) + + host = config_ssh['Host'] + dst = '{}:{}'.format(host, dst) if dst_is_host else dst + src = '{}:{}'.format(host, src) if not dst_is_host else src + cmds.extend((src, dst)) + + LOGGER.debug( + " ".join( + "'{}'".format( + c.replace( + "'", + "\\'")) if ' ' in c else c for c in cmds)) + return subprocess.run(cmds, capture_output=True) + finally: + os.unlink(temp_file.name) + + +def del_user(instance, username): + """Delete a user in guest VM.""" + + if not instance: + sys.exit(colored.red("Need to provide an instance to del_user().")) + if instance.vmx is None: + sys.exit(colored.red("VM must be created.")) -def provision_file(vm, source, destination): - return vm.copyFileFromHostToGuest(source, destination) + if instance.user is None: + sys.exit(colored.red("A user is required.")) + print(colored.green('Removing username ({}) from instance:{}...'.format(username, + instance.name))) -def provision_shell(vm, inline, path, args=[]): - tmp_path = vm.createTempfileInGuest() + cmd = 'sudo userdel -fr vagrant' + LOGGER.debug('cmd:', cmd) + + if instance.use_psk: + ssh(instance, cmd) + else: + vmrun = VMrun(instance.vmx, user=instance.user, + password=instance.password, use_psk=instance.use_psk) + # cannot run if vmware tools are not installed + if not vmrun.installed_tools(): + sys.exit(colored.red("Cannot add del_user if VMware Tools are not installed.")) + results = vmrun.run_script_in_guest('/bin/sh', cmd, quiet=True) + LOGGER.debug('results:%s', results) + if results is None: + print(colored.red("Failed running del_user().")) + else: + print(colored.green("Success running del_user().")) + + +def provision(instance, show=False): + """Provision an instance. + + Args: + instance (MechInstance): an instance + show (bool): just print the provisioning + + Notes: + Valid provision types are: + file: copies files to instances + shell: executes scripts + + """ + + if not instance: + sys.exit(colored.red("Need to provide an instance to provision().")) + + if instance.vmx is None or instance.user is None: + sys.exit(colored.red("Need to provide vmx/user to provision().")) + + print(colored.green('Provisioning instance:{}'.format(instance.name))) + + vmrun = VMrun(instance.vmx, instance.user, instance.password) + # cannot run provisioning if vmware tools are not installed + if not vmrun.installed_tools(): + sys.exit(colored.red("Cannot provision if VMware Tools are not installed.")) + + provisioned = 0 + if instance.provision: + for i, pro in enumerate(instance.provision): + provision_type = pro.get('type') + if provision_type == 'file': + source = pro.get('source') + destination = pro.get('destination') + if show: + print(colored.green("instance:{} provision_type:{} source:{} " + "destination:{}".format(instance.name, provision_type, + source, destination))) + else: + results = provision_file(vmrun, instance, source, destination) + LOGGER.debug('results:%s', results) + if results is None: + print(colored.red("Not Provisioned")) + return + provisioned += 1 + + elif provision_type == 'shell': + inline = pro.get('inline') + path = pro.get('path') + + args = pro.get('args') + if not isinstance(args, list): + args = [args] + if show: + print(colored.green(" instance:{} provision_type:{} inline:{} path:{} " + "args:{}".format(instance.name, provision_type, + inline, path, args))) + else: + if provision_shell(vmrun, instance, inline, path, args) is None: + print(colored.red("Not Provisioned")) + return + provisioned += 1 + + else: + print(colored.red("Not Provisioned ({}".format(i))) + return + else: + print(colored.green("VM ({}) Provision {} " + "entries".format(instance.name, provisioned))) + else: + print(colored.blue("Nothing to provision")) + + +def provision_file(vmrun, instance, source, destination): + """Provision from file. + + Args: + vmrun (VMrun): instance of the VMrun class + source (str): full path of a file to copy + source (str): full path where the file is to be copied to + + Notes: + This function copies a file from host to guest. + + """ + print(colored.blue("Copying ({}) to ({})".format(source, destination))) + if instance.use_psk: + results = scp(instance, source, destination, True) + else: + results = vmrun.copy_file_from_host_to_guest(source, destination) + return results + + +def create_tempfile_in_guest(instance): + """Create a tempfile in the guest.""" + cmd = 'tmpfile=$(mktemp); echo $tmpfile' + _, stdout, _ = ssh(instance, cmd) + return stdout + + +def provision_shell(vmrun, instance, inline, script_path, args=None): + """Provision from shell. + + Args: + vmrun (VMrun): instance of the VMrun class + instance (MechInstance): instance of the MechInstance class + inline (bool): run the script inline + script_path (str): path to the script to run + args (list of str): arguments to the script + + """ + if args is None: + args = [] + if instance.use_psk: + tmp_path = create_tempfile_in_guest(instance) + else: + tmp_path = vmrun.create_tempfile_in_guest() + LOGGER.debug('inline:%s script_path:%s args:%s tmp_path:%s', + inline, script_path, args, tmp_path) if tmp_path is None: + print(colored.red("Warning: Could not create tempfile in guest.")) return try: - if path and os.path.isfile(path): - puts_err(colored.blue("Configuring script {}...".format(path))) - if vm.copyFileFromHostToGuest(path, tmp_path) is None: - return + if script_path and os.path.isfile(script_path): + print(colored.blue("Configuring script {}...".format(script_path))) + if instance.use_psk: + results = scp(instance, script_path, tmp_path, True) + if results is None: + print(colored.red("Warning: Could not copy file to guest.")) + return + else: + if vmrun.copy_file_from_host_to_guest(script_path, tmp_path) is None: + print(colored.red("Warning: Could not copy file to guest.")) + return else: - if path: - if any(path.startswith(s) for s in ('https://', 'http://', 'ftp://')): - puts_err(colored.blue("Downloading {}...".format(path))) + if script_path: + if any(script_path.startswith(s) for s in ('https://', 'http://', 'ftp://')): + print(colored.blue("Downloading {}...".format(script_path))) try: - r = requests.get(path) - r.raise_for_status() - inline = r.read() + response = requests.get(script_path) + response.raise_for_status() + inline = response.read() except requests.HTTPError: return except requests.ConnectionError: return else: - puts_err(colored.red("Cannot open {}".format(path))) + print(colored.red("Cannot open {}".format(script_path))) return if not inline: - puts_err(colored.red("No script to execute")) + print(colored.red("No script to execute")) return - puts_err(colored.blue("Configuring script...")) - fp = tempfile.NamedTemporaryFile(delete=False) + print(colored.blue("Configuring script to run inline...")) + the_file = tempfile.NamedTemporaryFile(delete=False) try: - fp.write(inline) - fp.close() - if vm.copyFileFromHostToGuest(fp.name, tmp_path) is None: - return + the_file.write(str.encode(inline)) + the_file.close() + if instance.use_psk: + scp(instance, the_file.name, tmp_path, True) + else: + if vmrun.copy_file_from_host_to_guest(the_file.name, tmp_path) is None: + return finally: - os.unlink(fp.name) - - puts_err(colored.blue("Configuring environment...")) - if vm.runScriptInGuest('/bin/sh', "chmod +x '{}'".format(tmp_path)) is None: - return + os.unlink(the_file.name) + + print(colored.blue("Configuring environment...")) + make_executable = "chmod +x '{}'".format(tmp_path) + LOGGER.debug('make_executable:%s', make_executable) + if instance.use_psk: + if ssh(instance, make_executable) is None: + print(colored.red("Warning: Could not configure script in the environment.")) + return + else: + if vmrun.run_script_in_guest('/bin/sh', make_executable) is None: + print(colored.red("Warning: Could not configure script in the environment.")) + return - puts_err(colored.blue("Executing program...")) - return vm.runProgramInGuest(tmp_path, args) + print(colored.blue("Executing program...")) + if instance.use_psk: + args_string = ' '.join([str(elem) for elem in args]) + LOGGER.debug('args:%s args_string:%s', args, args_string) + return ssh(instance, tmp_path, args_string) + else: + return vmrun.run_program_in_guest(tmp_path, args) finally: - vm.deleteFileInGuest(tmp_path, quiet=True) + if instance.use_psk: + return ssh(instance, 'rm -f "{}"'.format(tmp_path)) + else: + vmrun.delete_file_in_guest(tmp_path, quiet=True) def config_ssh_string(config_ssh): - ssh_config = "Host {}".format(config_ssh['Host']) + os.linesep - for k, v in config_ssh.items(): - if k != 'Host': - ssh_config += " {} {}".format(k, v) + os.linesep + """Build the ssh-config string from a dict holding the keys/values.""" + ssh_config = "Host {}".format(config_ssh.get('Host', '')) + os.linesep + for key, value in config_ssh.items(): + if key != 'Host': + ssh_config += " {} {}".format(key, value) + os.linesep return ssh_config + + +def share_folders(vmrun, inst): + """Share folders. + Args: + vmrun (VMrun): an instance of the VMrun class + inst (MechInstance): an instance of the MechInstance class (representing a vm) + + """ + print(colored.blue("Sharing folders...")) + vmrun.enable_shared_folders(quiet=False) + for share in inst.shared_folders: + share_name = share.get('share_name') + host_path = share.get('host_path') + print(colored.blue("share:{} host_path:{}".format(share_name, host_path))) + vmrun.add_shared_folder(share_name, host_path, quiet=True) + + +def get_fallback_executable(): + """Get a fallback executable for the command line tool 'vmrun'.""" + if 'PATH' in os.environ: + LOGGER.debug("os.environ['PATH']:%s", os.environ['PATH']) + for path in os.environ['PATH'].split(os.pathsep): + vmrun = os.path.join(path, 'vmrun') + if os.path.exists(vmrun): + return vmrun + vmrun = os.path.join(path, 'vmrun.exe') + if os.path.exists(vmrun): + return vmrun + return None + + +def get_darwin_executable(): + """Get the full path for the 'vmrun' command on a mac host.""" + vmrun = '/Applications/VMware Fusion.app/Contents/Library/vmrun' + if os.path.exists(vmrun): + return vmrun + return get_fallback_executable() + + +def get_win32_executable(): + """Get the full path for the 'vmrun' command on a Windows host.""" + if PY3: + import winreg + else: + import _winreg as winreg + reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + try: + key = winreg.OpenKey(reg, 'SOFTWARE\\VMware, Inc.\\VMware Workstation') + try: + return os.path.join(winreg.QueryValueEx(key, 'InstallPath')[0], 'vmrun.exe') + finally: + winreg.CloseKey(key) + except WindowsError: + key = winreg.OpenKey(reg, 'SOFTWARE\\WOW6432Node\\VMware, Inc.\\VMware Workstation') + try: + return os.path.join(winreg.QueryValueEx(key, 'InstallPath')[0], 'vmrun.exe') + finally: + winreg.CloseKey(key) + finally: + reg.Close() + return get_fallback_executable() + + +def get_provider(vmrun_executable): + """ + Identifies the right hosttype for vmrun command (ws | fusion | player) + """ + + if sys.platform == 'darwin': + return 'fusion' + + for provider in ['ws', 'player', 'fusion']: + # To determine the provider, try + # running the vmrun command to see which one works. + try: + startupinfo = None + if os.name == "nt": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.SW_HIDE | subprocess.STARTF_USESHOWWINDOW + proc = subprocess.Popen([vmrun_executable, + '-T', + provider, + 'list'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + startupinfo=startupinfo) + except OSError: + pass + + map(b2s, proc.communicate()) + if proc.returncode == 0: + return provider diff --git a/mech/vmrun.py b/mech/vmrun.py index 9ac492f..5a08410 100644 --- a/mech/vmrun.py +++ b/mech/vmrun.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (c) 2018 German Mendez Bravo (Kronuz) +# Copyright (c) 2020 Mike Kinney # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -20,6 +21,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # +"""VMrun class""" from __future__ import absolute_import @@ -29,91 +31,48 @@ import subprocess import tempfile -from .compat import PY3, b2s - -logger = logging.getLogger(__name__) - - -def get_fallback_executable(): - if 'PATH' in os.environ: - for path in os.environ['PATH'].split(os.pathsep): - vmrun = os.path.join(path, 'vmrun') - if os.path.exists(vmrun): - return vmrun - vmrun = os.path.join(path, 'vmrun.exe') - if os.path.exists(vmrun): - return vmrun - - -def get_darwin_executable(): - vmrun = '/Applications/VMware Fusion.app/Contents/Library/vmrun' - if os.path.exists(vmrun): - return vmrun - return get_fallback_executable() - - -def get_win32_executable(): - if PY3: - import winreg - else: - import _winreg as winreg - reg = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - try: - key = winreg.OpenKey(reg, 'SOFTWARE\\VMware, Inc.\\VMware Workstation') - try: - return os.path.join(winreg.QueryValueEx(key, 'InstallPath')[0], 'vmrun.exe') - finally: - winreg.CloseKey(key) - except WindowsError: - key = winreg.OpenKey(reg, 'SOFTWARE\\WOW6432Node\\VMware, Inc.\\VMware Workstation') - try: - return os.path.join(winreg.QueryValueEx(key, 'InstallPath')[0], 'vmrun.exe') - finally: - winreg.CloseKey(key) - finally: - reg.Close() - return get_fallback_executable() - -def get_provider(vmrun_exe): - """ - identifies the right hosttype for vmrun command (ws | fusion | player) - """ +from .compat import b2s +from . import utils - if sys.platform == 'darwin': - return 'fusion' - - for provider in ['ws', 'player', 'fusion']: - try: - startupinfo = None - if os.name == "nt": - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.SW_HIDE | subprocess.STARTF_USESHOWWINDOW - proc = subprocess.Popen([vmrun_exe, '-T', provider, 'list'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo) - except OSError: - pass - stdoutdata, stderrdata = map(b2s, proc.communicate()) - if proc.returncode == 0: - return provider +LOGGER = logging.getLogger(__name__) -class VMrun(object): - if sys.platform == 'darwin': - default_executable = get_darwin_executable() - elif sys.platform == 'win32': - default_executable = get_win32_executable() - else: - default_executable = get_fallback_executable() - default_provider = get_provider(default_executable) +class VMrun(): # pylint: disable=too-many-public-methods + """Interface class for the 'vmrun' command. + The 'vmrun' command is used to interact with VMware. + To add/update vmware functionality, run the 'vmrun' command with '--help'. + """ - def __init__(self, vmx_file=None, user=None, password=None, executable=None, provider=None): + def __init__(self, vmx_file=None, # pylint: disable=too-many-arguments + user=None, password=None, executable=None, provider=None, + test_mode=False, use_psk=False): + """Constructor - set sane defaults.""" self.vmx_file = vmx_file self.user = user self.password = password - self.executable = executable or self.default_executable - self.provider = provider or self.default_provider + self.executable = executable + self.provider = provider + self.use_psk = use_psk + + if self.executable is None: + if sys.platform == 'darwin': + self.executable = utils.get_darwin_executable() + elif sys.platform == 'win32': + self.executable = utils.get_win32_executable() + else: + self.executable = utils.get_fallback_executable() + if self.provider is None: + if self.executable is not None: + self.provider = utils.get_provider(self.executable) + LOGGER.debug('self.executable:%s self.provider:%s', + self.executable, self.provider) + # If test_mode is True, then do not perform the action + # just return the command info + self.test_mode = test_mode def vmrun(self, cmd, *args, **kwargs): + """Execute a 'vmrun' command.""" quiet = kwargs.pop('quiet', False) arguments = kwargs.pop('arguments', ()) @@ -130,26 +89,38 @@ def vmrun(self, cmd, *args, **kwargs): cmds.extend(filter(None, args)) cmds.extend(filter(None, arguments)) - logger.debug(" ".join("'{}'".format(c.replace("'", "\\'")) if ' ' in c else c for c in cmds)) + LOGGER.debug( + " ".join( + "'{}'".format( + c.replace( + "'", + "\\'")) if ' ' in c else c for c in cmds)) + + if self.test_mode: + return cmds startupinfo = None if os.name == "nt": startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.SW_HIDE | subprocess.STARTF_USESHOWWINDOW - proc = subprocess.Popen(cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo) + proc = subprocess.Popen( + cmds, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + startupinfo=startupinfo) stdoutdata, stderrdata = map(b2s, proc.communicate()) if stderrdata and not quiet: - logger.error(stderrdata.strip()) - logger.debug("(⏎ %s)" % proc.returncode) + LOGGER.error(stderrdata.strip()) + LOGGER.debug("(⏎ %s)", proc.returncode) if not proc.returncode: stdoutdata = stdoutdata.strip() - logger.debug(repr(stdoutdata)) + LOGGER.debug(repr(stdoutdata)) return stdoutdata if stdoutdata and not quiet: - logger.error(stdoutdata.strip()) + LOGGER.error(stdoutdata.strip()) ############################################################################ # POWER COMMANDS PARAMETERS DESCRIPTION @@ -212,19 +183,28 @@ def unpause(self, quiet=False): # Snapshot name # - def listSnapshots(self, show_tree=False, quiet=False): + def list_snapshots(self, show_tree=False, quiet=False): '''List all snapshots in a VM''' - return self.vmrun('listSnapshots', self.vmx_file, 'showTree' if show_tree else None, quiet=quiet) + return self.vmrun( + 'listSnapshots', + self.vmx_file, + 'showTree' if show_tree else None, + quiet=quiet) def snapshot(self, snap_name, quiet=False): '''Create a snapshot of a VM''' return self.vmrun('snapshot', self.vmx_file, snap_name, quiet=quiet) - def deleteSnapshot(self, snap_name, and_delete_children=False, quiet=False): + def delete_snapshot(self, snap_name, and_delete_children=False, quiet=False): '''Remove a snapshot from a VM''' - return self.vmrun('deleteSnapshot', self.vmx_file, snap_name, 'andDeleteChildren' if and_delete_children else None, quiet=quiet) - - def revertToSnapshot(self, snap_name, quiet=False): + return self.vmrun( + 'deleteSnapshot', + self.vmx_file, + snap_name, + 'andDeleteChildren' if and_delete_children else None, + quiet=quiet) + + def revert_to_snapshot(self, snap_name, quiet=False): '''Set VM state to a snapshot''' return self.vmrun('revertToSnapshot', self.vmx_file, snap_name, quiet=quiet) @@ -248,19 +228,30 @@ def revertToSnapshot(self, snap_name, quiet=False): # deleteNetworkAdapter Path to vmx file Remove a network adapter on a VM # Network adapter index - def listNetworkAdapters(self, quiet=False): + def list_network_adapters(self, quiet=False): '''List all network adapters in a VM''' return self.vmrun('listNetworkAdapters', self.vmx_file, quiet=quiet) - def addNetworkAdapter(self, adapter_type, host_network=None, quiet=False): + def add_network_adapter(self, adapter_type, host_network=None, quiet=False): '''Add a network adapter on a VM''' - return self.vmrun('addNetworkAdapter', self.vmx_file, adapter_type, host_network, quiet=quiet) - - def setNetworkAdapter(self, adapter_index, adapter_type, host_network=None, quiet=False): + return self.vmrun( + 'addNetworkAdapter', + self.vmx_file, + adapter_type, + host_network, + quiet=quiet) + + def set_network_adapter(self, adapter_index, adapter_type, host_network=None, quiet=False): '''Update a network adapter on a VM''' - return self.vmrun('setNetworkAdapter', self.vmx_file, adapter_index, adapter_type, host_network, quiet=quiet) - - def deleteNetworkAdapter(self, adapter_index, quiet=False): + return self.vmrun( + 'setNetworkAdapter', + self.vmx_file, + adapter_index, + adapter_type, + host_network, + quiet=quiet) + + def delete_network_adapter(self, adapter_index, quiet=False): '''Remove a network adapter on a VM''' return self.vmrun('deleteNetworkAdapter', self.vmx_file, adapter_index, quiet=quiet) @@ -269,10 +260,12 @@ def deleteNetworkAdapter(self, adapter_index, quiet=False): # --------------------- ---------- ----------- # listHostNetworks List all networks in the host # - # listPortForwardings Host network name List all available port forwardings on a host network + # listPortForwardings Host network name List all available port + # forwardings on a host network # # - # setPortForwarding Host network name Add or update a port forwarding on a host network + # setPortForwarding Host network name Add or update a port + # forwarding on a host network # Protocol # Host port # Guest ip @@ -283,19 +276,36 @@ def deleteNetworkAdapter(self, adapter_index, quiet=False): # Protocol # Host port - def listHostNetworks(self, quiet=False): + def list_host_networks(self, quiet=False): '''List all networks in the host''' return self.vmrun('listHostNetworks', quiet=quiet) - def listPortForwardings(self, host_network, quiet=False): + def list_port_forwardings(self, host_network, quiet=False): '''List all available port forwardings on a host network''' return self.vmrun('listPortForwardings', host_network, quiet=quiet) - def setPortForwarding(self, host_network, protocol, host_port, guest_ip, guest_port, description=None, quiet=False): + def set_port_forwarding( # pylint: disable=too-many-arguments + self, + host_network, + protocol, + host_port, + guest_ip, + guest_port, + description=None, + quiet=False): '''Add or update a port forwarding on a host network''' - return self.vmrun('setPortForwarding', host_network, protocol, host_port, guest_ip, guest_port, description, quiet=quiet) - - def deletePortForwarding(self, host_network, protocol, host_port, quiet=False): + return self.vmrun( + 'setPortForwarding', + host_network, + protocol, + host_port, + guest_ip, + guest_port, + description, + quiet=quiet) + + def delete_port_forwarding(self, host_network, protocol, host_port, + quiet=False): # pylint: disable=too-many-arguments '''Delete a port forwarding on a host network''' return self.vmrun('deletePortForwarding', host_network, protocol, host_port, quiet=quiet) @@ -396,123 +406,180 @@ def deletePortForwarding(self, host_network, protocol, host_port, quiet=False): # [-wait] # - def runProgramInGuest(self, program_path, program_arguments=[], wait=True, activate_window=False, interactive=False, quiet=False): - return self.vmrun('runProgramInGuest', self.vmx_file, None if wait else '-noWait', '-activateWindow' if activate_window else None, '-interactive' if interactive else None, program_path, arguments=program_arguments, quiet=quiet) - - def fileExistsInGuest(self, file, quiet=False): - '''Check if a file exists in Guest OS''' - return 'not' not in self.execute('fileExistsInGuest', self.vmx_file, file) - - def directoryExistsInGuest(self, path, quiet=False): - '''Check if a directory exists in Guest OS''' - return 'not' not in self.execute('directoryExistsInGuest', self.vmx_file, path) - - def setSharedFolderState(self, share_name, new_path, mode='readonly', quiet=False): + def run_program_in_guest( # pylint: disable=too-many-arguments + self, + program_path, + program_arguments=None, + wait=True, + activate_window=False, + interactive=False, + quiet=False): + """Run a program in the guest vm.""" + if program_arguments is None: + program_arguments = [] + return self.vmrun( + 'runProgramInGuest', + self.vmx_file, + None if wait else '-noWait', + '-activateWindow' if activate_window else None, + '-interactive' if interactive else None, + program_path, + arguments=program_arguments, + quiet=quiet) + + def set_shared_folder_state(self, share_name, new_path, mode='readonly', quiet=False): '''Modify a Host-Guest shared folder''' - return self.vmrun('setSharedFolderState', self.vmx_file, share_name, new_path, mode, quiet=quiet) - - def addSharedFolder(self, share_name, host_path, quiet=False): + return self.vmrun( + 'setSharedFolderState', + self.vmx_file, + share_name, + new_path, + mode, + quiet=quiet) + + def add_shared_folder(self, share_name, host_path, quiet=False): '''Add a Host-Guest shared folder''' return self.vmrun('addSharedFolder', self.vmx_file, share_name, host_path, quiet=quiet) - def removeSharedFolder(self, share_name, quiet=False): + def remove_shared_folder(self, share_name, quiet=False): '''Remove a Host-Guest shared folder''' return self.vmrun('removeSharedFolder', self.vmx_file, share_name, quiet=quiet) - def enableSharedFolders(self, runtime=None, quiet=False): + def enable_shared_folders(self, runtime=None, quiet=False): + '''Enable shared folders.''' return self.vmrun('enableSharedFolders', self.vmx_file, runtime, quiet=quiet) - def disableSharedFolders(self, runtime=None, quiet=False): + def disable_shared_folders(self, runtime=None, quiet=False): '''Disable shared folders in Guest''' return self.vmrun('disableSharedFolders', self.vmx_file, runtime, quiet=quiet) - def listProcessesInGuest(self, quiet=False): + def list_processes_in_guest(self, quiet=False): '''List running processes in Guest OS''' return self.vmrun('listProcessesInGuest', self.vmx_file, quiet=quiet) - def killProcessInGuest(self, pid, quiet=False): + def kill_process_in_guest(self, pid, quiet=False): '''Kill a process in Guest OS''' return self.vmrun('killProcessInGuest', self.vmx_file, pid, quiet=quiet) - def runScriptInGuest(self, interpreter_path, script, wait=True, activate_window=False, interactive=False, quiet=False): + def run_script_in_guest( # pylint: disable=too-many-arguments + self, + interpreter_path, + script, + wait=True, + activate_window=False, + interactive=False, + quiet=False): '''Run a script in Guest OS''' - return self.vmrun('runScriptInGuest', self.vmx_file, interpreter_path, script, None if wait else '-noWait', '-activateWindow' if activate_window else None, '-interactive' if interactive else None, quiet=quiet) - - def deleteFileInGuest(self, file, quiet=False): + if self.use_psk: + # TODO call utils.ssh() + pass + else: + return self.vmrun( + 'runScriptInGuest', + self.vmx_file, + interpreter_path, + script, + None if wait else '-noWait', + '-activateWindow' if activate_window else None, + '-interactive' if interactive else None, + quiet=quiet) + + def delete_file_in_guest(self, filename, quiet=False): '''Delete a file in Guest OS''' - return self.vmrun('deleteFileInGuest', self.vmx_file, file, quiet=quiet) + return self.vmrun('deleteFileInGuest', self.vmx_file, filename, quiet=quiet) - def createDirectoryInGuest(self, path, quiet=False): + def create_directory_in_guest(self, path, quiet=False): '''Create a directory in Guest OS''' return self.vmrun('createDirectoryInGuest', self.vmx_file, path, quiet=quiet) - def deleteDirectoryInGuest(self, path, quiet=False): + def delete_directory_in_guest(self, path, quiet=False): '''Delete a directory in Guest OS''' return self.vmrun('deleteDirectoryInGuest', self.vmx_file, path, quiet=quiet) - def createTempfileInGuest(self, quiet=False): + def create_tempfile_in_guest(self, quiet=False): '''Create a temporary file in Guest OS''' return self.vmrun('createTempfileInGuest', self.vmx_file, quiet=quiet) - def listDirectoryInGuest(self, path, quiet=False): + def list_directory_in_guest(self, path, quiet=False): '''List a directory in Guest OS''' return self.vmrun('listDirectoryInGuest', self.vmx_file, path, quiet=quiet) - def copyFileFromHostToGuest(self, host_path, guest_path, quiet=False): + def copy_file_from_host_to_guest(self, host_path, guest_path, quiet=False): '''Copy a file from host OS to guest OS''' - return self.vmrun('copyFileFromHostToGuest', self.vmx_file, host_path, guest_path, quiet=quiet) - - def copyFileFromGuestToHost(self, guest_path, host_path, quiet=False): + return self.vmrun( + 'copyFileFromHostToGuest', + self.vmx_file, + host_path, + guest_path, + quiet=quiet) + + def copy_file_from_guest_to_host(self, guest_path, host_path, quiet=False): '''Copy a file from guest OS to host OS''' - return self.vmrun('copyFileFromGuestToHost', self.vmx_file, guest_path, host_path, quiet=quiet) - - def renameFileInGuest(self, original_name, new_name, quiet=False): + return self.vmrun( + 'copyFileFromGuestToHost', + self.vmx_file, + guest_path, + host_path, + quiet=quiet) + + def rename_file_in_guest(self, original_name, new_name, quiet=False): '''Rename a file in Guest OS''' return self.vmrun('renameFileInGuest', self.vmx_file, original_name, new_name, quiet=quiet) - def typeKeystrokesInGuest(self, keystroke, quiet=False): + def type_keystrokes_in_guest(self, keystroke, quiet=False): '''Type Keystrokes in Guest OS''' return self.vmrun('typeKeystrokesInGuest', self.vmx_file, keystroke, quiet=quiet) - def connectNamedDevice(self, device_name, quiet=False): + def connect_named_device(self, device_name, quiet=False): '''Connect the named device in the Guest OS''' return self.vmrun('connectNamedDevice', self.vmx_file, device_name, quiet=quiet) - def disconnectNamedDevice(self, device_name, quiet=False): + def disconnect_named_device(self, device_name, quiet=False): '''Disconnect the named device in the Guest OS''' return self.vmrun('disconnectNamedDevice', self.vmx_file, device_name, quiet=quiet) - def captureScreen(self, path_on_host, quiet=False): + def capture_screen(self, path_on_host, quiet=False): '''Capture the screen of the VM to a local file''' return self.vmrun('captureScreen', self.vmx_file, path_on_host, quiet=quiet) - def writeVariable(self, var_name, var_value, mode=None, quiet=False): + def write_variable(self, var_name, var_value, mode=None, quiet=False): '''Write a variable in the VM state''' return self.vmrun('writeVariable', self.vmx_file, mode, var_name, var_value, quiet=quiet) - def readVariable(self, var_name, mode=None, quiet=False): + def read_variable(self, var_name, mode=None, quiet=False): '''Read a variable in the VM state''' return self.vmrun('readVariable', self.vmx_file, mode, var_name, quiet=quiet) - def getGuestIPAddress(self, wait=True, quiet=False, lookup=False): + def get_guest_ip_address(self, wait=True, quiet=False, lookup=False): '''Gets the IP address of the guest''' if lookup is True: - self.runScriptInGuest('/bin/sh', "ifconfig | grep -Eo 'inet (addr:)?([0-9]*\\.){3}[0-9]*' | grep -Eo '([0-9]*\\.){3}[0-9]*' | grep -v '127.0.0.1' > /tmp/ip_address", quiet=quiet) - fp = tempfile.NamedTemporaryFile(delete=False) + guest_tmp_filename = '/tmp/.ip_address' + cmd = "ifconfig | grep -Eo 'inet (addr:)?([0-9]*\\.){3}[0-9]*'" + cmd += "| grep -Eo '([0-9]*\\.){3}[0-9]*' | grep -v '127.0.0.1' > " + guest_tmp_filename + LOGGER.debug("cmd:%s", cmd) + self.run_script_in_guest('/bin/sh', cmd, quiet=quiet) + temp_file = tempfile.NamedTemporaryFile(delete=False) try: - fp.close() - self.copyFileFromGuestToHost('/tmp/ip_address', fp.name, quiet=quiet) - ip_addresses = open(fp.name).read().split() + temp_file.close() + self.copy_file_from_guest_to_host(guest_tmp_filename, temp_file.name, quiet=quiet) + ip_addresses = open(temp_file.name).read().split() + # clean up the guest tmp file + self.run_script_in_guest( + '/bin/sh', + "rm {}".format(guest_tmp_filename), + quiet=False) if ip_addresses: return ip_addresses[0] else: return None finally: - os.unlink(fp.name) - ip = self.vmrun('getGuestIPAddress', self.vmx_file, '-wait' if wait else None, quiet=quiet) - if ip == 'unknown': - ip = '' - return ip + os.unlink(temp_file.name) + else: + ip_address = self.vmrun('getGuestIPAddress', self.vmx_file, + '-wait' if wait else None, quiet=quiet) + if ip_address == 'unknown': + ip_address = '' + return ip_address ############################################################################ # GENERAL COMMANDS PARAMETERS DESCRIPTION @@ -538,14 +605,16 @@ def list(self, quiet=False): return self.vmrun('list', self.vmx_file, quiet=quiet) def upgradevm(self, quiet=False): - '''Upgrade VM file format, virtual hw''' + '''Upgrade VM file format, virtual hw. + Note: The vm must be stopped before running this command. + ''' return self.vmrun('upgradevm', self.vmx_file, quiet=quiet) - def installTools(self, quiet=False): + def install_tools(self, quiet=False): '''Install Tools in Guest OS''' return self.vmrun('installTools', self.vmx_file, quiet=quiet) - def checkToolsState(self, quiet=False): + def check_tools_state(self, quiet=False): '''Check the current Tools state''' return self.vmrun('checkToolsState', self.vmx_file, quiet=quiet) @@ -559,12 +628,12 @@ def unregister(self, quiet=False): '''Unregister a VM''' return self.vmrun('unregister', self.vmx_file, quiet=quiet) - def listRegisteredVM(self, quiet=False): + def list_registered_vm(self, quiet=False): # unavailable in VMware Fusion 10 (OS X)? '''List registered VMs''' return self.vmrun('listRegisteredVM', self.vmx_file, quiet=quiet) - def deleteVM(self, quiet=False): + def delete_vm(self, quiet=False): '''Delete a VM''' return self.vmrun('deleteVM', self.vmx_file, quiet=quiet) @@ -585,22 +654,22 @@ def clone(self, dest_vmx, mode, snap_name=None, quiet=False): # # endReplay Path to vmx file End replaying a VM - def beginRecording(self, snap_name, quiet=False): + def begin_recording(self, snap_name, quiet=False): # unavailable in VMware Fusion 10 (OS X)? '''Begin recording a VM''' return self.vmrun('beginRecording', self.vmx_file, snap_name, quiet=quiet) - def endRecording(self, quiet=False): + def end_recording(self, quiet=False): # unavailable in VMware Fusion 10 (OS X)? '''End recording a VM''' return self.vmrun('endRecording', self.vmx_file, quiet=quiet) - def beginReplay(self, snap_name, quiet=False): + def begin_replay(self, snap_name, quiet=False): # unavailable in VMware Fusion 10 (OS X)? '''Begin replaying a VM''' return self.vmrun('beginReplay', self.vmx_file, snap_name, quiet=quiet) - def endReplay(self, quiet=False): + def end_replay(self, quiet=False): # unavailable in VMware Fusion 10 (OS X)? '''End replaying a VM''' return self.vmrun('endReplay', self.vmx_file, quiet=quiet) @@ -622,38 +691,41 @@ def endReplay(self, quiet=False): # # vprobeListGlobals Path to vmx file List global variables - def vprobeVersion(self, quiet=False): + def vprobe_version(self, quiet=False): # unavailable in VMware Fusion 10 (OS X)? '''List VP version''' return self.vmrun('vprobeVersion', self.vmx_file, quiet=quiet) - def vprobeLoad(self, script, quiet=False): + def vprobe_load(self, script, quiet=False): # unavailable in VMware Fusion 10 (OS X)? '''Load VP script''' return self.vmrun('vprobeLoad', self.vmx_file, script, quiet=quiet) - def vprobeLoadFile(self, vp, quiet=False): + def vprobe_load_file(self, vprobe, quiet=False): # unavailable in VMware Fusion 10 (OS X)? '''Load VP file''' - return self.vmrun('vprobeLoadFile', self.vmx_file, vp, quiet=quiet) + return self.vmrun('vprobeLoadFile', self.vmx_file, vprobe, quiet=quiet) - def vprobeReset(self, quiet=False): + def vprobe_reset(self, quiet=False): # unavailable in VMware Fusion 10 (OS X)? '''Disable all vprobes''' return self.vmrun('vprobeReset', self.vmx_file, quiet=quiet) - def vprobeListProbes(self, quiet=False): + def vprobe_list_probes(self, quiet=False): # unavailable in VMware Fusion 10 (OS X)? '''List probes''' return self.vmrun('vprobeListProbes', self.vmx_file, quiet=quiet) - def vprobeListGlobals(self, quiet=False): + def vprobe_list_globals(self, quiet=False): # unavailable in VMware Fusion 10 (OS X)? '''List global variables''' return self.vmrun('vprobeListGlobals', self.vmx_file, quiet=quiet) ############################################################################ - def installedTools(self, quiet=False): - state = self.checkToolsState(quiet=quiet) + def installed_tools(self, quiet=False): + '''Return True if VMware tools is in either 'installed' or 'running', + otherwise, return False. + ''' + state = self.check_tools_state(quiet=quiet) return state in ('installed', 'running') diff --git a/mech_completion.sh b/mech_completion.sh new file mode 100644 index 0000000..27dabf1 --- /dev/null +++ b/mech_completion.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# +# bash completion file for mech +# +# This script provides completion of: +# - commands and their options +# +# To enable the completions either: +# - place this file in /etc/bash_completion.d +# or +# - copy this file to e.g. ~/.mech-completion.sh and add the line +# below to your .bashrc after bash completion features are loaded +# . ~/.mech-completion.sh +# +_mech() { + local cur prev opts base + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + # Options that will complete + opts="box destroy down global-status halt init ip list ls pause port provision ps reload resume scp snapshot stop ssh-config suspend up" + + # Complete the arguments to some of the basic commands. + case "${prev}" in + box) + COMPREPLY=( $( compgen -W 'add delete list ls remove' -- "$cur" ) ) + return 0 + ;; + destroy) + local commands="-f --force -h --help" + COMPREPLY=( $(compgen -W "${commands} $(get_instances)" -- ${cur}) ) + return 0 + ;; + down) + local commands="-f --force -h --help" + COMPREPLY=( $(compgen -W "${commands} $(get_instances)" -- ${cur}) ) + return 0 + ;; + global-status) + local commands="-h --help" + COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) ) + return 0 + ;; + init) + local commands="--box --box-version --force -h --help --name" + COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) ) + return 0 + ;; + ip) + local commands="-h --help" + COMPREPLY=( $(compgen -W "${commands} $(get_instances)" -- ${cur}) ) + return 0 + ;; + list) + local commands="-d --detail -h --help" + COMPREPLY=( $(compgen -W "${commands} $(get_instances)" -- ${cur}) ) + return 0 + ;; + pause) + local commands="-h --help" + COMPREPLY=( $(compgen -W "${commands} $(get_instances)" -- ${cur}) ) + return 0 + ;; + port) + local commands="--guest -h --help" + COMPREPLY=( $(compgen -W "${commands} $(get_instances)" -- ${cur}) ) + return 0 + ;; + provision) + local commands="-h --help -s --show" + COMPREPLY=( $(compgen -W "${commands} $(get_instances)" -- ${cur}) ) + return 0 + ;; + ps) + local commands="-h --help" + COMPREPLY=( $(compgen -W "${commands} $(get_instances)" -- ${cur}) ) + return 0 + ;; + reload) + local commands="-h --help" + COMPREPLY=( $(compgen -W "${commands} $(get_instances)" -- ${cur}) ) + return 0 + ;; + scp) + local commands="-h --help" + COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) ) + return 0 + ;; + snapshot) + COMPREPLY=( $( compgen -W 'delete list ls remove save' -- "$cur" ) ) + return 0 + ;; + suspend) + local commands="-h --help" + COMPREPLY=( $(compgen -W "${commands} $(get_instances)" -- ${cur}) ) + return 0 + ;; + up) + local commands="--disable-provisioning --disable-shared-folders --gui -h --help --memsize --no-cache --no-nat --numvcpus" + COMPREPLY=( $(compgen -W "${commands} $(get_instances)" -- ${cur}) ) + return 0 + ;; + *) + ;; + esac + + COMPREPLY=($(compgen -W "${opts}" -- ${cur})) + +} + +get_instances () { + echo $(mech list | awk '(NR > 1) { printf("%s ", $1) }') +} + +complete -F _mech mech +# vim: ft=bash sw=2 ts=2 et diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..3d09010 --- /dev/null +++ b/pylintrc @@ -0,0 +1,4 @@ +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=9 diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..8853bad --- /dev/null +++ b/pytest.ini @@ -0,0 +1,14 @@ +[pytest] + +addopts = -n3 -m "not int" + +# Temp tune this next line down to find the slow unittest(s). +# unittests should each all run well under a second. +# (timeout is in seconds) +timeout = 300 + +filterwarnings = + ignore::DeprecationWarning + +markers = + int: marks tests as slow (deselect with '-m "not int"') diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..6f7ac37 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[flake8] +ignore = W503 +max-line-length = 100 +exclude = .git,venv,build diff --git a/setup.py b/setup.py index 32799ed..cbceac3 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +"""Setup mech""" try: from setuptools import setup @@ -11,9 +12,10 @@ def read(fname): + """Read in a file.""" try: - with open(os.path.join(os.path.dirname(__file__), fname), "r") as fp: - return fp.read().strip() + with open(os.path.join(os.path.dirname(__file__), fname), "r") as a_file: + return a_file.read().strip() except IOError: return '' diff --git a/smoke_test b/smoke_test new file mode 100755 index 0000000..11e635c --- /dev/null +++ b/smoke_test @@ -0,0 +1,2 @@ +# convenience script to run the smoke integration test +pytest -m"int or not int" -vv -l mech/test_int_simple.py::test_int_smoke diff --git a/tests/int/README.md b/tests/int/README.md new file mode 100644 index 0000000..410eb55 --- /dev/null +++ b/tests/int/README.md @@ -0,0 +1,5 @@ +# Integration tests for mech + +This directory contains directories and files for integration testing. + +All tests are run under `pytest` command. diff --git a/tests/int/provision/Mechfile b/tests/int/provision/Mechfile new file mode 100644 index 0000000..2857ffc --- /dev/null +++ b/tests/int/provision/Mechfile @@ -0,0 +1,52 @@ +{ + "first": { + "box": "mrlesmithjr/alpine311", + "box_version": "1578437753", + "name": "first", + "url": "https://vagrantcloud.com/mrlesmithjr/boxes/alpine311/versions/1578437753/providers/vmware_desktop.box", + "provision": [ + { + "type": "file", + "source": "file1.txt", + "destination": "/tmp/file1.txt" + }, + { + "type": "file", + "source": "file2.txt", + "destination": "/tmp/file2.txt" + } + ] + }, + "second": { + "box": "mrlesmithjr/alpine311", + "box_version": "1578437753", + "name": "second", + "url": "https://vagrantcloud.com/mrlesmithjr/boxes/alpine311/versions/1578437753/providers/vmware_desktop.box", + "provision": [ + { + "type": "shell", + "path": "file1.sh", + "args": [ + "a=1", + "b=true" + ] + }, + { + "type": "shell", + "path": "file2.sh", + "args": [] + }, + { + "type": "shell", + "inline": "echo hello from inline" + } + ] + }, + "third": { + "box": "mrlesmithjr/alpine311", + "box_version": "1578437753", + "name": "third", + "url": "https://vagrantcloud.com/mrlesmithjr/boxes/alpine311/versions/1578437753/providers/vmware_desktop.box", + "provision": [] + } +} diff --git a/tests/int/provision/file1.sh b/tests/int/provision/file1.sh new file mode 100644 index 0000000..1419ae0 --- /dev/null +++ b/tests/int/provision/file1.sh @@ -0,0 +1 @@ +echo "hello from file1.sh" > /tmp/file1.sh.out diff --git a/tests/int/provision/file1.txt b/tests/int/provision/file1.txt new file mode 100644 index 0000000..f6904ef --- /dev/null +++ b/tests/int/provision/file1.txt @@ -0,0 +1 @@ +hello from file1.txt diff --git a/tests/int/provision/file2.sh b/tests/int/provision/file2.sh new file mode 100644 index 0000000..9488f12 --- /dev/null +++ b/tests/int/provision/file2.sh @@ -0,0 +1 @@ +echo "hello from file2.sh" > /tmp/file2.sh.out diff --git a/tests/int/provision/file2.txt b/tests/int/provision/file2.txt new file mode 100644 index 0000000..a68955d --- /dev/null +++ b/tests/int/provision/file2.txt @@ -0,0 +1 @@ +hello from file2.txt diff --git a/tests/int/provision_using_psk/file1.sh b/tests/int/provision_using_psk/file1.sh new file mode 100644 index 0000000..1419ae0 --- /dev/null +++ b/tests/int/provision_using_psk/file1.sh @@ -0,0 +1 @@ +echo "hello from file1.sh" > /tmp/file1.sh.out diff --git a/tests/int/provision_using_psk/file1.txt b/tests/int/provision_using_psk/file1.txt new file mode 100644 index 0000000..f6904ef --- /dev/null +++ b/tests/int/provision_using_psk/file1.txt @@ -0,0 +1 @@ +hello from file1.txt diff --git a/tests/int/provision_using_psk/file2.sh b/tests/int/provision_using_psk/file2.sh new file mode 100644 index 0000000..9488f12 --- /dev/null +++ b/tests/int/provision_using_psk/file2.sh @@ -0,0 +1 @@ +echo "hello from file2.sh" > /tmp/file2.sh.out diff --git a/tests/int/provision_using_psk/file2.txt b/tests/int/provision_using_psk/file2.txt new file mode 100644 index 0000000..a68955d --- /dev/null +++ b/tests/int/provision_using_psk/file2.txt @@ -0,0 +1 @@ +hello from file2.txt diff --git a/tests/int/provision_using_psk/readme.txt b/tests/int/provision_using_psk/readme.txt new file mode 100644 index 0000000..91cb288 --- /dev/null +++ b/tests/int/provision_using_psk/readme.txt @@ -0,0 +1,5 @@ +This test exercises mech provisioning using preshared key. + +file provisioning just copies over files + +shell provisioning executes the files/inline code diff --git a/tests/int/shared_folders/Mechfile b/tests/int/shared_folders/Mechfile new file mode 100644 index 0000000..4d689b7 --- /dev/null +++ b/tests/int/shared_folders/Mechfile @@ -0,0 +1,18 @@ +{ + "first": { + "box": "bento/ubuntu-18.04", + "box_version": "201912.04.0", + "name": "first", + "url": "https://vagrantcloud.com/bento/boxes/ubuntu-18.04/versions/201912.04.0/providers/vmware_desktop.box", + "shared_folders": [ + { + "share_name": "mech", + "host_path": "../.." + }, + { + "share_name": "mech2", + "host_path": "/tmp" + } + ] + } +}