From f1d55f50645dac116a69198b215b537217c104ae Mon Sep 17 00:00:00 2001 From: Takashi Ando Date: Sat, 4 May 2019 10:27:51 +0900 Subject: [PATCH 001/116] Fix OSError if mech destroy #57 --- mech/mech.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mech/mech.py b/mech/mech.py index afdcaae..6fc6bf2 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -720,7 +720,10 @@ def destroy(self, arguments): vmrun.stop(mode='hard', quiet=True) time.sleep(3) vmrun.deleteVM() - shutil.rmtree(mech_path) + if os.path.exists(mech_path): + shutil.rmtree(mech_path) + else: + logger.debug("{} was not found.".format(mech_path)) else: puts_err(colored.red("Deletion aborted")) else: From a67380ce2f650dbcb3b109aeea406b7fe616ae69 Mon Sep 17 00:00:00 2001 From: Takashi Ando Date: Sun, 9 Jun 2019 08:54:17 +0900 Subject: [PATCH 002/116] This fixes #59: json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes if was broken --- mech/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mech/utils.py b/mech/utils.py index 613a969..d6fb14b 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -187,6 +187,9 @@ def instances(): except Timeout: puts_err(colored.red(textwrap.fill("Couldn't access index, it seems locked."))) sys.exit(1) + except json.decoder.JSONDecodeError: + puts_err(colored.red(textwrap.fill("Index file seems broken. Try to remove {}.".format(index_path)))) + sys.exit(1) def settle_instance(instance_name, obj=None, force=False): @@ -222,6 +225,9 @@ def settle_instance(instance_name, obj=None, force=False): except Timeout: puts_err(colored.red(textwrap.fill("Couldn't access index, it seems locked."))) sys.exit(1) + except json.decoder.JSONDecodeError: + puts_err(colored.red(textwrap.fill("Index file seems broken. Try to remove {}.".format(index_path)))) + sys.exit(1) def load_mechfile(pwd): From 204c779f51cd9a91838b34b033c86acf7207d8e1 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Thu, 2 Jan 2020 20:57:50 -0800 Subject: [PATCH 003/116] show instance name when ssh-config command is run --- mech/mech.py | 56 ++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index afdcaae..34930ef 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -162,7 +162,7 @@ def config_ssh(self): f.write(INSECURE_PRIVATE_KEY) os.chmod(insecure_private_key, 0o400) config = { - "Host": DEFAULT_HOST, + "Host": self.instance_name, "User": self.user, "Port": "22", "UserKnownHostsFile": "/dev/null", @@ -371,7 +371,7 @@ def delete(self, arguments): name = arguments[''] instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, user=self.user, password=self.password) if vmrun.deleteSnapshot(name) is None: @@ -389,7 +389,7 @@ def list(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, user=self.user, password=self.password) print(vmrun.listSnapshots()) @@ -459,7 +459,7 @@ def save(self, arguments): name = arguments[''] instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, user=self.user, password=self.password) if vmrun.snapshot(name) is None: @@ -557,7 +557,7 @@ def init(self, arguments): url = arguments[''] name = None version = arguments['--box-version'] - instance_name = arguments['--name'] + self.instance_name = arguments['--name'] force = arguments['--force'] requests_kwargs = utils.get_requests_kwargs(arguments) @@ -569,7 +569,7 @@ def init(self, arguments): return puts_err(colored.green("Initializing mech")) - if utils.init_mechfile(instance_name, url, name=name, version=version, requests_kwargs=requests_kwargs): + if utils.init_mechfile(self.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!" @@ -600,9 +600,9 @@ def up(self, arguments): requests_kwargs = utils.get_requests_kwargs(arguments) instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) - utils.index_active_instance(instance_name) + utils.index_active_instance(self.instance_name) 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) @@ -653,7 +653,7 @@ def ps(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, self.user, self.password) print(vmrun.listProcessesInGuest()) @@ -668,7 +668,7 @@ def status(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, user=self.user, password=self.password) @@ -677,12 +677,12 @@ def status(self, arguments): ip = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) state = vmrun.checkToolsState(quiet=True) - print("Current machine states:" + os.linesep) + print("Current machine state:" + 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)) + print("%s\t%s\t%s\t(VMware Tools %s)" % (self.instance_name, box_name, ip, state)) if ip == "poweroff": print(os.linesep + "The VM is powered off. To restart the VM, simply run `mech up`") @@ -704,17 +704,17 @@ def destroy(self, arguments): force = arguments['--force'] instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) - if instance_name: - instance = utils.settle_instance(instance_name) + if self.instance_name: + instance = utils.settle_instance(self.instance_name) path = instance['path'] 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'): + if force or utils.confirm("Are you sure you want to delete {self.instance_name} at {path}".format(instance_name=self.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) @@ -739,7 +739,7 @@ def down(self, arguments): force = arguments['--force'] instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, user=self.user, password=self.password) if not force and vmrun.installedTools(): @@ -763,7 +763,7 @@ def pause(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, user=self.user, password=self.password) if vmrun.pause() is None: @@ -782,9 +782,9 @@ def resume(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) - utils.index_active_instance(instance_name) + utils.index_active_instance(self.instance_name) vmrun = VMrun(self.vmx, user=self.user, password=self.password) @@ -833,7 +833,7 @@ def suspend(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, user=self.user, password=self.password) if vmrun.suspend() is None: @@ -851,7 +851,7 @@ def ssh_config(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) print(utils.config_ssh_string(self.config_ssh)) @@ -871,7 +871,7 @@ def ssh(self, arguments): command = arguments['--command'] instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) config_ssh = self.config_ssh fp = tempfile.NamedTemporaryFile(delete=False) @@ -922,7 +922,7 @@ def scp(self, arguments): else: src = src_instance - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) config_ssh = self.config_ssh fp = tempfile.NamedTemporaryFile(delete=False) @@ -955,7 +955,7 @@ def ip(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, user=self.user, password=self.password) lookup = self.get("enable_ip_lookup", False) @@ -975,7 +975,7 @@ def provision(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, self.user, self.password) @@ -1025,7 +1025,7 @@ def reload(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, user=self.user, password=self.password) @@ -1061,7 +1061,7 @@ def port(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - instance_name = self.activate(instance_name) + self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, user=self.user, password=self.password) for network in vmrun.listHostNetworks().split('\n'): From 1180354c410b7e7abed77f42021f8bc1b6ea4d95 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 5 Jan 2020 11:29:06 -0800 Subject: [PATCH 004/116] add command line option to disable shared folder --- mech/mech.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index afdcaae..9fc4944 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -585,6 +585,7 @@ def up(self, arguments): Options: --gui Start GUI + --disable-shared-folder Do not share folder with VM --provision Enable provisioning --insecure Do not validate SSL certificates --cacert FILE CA certificate for SSL download @@ -596,6 +597,7 @@ def up(self, arguments): -h, --help Print this help """ gui = arguments['--gui'] + disable_shared_folder = not arguments['--disable-shared-folder'] save = not arguments['--no-cache'] requests_kwargs = utils.get_requests_kwargs(arguments) @@ -616,8 +618,9 @@ def up(self, arguments): 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 not disable_shared_folder: + vmrun.enableSharedFolders() + vmrun.addSharedFolder('mech', os.getcwd(), quiet=True) if ip: if started: puts_err(colored.green("VM started on {}".format(ip))) From ee1e270a7741ef937f632fbd64bc8921357b2118 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 8 Jan 2020 12:53:51 -0800 Subject: [PATCH 005/116] add ability to override vcpus/mem --- .gitignore | 2 ++ README.md | 7 +++++++ mech/mech.py | 8 +++++++- mech/utils.py | 15 ++++++++++++--- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 82132df..523f091 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ dist/ mech.egg-info/ .mech/ Mechfile +.*swp +venv/ diff --git a/README.md b/README.md index d4da221..60f088e 100644 --- a/README.md +++ b/README.md @@ -85,3 +85,10 @@ 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` diff --git a/mech/mech.py b/mech/mech.py index afdcaae..dc4e1c7 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -593,6 +593,8 @@ def up(self, arguments): --checksum CHECKSUM Checksum for the box --checksum-type TYPE Checksum type (md5, sha1, sha256) --no-cache Do not save the downloaded box + --memsize 1024 Specify the size of memory for VM + --numvcpus 1 Specify the number of vcpus for VM -h, --help Print this help """ gui = arguments['--gui'] @@ -604,7 +606,11 @@ def up(self, arguments): utils.index_active_instance(instance_name) - vmx = utils.init_box(self.box_name, self.box_version, requests_kwargs=requests_kwargs, save=save) + numvcpus = arguments['--numvcpus'] + memsize = arguments['--memsize'] + + vmx = utils.init_box(self.box_name, self.box_version, requests_kwargs=requests_kwargs, save=save, numvcpus=numvcpus, memsize=memsize) + vmrun = VMrun(vmx, user=self.user, password=self.password) puts_err(colored.blue("Bringing machine up...")) started = vmrun.start(gui=gui) diff --git a/mech/utils.py b/mech/utils.py index 613a969..c967ff2 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -124,7 +124,7 @@ def parse_vmx(path): return vmx -def update_vmx(path): +def update_vmx(path, numvcpus=None, memsize=None): updated = False vmx = parse_vmx(path) @@ -149,6 +149,15 @@ def update_vmx(path): puts_err(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: with open(path, 'w') as new_vmx: for key in vmx: @@ -331,7 +340,7 @@ def tar_cmd(*args, **kwargs): return tar -def init_box(name, version, force=False, save=True, requests_kwargs={}): +def init_box(name, version, force=False, save=True, requests_kwargs={}, numvcpus=None, memsize=None): if not locate('.mech', '*.vmx'): name_version_box = add_box(name, name=name, version=version, force=force, save=save, requests_kwargs=requests_kwargs) if not name_version_box: @@ -364,7 +373,7 @@ def init_box(name, version, force=False, save=True, requests_kwargs={}): vmx = get_vmx() - update_vmx(vmx) + update_vmx(vmx, numvcpus=numvcpus, memsize=memsize) return vmx From d461bbf2cd3fbb953121b87e96bc4899767f17ad Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 1 Feb 2020 10:31:36 -0800 Subject: [PATCH 006/116] add info on how to install from diff repo; update example --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 60f088e..5024014 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # mech -I made this because I don't like VirtualBox and I wanted to use vagrant +One of the authors made this because they don't like VirtualBox and wanted to use vagrant with VMWare Fusion but was too cheap to buy the Vagrant plugin. https://blog.kchung.co/mech-vagrant-with-vmware-integration-for-free/ @@ -44,7 +44,7 @@ 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 ``` @@ -63,6 +63,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 From 321fd37aa07727ee63505fa5d514aac48e8dce1f Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 1 Feb 2020 10:35:43 -0800 Subject: [PATCH 007/116] add info in readme about new up options --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 5024014..720500e 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,32 @@ 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: + --gui Start GUI + --disable-shared-folder Do not share folder with VM + --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) + --no-cache Do not save the downloaded box + --memsize 1024 Specify the size of memory for VM + --numvcpus 1 Specify the number of vcpus for VM + -h, --help Print this help + + +Example using mech: + + Initializing and using a machine from HashiCorp's Vagrant Cloud: mech init bento/ubuntu-18.04 From 578a811631ef844df1cf9f023061f22843d088c8 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 1 Feb 2020 11:08:58 -0800 Subject: [PATCH 008/116] flake8 fixes; add contributing file --- CONTRIBUTING.md | 35 ++++++++++++ mech/__main__.py | 1 + mech/command.py | 9 ++- mech/compat.py | 25 ++++++--- mech/mech.py | 49 +++++++++++++--- mech/utils.py | 93 ++++++++++++++++++++++++++----- mech/vmrun.py | 142 ++++++++++++++++++++++++++++++++++++++++------- setup.cfg | 4 ++ 8 files changed, 307 insertions(+), 51 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 setup.cfg diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8cc656f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# 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 + +# Create a virtualenv +virtualenv -p python3 venv + +# Activate the python virtual environment +source venv/bin/activate + +# install mech from this code +python setup.py install + +# if doing development +pip install flake8 + +# 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 +``` diff --git a/mech/__main__.py b/mech/__main__.py index c1840af..c7c7a47 100644 --- a/mech/__main__.py +++ b/mech/__main__.py @@ -24,6 +24,7 @@ from __future__ import absolute_import + def main(): try: import os diff --git a/mech/command.py b/mech/command.py index fa3172b..28531f2 100644 --- a/mech/command.py +++ b/mech/command.py @@ -33,16 +33,22 @@ NBSP = '__' + def cmd_usage(doc): 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 DocoptExit____init__(self, message=''): SystemExit.__init__(self, (message + '\n' + cmd_usage(self.usage)).strip()) + docopt.extras = docopt_extras docopt.DocoptExit.__init__ = DocoptExit____init__ @@ -83,7 +89,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) diff --git a/mech/compat.py b/mech/compat.py index d2d2007..6ff6d8d 100644 --- a/mech/compat.py +++ b/mech/compat.py @@ -38,7 +38,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 +64,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/mech.py b/mech/mech.py index 1926712..30bfe9b 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -94,7 +94,10 @@ def activate(self, instance_name=None): 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)))) + 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) @@ -102,7 +105,8 @@ def activate(self, instance_name=None): 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 + 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): @@ -176,7 +180,10 @@ def config_ssh(self): 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() + + def callback(pat): + return pat.group(1).upper() + k = re.sub(r' (\w)', callback, k) if k[0].islower(): k = k[0].upper() + k[1:] @@ -569,7 +576,12 @@ def init(self, arguments): return puts_err(colored.green("Initializing mech")) - if utils.init_mechfile(self.instance_name, url, name=name, version=version, requests_kwargs=requests_kwargs): + if utils.init_mechfile( + self.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!" @@ -611,7 +623,13 @@ def up(self, arguments): numvcpus = arguments['--numvcpus'] memsize = arguments['--memsize'] - vmx = utils.init_box(self.box_name, self.box_version, requests_kwargs=requests_kwargs, save=save, numvcpus=numvcpus, memsize=memsize) + vmx = utils.init_box( + self.box_name, + self.box_version, + requests_kwargs=requests_kwargs, + save=save, + numvcpus=numvcpus, + memsize=memsize) vmrun = VMrun(vmx, user=self.user, password=self.password) puts_err(colored.blue("Bringing machine up...")) @@ -696,7 +714,8 @@ def status(self, arguments): 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") + 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`") @@ -723,7 +742,9 @@ def destroy(self, arguments): 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 {self.instance_name} at {path}".format(instance_name=self.instance_name, path=path), default='n'): + if force or utils.confirm( + "Are you sure you want to delete {self.instance_name} at {path}".format( + instance_name=self.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) @@ -901,7 +922,12 @@ def ssh(self, arguments): if command: cmds.extend(('--', command)) - 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)) return subprocess.call(cmds) finally: os.unlink(fp.name) @@ -952,7 +978,12 @@ def scp(self, arguments): 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)) + logger.debug( + " ".join( + "'{}'".format( + c.replace( + "'", + "\\'")) if ' ' in c else c for c in cmds)) return subprocess.call(cmds) finally: os.unlink(fp.name) diff --git a/mech/utils.py b/mech/utils.py index 02041db..b352090 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -197,7 +197,10 @@ def instances(): puts_err(colored.red(textwrap.fill("Couldn't access index, it seems locked."))) sys.exit(1) except json.decoder.JSONDecodeError: - puts_err(colored.red(textwrap.fill("Index file seems broken. Try to remove {}.".format(index_path)))) + puts_err( + colored.red( + textwrap.fill( + "Index file seems broken. Try to remove {}.".format(index_path)))) sys.exit(1) @@ -235,7 +238,10 @@ def settle_instance(instance_name, obj=None, force=False): puts_err(colored.red(textwrap.fill("Couldn't access index, it seems locked."))) sys.exit(1) except json.decoder.JSONDecodeError: - puts_err(colored.red(textwrap.fill("Index file seems broken. Try to remove {}.".format(index_path)))) + puts_err( + colored.red( + textwrap.fill( + "Index file seems broken. Try to remove {}.".format(index_path)))) sys.exit(1) @@ -293,7 +299,11 @@ def build_mechfile(descriptor, name=None, version=None, requests_kwargs={}): 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 ""))) + puts_err( + colored.blue( + "Loading metadata for box '{}'{}".format( + descriptor, + " ({})".format(version) if version else ""))) url = 'https://app.vagrantup.com/{}/boxes/{}'.format(account, box) r = requests.get(url, **requests_kwargs) r.raise_for_status() @@ -319,7 +329,11 @@ def catalog_to_mechfile(catalog, name=None, version=None): mechfile['box_version'] = current_version mechfile['url'] = provider['url'] return mechfile - puts_err(colored.red("Couldn't find a VMWare compatible VM for '{}'{}".format(name, " ({})".format(version) if version else ""))) + puts_err( + colored.red( + "Couldn't find a VMWare compatible VM for '{}'{}".format( + name, + " ({})".format(version) if version else ""))) sys.exit(1) @@ -329,7 +343,8 @@ def tar_cmd(*args, **kwargs): 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: @@ -346,9 +361,22 @@ def tar_cmd(*args, **kwargs): return tar -def init_box(name, version, force=False, save=True, requests_kwargs={}, numvcpus=None, memsize=None): +def init_box( + name, + version, + force=False, + save=True, + requests_kwargs={}, + numvcpus=None, + memsize=None): if not locate('.mech', '*.vmx'): - name_version_box = add_box(name, name=name, version=version, force=force, save=save, requests_kwargs=requests_kwargs) + name_version_box = add_box( + name, + name=name, + version=version, + force=force, + save=save, + requests_kwargs=requests_kwargs) if not name_version_box: puts_err(colored.red("Cannot find a valid box with a VMX file in it")) sys.exit(1) @@ -385,8 +413,18 @@ def init_box(name, version, force=False, save=True, requests_kwargs={}, numvcpus 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) + 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={}): @@ -397,8 +435,18 @@ def add_mechfile(mechfile, name=None, version=None, force=False, save=True, requ 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, + 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 ""))) def add_box_url(name, version, url, force=False, save=True, requests_kwargs={}): @@ -409,7 +457,8 @@ def add_box_url(name, version, url, force=False, save=True, requests_kwargs={}): if exists: puts_err(colored.blue("Attempting to download box '{}'...".format(name))) else: - puts_err(colored.blue("Box '{}' could not be found. Attempting to download...".format(name))) + puts_err(colored.blue("Box '{}' could not be found. " + "Attempting to download...".format(name))) try: puts_err(colored.blue("URL: {}".format(url))) r = requests.get(url, stream=True, **requests_kwargs) @@ -423,7 +472,11 @@ def add_box_url(name, version, url, force=False, save=True, requests_kwargs={}): progress_type = progress.dots fp = 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( + r.iter_content( + chunk_size=1024), + label="{} ".format(boxname), + **progress_args): if chunk: fp.write(chunk) fp.close() @@ -431,7 +484,13 @@ def add_box_url(name, version, url, force=False, save=True, requests_kwargs={}): # 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) + return add_mechfile( + mechfile, + name=name, + version=version, + force=force, + save=save, + requests_kwargs=requests_kwargs) else: # Otherwise it must be a valid box: return add_box_file(name, version, fp.name, url=url, force=force, save=save) @@ -505,7 +564,11 @@ def init_mechfile(instance_name, descriptor, name=None, version=None, requests_k 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 = build_mechfile( + descriptor, + name=name, + version=version, + requests_kwargs=requests_kwargs) mechfile['name'] = instance_name return save_mechfile(mechfile, path) diff --git a/mech/vmrun.py b/mech/vmrun.py index 9ac492f..f341f24 100644 --- a/mech/vmrun.py +++ b/mech/vmrun.py @@ -74,6 +74,7 @@ def get_win32_executable(): reg.Close() return get_fallback_executable() + def get_provider(vmrun_exe): """ identifies the right hosttype for vmrun command (ws | fusion | player) @@ -88,7 +89,13 @@ def get_provider(vmrun_exe): 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) + proc = subprocess.Popen([vmrun_exe, + '-T', + provider, + 'list'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + startupinfo=startupinfo) except OSError: pass @@ -130,13 +137,22 @@ 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)) 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: @@ -214,7 +230,11 @@ def unpause(self, quiet=False): def listSnapshots(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''' @@ -222,7 +242,12 @@ def snapshot(self, snap_name, quiet=False): def deleteSnapshot(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) + 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): '''Set VM state to a snapshot''' @@ -254,11 +279,22 @@ def listNetworkAdapters(self, quiet=False): def addNetworkAdapter(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) + 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): '''Update a network adapter on a VM''' - return self.vmrun('setNetworkAdapter', self.vmx_file, adapter_index, adapter_type, host_network, quiet=quiet) + return self.vmrun( + 'setNetworkAdapter', + self.vmx_file, + adapter_index, + adapter_type, + host_network, + quiet=quiet) def deleteNetworkAdapter(self, adapter_index, quiet=False): '''Remove a network adapter on a VM''' @@ -269,10 +305,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 @@ -291,9 +329,25 @@ def listPortForwardings(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 setPortForwarding( + 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) + 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): '''Delete a port forwarding on a host network''' @@ -396,8 +450,23 @@ 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 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''' @@ -409,7 +478,13 @@ def directoryExistsInGuest(self, path, quiet=False): def setSharedFolderState(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) + return self.vmrun( + 'setSharedFolderState', + self.vmx_file, + share_name, + new_path, + mode, + quiet=quiet) def addSharedFolder(self, share_name, host_path, quiet=False): '''Add a Host-Guest shared folder''' @@ -434,9 +509,24 @@ def killProcessInGuest(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 runScriptInGuest( + 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) + 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): '''Delete a file in Guest OS''' @@ -460,11 +550,21 @@ def listDirectoryInGuest(self, path, quiet=False): def copyFileFromHostToGuest(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) + return self.vmrun( + 'copyFileFromHostToGuest', + self.vmx_file, + host_path, + guest_path, + quiet=quiet) def copyFileFromGuestToHost(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) + return self.vmrun( + 'copyFileFromGuestToHost', + self.vmx_file, + guest_path, + host_path, + quiet=quiet) def renameFileInGuest(self, original_name, new_name, quiet=False): '''Rename a file in Guest OS''' @@ -497,7 +597,11 @@ def readVariable(self, var_name, mode=None, quiet=False): def getGuestIPAddress(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) + 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) try: fp.close() 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 From 7b20319168a262a6f74f6bd5c26dd723dc11a09b Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 1 Feb 2020 11:25:36 -0800 Subject: [PATCH 009/116] move all files under this dir .mech --- mech/__init__.py | 2 +- mech/__main__.py | 4 ++-- mech/mech.py | 6 +++--- mech/utils.py | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mech/__init__.py b/mech/__init__.py index f81418d..5209036 100644 --- a/mech/__init__.py +++ b/mech/__init__.py @@ -22,5 +22,5 @@ # IN THE SOFTWARE. # -__version__ = '0.7.6' +__version__ = '0.7.7' VERSION = "{} v{}".format(__name__, __version__) diff --git a/mech/__main__.py b/mech/__main__.py index c7c7a47..c8ad1ce 100644 --- a/mech/__main__.py +++ b/mech/__main__.py @@ -34,8 +34,8 @@ def main(): from .mech import Mech from .utils import makedirs - HOME = os.path.expanduser('~/.mech') - makedirs(HOME) + MECH_DIR = os.path.expanduser(os.getcwd() + '/.mech') + makedirs(MECH_DIR) arguments = Mech.docopt(Mech.__doc__, argv=sys.argv[1:], version=VERSION) return Mech(arguments)() except KeyboardInterrupt: diff --git a/mech/mech.py b/mech/mech.py index 30bfe9b..9e0ab12 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -75,7 +75,7 @@ -----END RSA PRIVATE KEY----- """ -HOME = os.path.expanduser("~/.mech") +MECH_DIR = os.path.expanduser(os.getcwd() + '/.mech') class MechCommand(Command): @@ -160,7 +160,7 @@ def config_ssh(self): ))) sys.exit(1) - insecure_private_key = os.path.abspath(os.path.join(HOME, "insecure_private_key")) + insecure_private_key = os.path.abspath(os.path.join(MECH_DIR, "insecure_private_key")) if not os.path.exists(insecure_private_key): with open(insecure_private_key, 'w') as f: f.write(INSECURE_PRIVATE_KEY) @@ -258,7 +258,7 @@ def list(self, arguments): 'BOX'.rjust(35), 'VERSION'.rjust(12), )) - path = os.path.abspath(os.path.join(HOME, 'boxes')) + path = os.path.abspath(os.path.join(MECH_DIR, '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:] diff --git a/mech/utils.py b/mech/utils.py index b352090..e3b3318 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -47,8 +47,8 @@ logger = logging.getLogger(__name__) -HOME = os.path.expanduser('~/.mech') -DATA_DIR = os.path.join(HOME, 'data') +MECH_DIR = os.path.expanduser(os.getcwd() + '/.mech') +DATA_DIR = os.path.join(MECH_DIR, 'data') def makedirs(name, mode=0o777): @@ -451,7 +451,7 @@ def add_mechfile(mechfile, name=None, version=None, force=False, save=True, requ def add_box_url(name, version, url, force=False, save=True, requests_kwargs={}): boxname = os.path.basename(url) - box = os.path.join(*filter(None, (HOME, 'boxes', name, version, boxname))) + box = os.path.join(*filter(None, (MECH_DIR, 'boxes', name, version, boxname))) exists = os.path.exists(box) if not exists or force: if exists: @@ -537,7 +537,7 @@ def add_box_file(name, version, filename, url=None, force=False, save=True): 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', name, version, boxname))) path = os.path.dirname(box) makedirs(path) if not os.path.exists(box) or force: From 13e265a1442cd8c78eecc2f3f135675ac859282c Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 1 Feb 2020 15:40:08 -0800 Subject: [PATCH 010/116] get list/destroy to work as expected --- mech/mech.py | 67 ++++++++---------- mech/utils.py | 189 ++++++++++++++++++++++++++------------------------ 2 files changed, 126 insertions(+), 130 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 9e0ab12..b0ff09d 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -81,11 +81,8 @@ 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_mechfile(self): + self.active_mechfile = utils.load_mechfile() def activate(self, instance_name=None): if not hasattr(self, 'mechfiles'): @@ -100,13 +97,10 @@ def activate(self, instance_name=None): "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) + self.activate_mechfile() 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 + self.activate_mechfile() + instance_name = self.active_mechfile.get('name') return instance_name def get(self, name, default=None): @@ -538,12 +532,9 @@ def init(self, arguments): """ 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. + Example box: bento/ubuntu-18.04 Options: -f, --force Overwrite existing Mechfile @@ -554,17 +545,18 @@ def init(self, arguments): --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 + --name INSTANCE Name of the instance (myinst1) + --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) -h, --help Print this help """ - url = arguments[''] - if url: - name = arguments[''] - else: - url = arguments[''] - name = None - version = arguments['--box-version'] - self.instance_name = arguments['--name'] + + name = arguments['--name'] + box_version = arguments['--box-version'] + box = arguments[''] + + if not name or name == "": + name = "first" + force = arguments['--force'] requests_kwargs = utils.get_requests_kwargs(arguments) @@ -577,10 +569,9 @@ def init(self, arguments): puts_err(colored.green("Initializing mech")) if utils.init_mechfile( - self.instance_name, - url, + box, name=name, - version=version, + box_version=box_version, requests_kwargs=requests_kwargs): puts_err(colored.green(textwrap.fill( "A `Mechfile` has been initialized and placed in this directory. " @@ -624,6 +615,7 @@ def up(self, arguments): memsize = arguments['--memsize'] vmx = utils.init_box( + self.instance_name, self.box_name, self.box_version, requests_kwargs=requests_kwargs, @@ -736,24 +728,23 @@ def destroy(self, arguments): if self.instance_name: instance = utils.settle_instance(self.instance_name) - path = instance['path'] + inst_path = instance['path'] else: - path = os.getcwd() - mech_path = os.path.join(path, '.mech') + inst_path = os.getcwd() - if os.path.exists(mech_path): + if os.path.exists(inst_path): if force or utils.confirm( - "Are you sure you want to delete {self.instance_name} at {path}".format( - instance_name=self.instance_name, path=path), default='n'): + "Are you sure you want to delete {instance_name} at {inst_path}".format( + instance_name=self.instance_name, inst_path=inst_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() - if os.path.exists(mech_path): - shutil.rmtree(mech_path) + if os.path.exists(inst_path): + shutil.rmtree(inst_path) else: - logger.debug("{} was not found.".format(mech_path)) + logger.debug("{} was not found.".format(inst_path)) else: puts_err(colored.red("Deletion aborted")) else: @@ -1146,7 +1137,7 @@ def list(self, arguments): path = instance.get('path') if path and os.path.exists(path): self.activate(instance_name) - mech_path = os.path.join(path, '.mech') + mech_path = os.path.join(path, '.mech/' + instance_name) if os.path.exists(mech_path): vmx = self.get_vmx(silent=True) if vmx: diff --git a/mech/utils.py b/mech/utils.py index e3b3318..3d2127d 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -180,13 +180,6 @@ def instances(): 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: @@ -214,13 +207,6 @@ def settle_instance(instance_name, obj=None, force=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) @@ -245,39 +231,37 @@ def settle_instance(instance_name, obj=None, force=False): 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 load_mechfile(): + mechfile_full = os.path.join(os.path.expanduser(os.getcwd()), 'Mechfile') + if os.path.isfile(mechfile_full): + with open(mechfile_full) as fp: + try: + return json.loads(uncomment(fp.read())) + except ValueError: + puts_err(colored.red("Invalid Mechfile." + os.linesep)) + else: + 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={}): +def build_mechfile(descriptor, name=None, box_version=None, requests_kwargs={}): mechfile = {} if descriptor is None: return mechfile + mechfile['name'] = name if any(descriptor.startswith(s) for s in ('https://', 'http://', 'ftp://')): mechfile['url'] = descriptor if not name: name = os.path.splitext(os.path.basename(descriptor))[0] mechfile['box'] = name - if version: - mechfile['box_version'] = version + if box_version: + mechfile['box_version'] = box_version return mechfile elif descriptor.startswith('file:') or os.path.isfile(re.sub(r'^file:(?://)?', '', descriptor)): descriptor = re.sub(r'^file:(?://)?', '', descriptor) @@ -289,8 +273,8 @@ def build_mechfile(descriptor, name=None, version=None, requests_kwargs={}): if not name: name = os.path.splitext(os.path.basename(descriptor))[0] mechfile['box'] = name - if version: - mechfile['box_version'] = version + if box_version: + mechfile['box_version'] = box_version return mechfile else: try: @@ -298,12 +282,12 @@ def build_mechfile(descriptor, name=None, version=None, requests_kwargs={}): if not account or not box: puts_err(colored.red("Provided box name is not valid")) if v: - version = v + box_version = v puts_err( colored.blue( "Loading metadata for box '{}'{}".format( descriptor, - " ({})".format(version) if version else ""))) + " ({})".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() @@ -314,7 +298,7 @@ def build_mechfile(descriptor, name=None, version=None, requests_kwargs={}): 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) + return catalog_to_mechfile(catalog, name, box_version) def catalog_to_mechfile(catalog, name=None, version=None): @@ -325,6 +309,7 @@ def catalog_to_mechfile(catalog, name=None, version=None): if not version or current_version == version: for provider in v['providers']: if 'vmware' in provider['name']: + mechfile['name'] = name mechfile['box'] = catalog['name'] mechfile['box_version'] = current_version mechfile['url'] = provider['url'] @@ -363,44 +348,52 @@ def tar_cmd(*args, **kwargs): def init_box( name, - version, + box, + box_version, force=False, save=True, requests_kwargs={}, numvcpus=None, memsize=None): - if not locate('.mech', '*.vmx'): + inst_dir = '.mech/' + name + if not locate(inst_dir, '*.vmx'): name_version_box = add_box( - name, name=name, - version=version, + box=box, + box_version=box_version, force=force, save=save, requests_kwargs=requests_kwargs) 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') - puts_err(colored.blue("Extracting box '{}'...".format(name))) - makedirs('.mech') + print('box:{}'.format(box)) + box_parts = box.split('/') + box_dir = os.path.join(*filter(None, (MECH_DIR, 'boxes', + box_parts[0], box_parts[1], box_version))) + print('box_dir:{}'.format(box_dir)) + box_file = locate(box_dir, '*.box') + print('box_file:{}'.format(box_file)) + + puts_err(colored.blue("Extracting box '{}'...".format(box_file))) + makedirs(inst_dir) 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=inst_dir, startupinfo=startupinfo) if proc.wait(): puts_err(colored.red("Cannot extract box")) sys.exit(1) else: - tar = tarfile.open(box, 'r') - tar.extractall('.mech') + tar = tarfile.open(box_file, 'r') + tar.extractall(inst_dir) if not save and box.startswith(tempfile.gettempdir()): os.unlink(box) @@ -412,33 +405,42 @@ def init_box( return vmx -def add_box(descriptor, name=None, version=None, force=False, save=True, requests_kwargs={}): +def add_box(name=None, box=None, box_version=None, force=False, save=True, requests_kwargs={}): + # build the dict mechfile = build_mechfile( - descriptor, + box, name=name, - version=version, + box_version=box_version, requests_kwargs=requests_kwargs) + print('mechfile:{}'.format(mechfile)) + return add_mechfile( mechfile, name=name, - version=version, + box_version=box_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') +def add_mechfile(mechfile, name=None, box=None, box_version=None, + force=False, save=True, requests_kwargs={}): + box = mechfile.get('box') + name = mechfile.get('name') version = mechfile.get('box_version') - if file: - return add_box_file(name, version, file, force=force, save=save) + + url = mechfile.get('url') + box_file = mechfile.get('file') + + if box_file: + return add_box_file(box, version, box_file, force=force, save=save) + if url: return add_box_url( - name, - version, - url, + name=name, + box=box, + box_version=box_version, + url=url, force=force, save=save, requests_kwargs=requests_kwargs) @@ -449,16 +451,19 @@ def add_mechfile(mechfile, name=None, version=None, force=False, save=True, requ " ({})".format(version) if 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, requests_kwargs={}): boxname = os.path.basename(url) - box = os.path.join(*filter(None, (MECH_DIR, 'boxes', name, version, boxname))) - exists = os.path.exists(box) + box_parts = box.split('/') + box_dir = os.path.join(*filter(None, (MECH_DIR, 'boxes', + box_parts[0], box_parts[1], box_version))) + exists = os.path.exists(box_dir) + print("box_parts:{} box_dir:{} exists:{}".format(box_parts, box_dir, exists)) if not exists or force: if exists: - puts_err(colored.blue("Attempting to download box '{}'...".format(name))) + puts_err(colored.blue("Attempting to download box '{}'...".format(box))) else: puts_err(colored.blue("Box '{}' could not be found. " - "Attempting to download...".format(name))) + "Attempting to download...".format(box))) try: puts_err(colored.blue("URL: {}".format(url))) r = requests.get(url, stream=True, **requests_kwargs) @@ -483,17 +488,17 @@ def add_box_url(name, version, url, force=False, save=True, requests_kwargs={}): if r.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) + mechfile = catalog_to_mechfile(catalog, name, box, box_version) return add_mechfile( mechfile, name=name, - version=version, + box_version=box_version, force=force, save=save, requests_kwargs=requests_kwargs) 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_version, fp.name, url=url, force=force, save=save) finally: os.unlink(fp.name) except requests.HTTPError as exc: @@ -502,11 +507,11 @@ def add_box_url(name, version, url, force=False, save=True, requests_kwargs={}): except requests.ConnectionError: puts_err(colored.red("Couldn't connect to '%s'" % url)) sys.exit(1) - return name, version, box + 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, box_version, filename, url=None, force=False, save=True): + puts_err(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) @@ -537,39 +542,39 @@ def add_box_file(name, version, filename, url=None, force=False, save=True): if valid_tar: if save: boxname = os.path.basename(url if url else filename) - box = os.path.join(*filter(None, (MECH_DIR, '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 + return box, box_version -def index_active_instance(instance_name): - path = os.getcwd() - instance = settle_instance(instance_name, { +def index_active_instance(name): + path = os.path.join(MECH_DIR, name) + print('in index_active_index path:{}'.format(path)) + instance = settle_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'))))) + ).format(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) +def init_mechfile(box=None, name=None, box_version=None, requests_kwargs={}): + path = os.path.expanduser(os.getcwd()) + print("in init_mechfile name:{}".format(name)) mechfile = build_mechfile( - descriptor, + box, name=name, - version=version, + box_version=box_version, requests_kwargs=requests_kwargs) - mechfile['name'] = instance_name + print("saving mechfile:{} to path:{}".format(mechfile, path)) return save_mechfile(mechfile, path) From 16812330a1c89f14b46449bb6c91f7b424576e3a Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 2 Feb 2020 08:57:40 -0800 Subject: [PATCH 011/116] got mech up and ls working with multiple --- mech/mech.py | 284 ++++++++++++++++++++++++++++---------------------- mech/utils.py | 41 ++++---- 2 files changed, 180 insertions(+), 145 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index b0ff09d..bf739f7 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -79,62 +79,90 @@ class MechCommand(Command): - active_mechfile = None + mechfile = None + active_name = None + created = False + box = None + box_version = None + url = None + vmx = None + user = None + password = None + enable_ip_lookup = False def activate_mechfile(self): - self.active_mechfile = utils.load_mechfile() + """Load the Mechfile.""" + self.mechfile = utils.load_mechfile() - 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)) - self.activate_mechfile() - else: + def instances(self): + """Returns a list of the instances from the Mechfile.""" + if not self.mechfile: self.activate_mechfile() - instance_name = self.active_mechfile.get('name') - return instance_name + return list(self.mechfile) - 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) + @staticmethod + def instance_path(name): + """Return the path for an instance.""" + return os.path.join(MECH_DIR, name) - def get_vmx(self, silent=False): - self.get("") # Check if there's a Mechfile - return utils.get_vmx(silent=silent) + def activate(self, name): + """Sets the active instance name and changes to the + directory for that instance.""" + if not self.mechfile: + self.activate_mechfile() + if name == "": + raise AttributeError("Must activate with name.") + print('self.mechfile:{}'.format(self.mechfile)) + print('name:{}'.format(name)) + if self.mechfile.get(name, None): + self.active_name = name + else: + puts_err(colored.red("Instance ({}) was not found in the Mechfile".format(name))) + sys.exit(1) + self.box = self.mechfile[name].get('box', None) + self.box_version = self.mechfile[name].get('box_version', None) + self.url = self.mechfile[name].get('url', None) + self.user = DEFAULT_USER + self.password = DEFAULT_PASSWORD + path = MechCommand.instance_path(name) + print(' path:{}'.format(path)) + vmx = utils.locate(path, '*.vmx') + print(' vmx:{}'.format(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 os.path.exists(path): + #print('path exists') + # change to the path of the instance + os.chdir(path) - @property def vmx(self): - return self.get_vmx() + """Get the fully qualified path to the vmx file.""" + return self.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 + def box(self): + """Get the box (ex: 'bento/ubuntu-18.04').""" + return self.box - @property def box_version(self): - return self.get('box_version') + """Get the box_version (ex: '2020-01-16').""" + return self.box_version - @property def user(self): - return self.get('user', DEFAULT_USER) + """Get the username (ex: 'vagrant').""" + return self.user - @property def password(self): - return self.get('password', DEFAULT_PASSWORD) + """Get the password (ex: 'temp123').""" + return self.password + + def enable_ip_lookup(self): + """Enable ip lookup (defaults to False).""" + return self.enable_ip_lookup @property def config(self): @@ -143,7 +171,7 @@ def config(self): @property def config_ssh(self): vmrun = VMrun(self.vmx, user=self.user, password=self.password) - lookup = self.get("enable_ip_lookup", False) + lookup = self.enable_ip_lookup ip = vmrun.getGuestIPAddress(wait=False, lookup=lookup) if vmrun.installedTools() else None if not ip: puts_err(colored.red(textwrap.fill( @@ -586,6 +614,8 @@ def up(self, arguments): Usage: mech up [options] [] + Note: If no instance is specified, all instances will be started. + Options: --gui Start GUI --disable-shared-folder Do not share folder with VM @@ -606,47 +636,67 @@ def up(self, arguments): save = not arguments['--no-cache'] requests_kwargs = utils.get_requests_kwargs(arguments) - instance_name = arguments[''] - self.instance_name = self.activate(instance_name) - - utils.index_active_instance(self.instance_name) - numvcpus = arguments['--numvcpus'] memsize = arguments['--memsize'] - vmx = utils.init_box( - self.instance_name, - self.box_name, - self.box_version, - requests_kwargs=requests_kwargs, - save=save, - numvcpus=numvcpus, - memsize=memsize) - - 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")) + instance_name = arguments[''] + + if instance_name: + # spin up a 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...")) - if not disable_shared_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))) + # want to spin up multiple instances + instances = self.instances() + + print('instances:{}'.format(instances)) + + for instance in instances: + print('instance:{}'.format(instance)) + self.activate(instance) + print('{} has been activated'.format(instance)) + print('self.active_name:{}'.format(self.active_name)) + instance_path = MechCommand.instance_path(instance) + + vmx = utils.init_box( + instance, + box=self.box, + box_version=self.box_version, + instance_path=instance_path, + requests_kwargs=requests_kwargs, + save=save, + numvcpus=numvcpus, + memsize=memsize) + print('vmx:{}'.format(vmx)) + if vmx: + self.vmx = vmx + self.created = True + + 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")) else: - if started: - puts_err(colored.green("VM started on an unknown IP address")) + time.sleep(3) + puts_err(colored.blue("Getting IP address...")) + lookup = self.enable_ip_lookup + ip = vmrun.getGuestIPAddress(lookup=lookup) + puts_err(colored.blue("Sharing current folder...")) + if not disable_shared_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))) else: - puts_err(colored.yellow("VM was already started on an unknown IP address")) + if started: + puts_err(colored.green("VM started on an unknown IP address")) + else: + puts_err(colored.yellow("VM was already started on an unknown IP address")) + + # allows "mech start" to alias to "mech up" start = up def global_status(self, arguments): @@ -692,7 +742,7 @@ def status(self, arguments): vmrun = VMrun(self.vmx, user=self.user, password=self.password) box_name = self.box_name - lookup = self.get("enable_ip_lookup", False) + lookup = self.enable_ip_lookup ip = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) state = vmrun.checkToolsState(quiet=True) @@ -816,7 +866,7 @@ def resume(self, arguments): 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) + lookup = self.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) if ip: puts_err(colored.green("VM resumed on {}".format(ip))) @@ -831,7 +881,7 @@ def resume(self, arguments): else: time.sleep(3) puts_err(colored.blue("Getting IP address...")) - lookup = self.get("enable_ip_lookup", False) + lookup = self.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) puts_err(colored.blue("Sharing current folder...")) vmrun.enableSharedFolders() @@ -992,7 +1042,7 @@ def ip(self, arguments): self.instance_name = self.activate(instance_name) vmrun = VMrun(self.vmx, user=self.user, password=self.password) - lookup = self.get("enable_ip_lookup", False) + lookup = self.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) if ip: puts_err(colored.green(ip)) @@ -1070,7 +1120,7 @@ def reload(self, arguments): else: time.sleep(3) puts_err(colored.blue("Getting IP address...")) - lookup = self.get("enable_ip_lookup", False) + lookup = self.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) if ip: if started: @@ -1106,16 +1156,6 @@ def port(self, arguments): else: puts_err(colored.red("Cannot find a nat network")) - def push(self, arguments): - """ - Deploys code in this environment to a configured destination. - - Usage: mech push [options] [] - - Options: - -h, --help Print this help - """ - puts_err(colored.red("Not implemented!")) def list(self, arguments): """ @@ -1126,41 +1166,37 @@ def list(self, arguments): Options: -h, --help Print this help """ - print("{}\t{}\t{}\t{}\t{}".format( + + self.activate_mechfile() + + print("{}\t{}\t{}\t{}".format( 'NAME'.rjust(20), 'ADDRESS'.rjust(15), 'BOX'.rjust(35), - 'VERSION'.rjust(12), - 'PATH', + 'VERSION'.rjust(12) )) - 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/' + instance_name) - 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) + for name in self.mechfile: + self.activate(name) + #print('name:{} box:{} created:{}'.format(name, self.box, self.created)) + if self.created: + vmrun = VMrun(self.vmx, user=self.user, password=self.password) + lookup = self.enable_ip_lookup + ip = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) + if ip is None: + ip = colored.yellow("poweroff") + elif not ip: + ip = 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 = colored.green(ip) + else: + ip = "notcreated" + + print("{}\t{}\t{}\t{}".format( + colored.green(name.rjust(20)), + ip.rjust(15), + self.box.rjust(35), + self.box_version.rjust(12) + )) + + # allow 'mech ls' as alias to 'mech list' ls = list diff --git a/mech/utils.py b/mech/utils.py index 3d2127d..2ed256f 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -101,9 +101,10 @@ def confirm(prompt, default='y'): return False -def save_mechfile(mechfile, path): +def save_mechfile(mechfile, name, path): + multiple_mechfiles = { name: mechfile } with open(os.path.join(path, 'Mechfile'), 'w+') as fp: - json.dump(mechfile, fp, sort_keys=True, indent=2, separators=(',', ': ')) + json.dump(multiple_mechfiles, fp, sort_keys=True, indent=2, separators=(',', ': ')) return True @@ -198,6 +199,7 @@ def instances(): def settle_instance(instance_name, obj=None, force=False): + print('in settle_instance instance_name:{}'.format(instance_name)) makedirs(DATA_DIR) index_path = os.path.join(DATA_DIR, 'index') index_lock = os.path.join(DATA_DIR, 'index.lock') @@ -207,6 +209,7 @@ def settle_instance(instance_name, obj=None, force=False): if os.path.exists(index_path): with open(index_path) as fp: instances = json.loads(uncomment(fp.read())) + print('instances:{}'.format(instances)) else: instances = {} instance_data = instances.get(instance_name) @@ -352,11 +355,11 @@ def init_box( box_version, force=False, save=True, + instance_path=None, requests_kwargs={}, numvcpus=None, memsize=None): - inst_dir = '.mech/' + name - if not locate(inst_dir, '*.vmx'): + if not locate(instance_path, '*.vmx'): name_version_box = add_box( name=name, box=box, @@ -368,16 +371,16 @@ def init_box( puts_err(colored.red("Cannot find a valid box with a VMX file in it")) sys.exit(1) - print('box:{}'.format(box)) + #print('box:{}'.format(box)) box_parts = box.split('/') box_dir = os.path.join(*filter(None, (MECH_DIR, 'boxes', box_parts[0], box_parts[1], box_version))) - print('box_dir:{}'.format(box_dir)) + #print('box_dir:{}'.format(box_dir)) box_file = locate(box_dir, '*.box') - print('box_file:{}'.format(box_file)) + #print('box_file:{}'.format(box_file)) puts_err(colored.blue("Extracting box '{}'...".format(box_file))) - makedirs(inst_dir) + makedirs(instance_path) if sys.platform == 'win32': cmd = tar_cmd('-xf', box_file, force_local=True) else: @@ -387,18 +390,22 @@ def init_box( if os.name == "nt": startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.SW_HIDE | subprocess.STARTF_USESHOWWINDOW - proc = subprocess.Popen(cmd, cwd=inst_dir, startupinfo=startupinfo) + proc = subprocess.Popen(cmd, cwd=instance_path, startupinfo=startupinfo) if proc.wait(): puts_err(colored.red("Cannot extract box")) sys.exit(1) else: tar = tarfile.open(box_file, 'r') - tar.extractall(inst_dir) + tar.extractall(instance_path) if not save and box.startswith(tempfile.gettempdir()): os.unlink(box) - vmx = get_vmx() + vmx = locate(instance_path, '*.vmx') + if not vmx and not silent: + puts_err(colored.red("Cannot locate a VMX file")) + sys.exit(1) + #print('in init_box name:{} vmx:{}'.format(name, vmx)) update_vmx(vmx, numvcpus=numvcpus, memsize=memsize) @@ -412,7 +419,7 @@ def add_box(name=None, box=None, box_version=None, force=False, save=True, reque name=name, box_version=box_version, requests_kwargs=requests_kwargs) - print('mechfile:{}'.format(mechfile)) + #print('mechfile:{}'.format(mechfile)) return add_mechfile( mechfile, @@ -575,7 +582,7 @@ def init_mechfile(box=None, name=None, box_version=None, requests_kwargs={}): box_version=box_version, requests_kwargs=requests_kwargs) print("saving mechfile:{} to path:{}".format(mechfile, path)) - return save_mechfile(mechfile, path) + return save_mechfile(mechfile, name, path) def get_requests_kwargs(arguments): @@ -591,14 +598,6 @@ def get_requests_kwargs(arguments): 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 - - def provision_file(vm, source, destination): return vm.copyFileFromHostToGuest(source, destination) From 3c4a20ba7bd9a8aaef9b5628f296ff1a718246f8 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 2 Feb 2020 09:42:50 -0800 Subject: [PATCH 012/116] enable most commands to deal with single or multiple instances --- mech/mech.py | 648 ++++++++++++++++++++++++++++---------------------- mech/utils.py | 89 +------ 2 files changed, 360 insertions(+), 377 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index bf739f7..0a6949e 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -105,6 +105,19 @@ def instance_path(name): """Return the path for an instance.""" return os.path.join(MECH_DIR, name) + def deactivate(self): + """Make it so we do have have an active instance selected.""" + self.active_name = None + self.created = False + self.box = None + self.box_version = None + self.url = None + self.vmx = None + self.user = None + self.password = None + self.enable_ip_lookup = False + os.chdir(MECH_DIR) + def activate(self, name): """Sets the active instance name and changes to the directory for that instance.""" @@ -112,8 +125,6 @@ def activate(self, name): self.activate_mechfile() if name == "": raise AttributeError("Must activate with name.") - print('self.mechfile:{}'.format(self.mechfile)) - print('name:{}'.format(name)) if self.mechfile.get(name, None): self.active_name = name else: @@ -125,9 +136,7 @@ def activate(self, name): self.user = DEFAULT_USER self.password = DEFAULT_PASSWORD path = MechCommand.instance_path(name) - print(' path:{}'.format(path)) vmx = utils.locate(path, '*.vmx') - print(' vmx:{}'.format(vmx)) # Note: If vm has not been started vmx will be None if vmx: self.vmx = vmx @@ -136,7 +145,6 @@ def activate(self, name): self.vmx = None self.created = False if os.path.exists(path): - #print('path exists') # change to the path of the instance os.chdir(path) @@ -188,7 +196,7 @@ def config_ssh(self): f.write(INSECURE_PRIVATE_KEY) os.chmod(insecure_private_key, 0o400) config = { - "Host": self.instance_name, + "Host": self.active_name, "User": self.user, "Port": "22", "UserKnownHostsFile": "/dev/null", @@ -224,9 +232,7 @@ class MechBox(MechCommand): 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` @@ -307,22 +313,6 @@ def outdated(self, arguments): """ puts_err(colored.red("Not implemented!")) - def prune(self, arguments): - """ - Remove old versions of installed boxes. - - 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. @@ -337,21 +327,6 @@ def remove(self, arguments): """ 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. @@ -380,9 +355,6 @@ class MechSnapshot(MechCommand): 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` @@ -400,13 +372,22 @@ def delete(self, arguments): name = arguments[''] instance_name = arguments[''] - self.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")) + if instance_name: + # single instance + instances = [instance_name] else: - puts_err(colored.green("Snapshot {} deleted".format(name))) + # multiple instances + instances = self.instances() + + for instance in instances: + self.activate(instance) + + 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): """ @@ -418,54 +399,18 @@ def list(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - self.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] [] + if instance_name: + # single instance + instances = [instance_name] + else: + # multiple instances + instances = self.instances() - Options: - --provision Enable provisioning - -h, --help Print this help - """ - puts_err(colored.red("Not implemented!")) + for instance in instances: + self.activate(instance) + vmrun = VMrun(self.vmx, user=self.user, password=self.password) + print(vmrun.listSnapshots()) def save(self, arguments): """ @@ -488,13 +433,21 @@ def save(self, arguments): name = arguments[''] instance_name = arguments[''] - self.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")) + if instance_name: + # single instance + instances = [instance_name] else: - puts_err(colored.green("Snapshot {} taken".format(name))) + # multiple instances + instances = self.instances() + + for instance in instances: + self.activate(instance) + 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))) class Mech(MechCommand): @@ -642,19 +595,14 @@ def up(self, arguments): instance_name = arguments[''] if instance_name: - # spin up a single instance + # single instance instances = [instance_name] else: - # want to spin up multiple instances + # multiple instances instances = self.instances() - print('instances:{}'.format(instances)) - for instance in instances: - print('instance:{}'.format(instance)) self.activate(instance) - print('{} has been activated'.format(instance)) - print('self.active_name:{}'.format(self.active_name)) instance_path = MechCommand.instance_path(instance) vmx = utils.init_box( @@ -666,7 +614,6 @@ def up(self, arguments): save=save, numvcpus=numvcpus, memsize=memsize) - print('vmx:{}'.format(vmx)) if vmx: self.vmx = vmx self.created = True @@ -722,10 +669,18 @@ def ps(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - self.instance_name = self.activate(instance_name) - vmrun = VMrun(self.vmx, self.user, self.password) - print(vmrun.listProcessesInGuest()) + if instance_name: + # single instance + instances = [instance_name] + else: + # multiple instances + instances = self.instances() + + for instance in instances: + self.activate(instance) + vmrun = VMrun(self.vmx, self.user, self.password) + print(vmrun.listProcessesInGuest()) def status(self, arguments): """ @@ -737,29 +692,39 @@ def status(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - self.instance_name = self.activate(instance_name) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + if instance_name: + # single instance + instances = [instance_name] + else: + # multiple instances + instances = self.instances() - box_name = self.box_name - lookup = self.enable_ip_lookup - ip = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) - state = vmrun.checkToolsState(quiet=True) - - print("Current machine state:" + os.linesep) - if ip is None: - ip = "poweroff" - elif not ip: - ip = "unknown" - print("%s\t%s\t%s\t(VMware Tools %s)" % (self.instance_name, 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`") + for instance in instances: + self.activate(instance) + + vmrun = VMrun(self.vmx, user=self.user, password=self.password) + + lookup = self.enable_ip_lookup + ip = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) + state = vmrun.checkToolsState(quiet=True) + + print("Current machine state:" + os.linesep) + if ip is None: + ip = "poweroff" + elif not ip: + ip = "unknown" + print("%s\t%s\t%s\t(VMware Tools %s)" % (self.active_name, self.box, ip, state)) + + if ip == "poweroff": + print(os.linesep + "The VM is powered off. To restart the VM, " + "simply run `mech up {}`".format(instance)) + 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 {}`".format(instance)) def destroy(self, arguments): """ @@ -774,31 +739,41 @@ def destroy(self, arguments): force = arguments['--force'] instance_name = arguments[''] - self.instance_name = self.activate(instance_name) - if self.instance_name: - instance = utils.settle_instance(self.instance_name) - inst_path = instance['path'] + if instance_name: + # single instance + instances = [instance_name] else: - inst_path = os.getcwd() + # multiple instances + instances = self.instances() - if os.path.exists(inst_path): - if force or utils.confirm( - "Are you sure you want to delete {instance_name} at {inst_path}".format( - instance_name=self.instance_name, inst_path=inst_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() - if os.path.exists(inst_path): - shutil.rmtree(inst_path) + for instance in instances: + self.activate(instance) + + if self.active_name: + instance_path = MechCommand.instance_path(instance) + + if os.path.exists(instance_path): + if force or utils.confirm( + "Are you sure you want to delete {} at {}".format( + self.active_name, instance_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() + + # change out of this directory + self.deactivate() + + if os.path.exists(instance_path): + shutil.rmtree(instance_path) + else: + logger.debug("{} was not found.".format(instance_path)) else: - logger.debug("{} was not found.".format(inst_path)) + puts_err(colored.red("Deletion aborted")) else: - puts_err(colored.red("Deletion aborted")) - else: - puts_err(colored.red("The box hasn't been initialized.")) + puts_err(colored.red("The box hasn't been initialized.")) def down(self, arguments): """ @@ -813,17 +788,28 @@ def down(self, arguments): force = arguments['--force'] instance_name = arguments[''] - self.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: + self.activate(instance) + + 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)) + else: + puts_err(colored.green("Stopped", vmrun)) + + # alias 'mech stop' and 'mech halt' to 'mech down' stop = down halt = down @@ -837,13 +823,21 @@ def pause(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - self.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() + + for instance in instances: + self.activate(instance) + vmrun = VMrun(self.vmx, user=self.user, password=self.password) + if vmrun.pause() is None: + puts_err(colored.red("Not paused", vmrun)) + else: + puts_err(colored.yellow("Paused", vmrun)) def resume(self, arguments): """ @@ -856,46 +850,54 @@ def resume(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - self.instance_name = self.activate(instance_name) - utils.index_active_instance(self.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: + self.activate(instance) - # Try to unpause - if vmrun.unpause(quiet=True) is not None: - time.sleep(1) - puts_err(colored.blue("Getting IP address...")) - lookup = self.enable_ip_lookup - ip = vmrun.getGuestIPAddress(lookup=lookup) - if ip: - puts_err(colored.green("VM resumed on {}".format(ip))) - else: - puts_err(colored.green("VM resumed on an unknown IP address")) + vmrun = VMrun(self.vmx, user=self.user, password=self.password) - # Otherwise try starting - else: - started = vmrun.start() - if started is None: - puts_err(colored.red("VM not started")) - else: - time.sleep(3) + # Try to unpause + if vmrun.unpause(quiet=True) is not None: + time.sleep(1) puts_err(colored.blue("Getting IP address...")) lookup = self.enable_ip_lookup 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 already was started on {}".format(ip))) + puts_err(colored.green("VM resumed on {}".format(ip))) else: - if started: - puts_err(colored.green("VM started on an unknown IP address")) + puts_err(colored.green("VM resumed on an unknown IP address")) + + # Otherwise try starting + 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.enable_ip_lookup + 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 already was started on {}".format(ip))) else: - puts_err(colored.yellow("VM already was started on an unknown IP address")) + 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")) def suspend(self, arguments): """ @@ -907,13 +909,21 @@ def suspend(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - self.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: + self.activate(instance) + vmrun = VMrun(self.vmx, user=self.user, password=self.password) + if vmrun.suspend() is None: + puts_err(colored.red("Not suspended", vmrun)) + else: + puts_err(colored.green("Suspended", vmrun)) def ssh_config(self, arguments): """ @@ -925,9 +935,17 @@ def ssh_config(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - self.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() + + for instance in instances: + self.activate(instance) + print(utils.config_ssh_string(self.config_ssh)) def ssh(self, arguments): """ @@ -945,33 +963,42 @@ def ssh(self, arguments): command = arguments['--command'] instance_name = arguments[''] - self.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() + if instance_name: + # single instance + instances = [instance_name] + else: + # multiple instances + instances = self.instances() - 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)) + for instance in instances: + self.activate(instance) - logger.debug( - " ".join( - "'{}'".format( - c.replace( - "'", - "\\'")) if ' ' in c else c for c in cmds)) - return subprocess.call(cmds) - finally: - os.unlink(fp.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): """ @@ -989,6 +1016,7 @@ def scp(self, arguments): dst_instance, dst_is_host, dst = dst.partition(':') src_instance, src_is_host, src = src.partition(':') + # TODO: review this if dst_is_host and src_is_host: puts_err(colored.red("Both src and host are host destinations")) sys.exit(1) @@ -1039,15 +1067,28 @@ def ip(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - self.instance_name = self.activate(instance_name) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - lookup = self.enable_ip_lookup - ip = vmrun.getGuestIPAddress(lookup=lookup) - if ip: - puts_err(colored.green(ip)) + if instance_name: + # single instance + instances = [instance_name] else: - puts_err(colored.red("Unknown IP address")) + # TODO: Does it make sense to have multiple? + # multiple instances + instances = self.instances() + + for instance in instances: + self.activate(instance) + + if self.created: + vmrun = VMrun(self.vmx, user=self.user, password=self.password) + lookup = self.enable_ip_lookup + ip = vmrun.getGuestIPAddress(lookup=lookup) + if ip: + puts_err(colored.green(ip)) + else: + puts_err(colored.red("Unknown IP address")) + else: + puts_err(colored.yellow("VM not created")) def provision(self, arguments): """ @@ -1059,44 +1100,54 @@ def provision(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - self.instance_name = self.activate(instance_name) - vmrun = VMrun(self.vmx, self.user, self.password) + if instance_name: + # single instance + instances = [instance_name] + else: + # multiple instances + instances = self.instances() + + for instance in instances: + self.activate(instance) - if not vmrun.installedTools(): - puts_err(colored.red("Tools not installed")) - return + vmrun = VMrun(self.vmx, self.user, self.password) - provisioned = 0 - for i, provision in enumerate(self.get('provision', [])): + if not vmrun.installedTools(): + puts_err(colored.red("Tools not installed")) + return - 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 + provisioned = 0 + for i, provision in enumerate(self.get('provision', [])): + + # TODO: re-review this + 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 else: - puts_err(colored.red("Not Provisioned ({}".format(i))) + puts_err(colored.green("Provisioned {} entries".format(provisioned))) return - else: - puts_err(colored.green("Provisioned {} entries".format(provisioned))) - return - puts_err(colored.red("Not Provisioned ({}".format(i))) + puts_err(colored.red("Not Provisioned ({}".format(i))) def reload(self, arguments): """ @@ -1109,29 +1160,38 @@ def reload(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - self.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.enable_ip_lookup - ip = vmrun.getGuestIPAddress(lookup=lookup) - if ip: - if started: - puts_err(colored.green("VM started on {}".format(ip))) - else: - puts_err(colored.yellow("VM already was started on {}".format(ip))) + # multiple instances + instances = self.instances() + + for instance in instances: + self.activate(instance) + + 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")) else: - if started: - puts_err(colored.green("VM started on an unknown IP address")) + time.sleep(3) + puts_err(colored.blue("Getting IP address...")) + lookup = self.enable_ip_lookup + ip = vmrun.getGuestIPAddress(lookup=lookup) + if ip: + if started: + puts_err(colored.green("VM started on {}".format(ip))) + else: + puts_err(colored.yellow("VM already was started on {}".format(ip))) else: - puts_err(colored.yellow("VM already was started on an unknown IP address")) + 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")) def port(self, arguments): """ @@ -1145,17 +1205,26 @@ def port(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - self.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() + # TODO: implement port forwarding? + for instance in instances: + self.activate(instance) + + 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 + else: + puts_err(colored.red("Cannot find a nat network")) def list(self, arguments): """ @@ -1177,7 +1246,6 @@ def list(self, arguments): )) for name in self.mechfile: self.activate(name) - #print('name:{} box:{} created:{}'.format(name, self.box, self.created)) if self.created: vmrun = VMrun(self.vmx, user=self.user, password=self.password) lookup = self.enable_ip_lookup diff --git a/mech/utils.py b/mech/utils.py index 2ed256f..24a9e9c 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -38,7 +38,6 @@ from shutil import copyfile import requests -from filelock import Timeout, FileLock from clint.textui import colored, puts_err from clint.textui import progress @@ -102,7 +101,7 @@ def confirm(prompt, default='y'): def save_mechfile(mechfile, name, path): - multiple_mechfiles = { name: mechfile } + multiple_mechfiles = {name: mechfile} with open(os.path.join(path, 'Mechfile'), 'w+') as fp: json.dump(multiple_mechfiles, fp, sort_keys=True, indent=2, separators=(',', ': ')) return True @@ -171,69 +170,6 @@ def update_vmx(path, numvcpus=None, memsize=None): # vmrun.upgradevm() -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())) - 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) - except json.decoder.JSONDecodeError: - puts_err( - colored.red( - textwrap.fill( - "Index file seems broken. Try to remove {}.".format(index_path)))) - sys.exit(1) - - -def settle_instance(instance_name, obj=None, force=False): - print('in settle_instance instance_name:{}'.format(instance_name)) - 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())) - print('instances:{}'.format(instances)) - 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) - except json.decoder.JSONDecodeError: - puts_err( - colored.red( - textwrap.fill( - "Index file seems broken. Try to remove {}.".format(index_path)))) - sys.exit(1) - - def load_mechfile(): mechfile_full = os.path.join(os.path.expanduser(os.getcwd()), 'Mechfile') if os.path.isfile(mechfile_full): @@ -371,13 +307,10 @@ def init_box( puts_err(colored.red("Cannot find a valid box with a VMX file in it")) sys.exit(1) - #print('box:{}'.format(box)) box_parts = box.split('/') box_dir = os.path.join(*filter(None, (MECH_DIR, 'boxes', box_parts[0], box_parts[1], box_version))) - #print('box_dir:{}'.format(box_dir)) box_file = locate(box_dir, '*.box') - #print('box_file:{}'.format(box_file)) puts_err(colored.blue("Extracting box '{}'...".format(box_file))) makedirs(instance_path) @@ -402,10 +335,9 @@ def init_box( os.unlink(box) vmx = locate(instance_path, '*.vmx') - if not vmx and not silent: + if not vmx: puts_err(colored.red("Cannot locate a VMX file")) sys.exit(1) - #print('in init_box name:{} vmx:{}'.format(name, vmx)) update_vmx(vmx, numvcpus=numvcpus, memsize=memsize) @@ -419,7 +351,6 @@ def add_box(name=None, box=None, box_version=None, force=False, save=True, reque name=name, box_version=box_version, requests_kwargs=requests_kwargs) - #print('mechfile:{}'.format(mechfile)) return add_mechfile( mechfile, @@ -464,7 +395,6 @@ def add_box_url(name, box, box_version, url, force=False, save=True, requests_kw box_dir = os.path.join(*filter(None, (MECH_DIR, 'boxes', box_parts[0], box_parts[1], box_version))) exists = os.path.exists(box_dir) - print("box_parts:{} box_dir:{} exists:{}".format(box_parts, box_dir, exists)) if not exists or force: if exists: puts_err(colored.blue("Attempting to download box '{}'...".format(box))) @@ -559,23 +489,8 @@ def add_box_file(box, box_version, filename, url=None, force=False, save=True): return box, box_version -def index_active_instance(name): - path = os.path.join(MECH_DIR, name) - print('in index_active_index path:{}'.format(path)) - instance = settle_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(name, instance.get('path'))))) - sys.exit(1) - return path - - def init_mechfile(box=None, name=None, box_version=None, requests_kwargs={}): path = os.path.expanduser(os.getcwd()) - print("in init_mechfile name:{}".format(name)) mechfile = build_mechfile( box, name=name, From fff0393031cc4a4f3ce9c459ef1807c8d85f62db Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 2 Feb 2020 11:09:27 -0800 Subject: [PATCH 013/116] add docstrings; get scp working --- mech/command.py | 5 ++ mech/mech.py | 130 +++++++++++++++++++++++------------------------- mech/utils.py | 41 +++++++++++---- 3 files changed, 96 insertions(+), 80 deletions(-) diff --git a/mech/command.py b/mech/command.py index 28531f2..af36d18 100644 --- a/mech/command.py +++ b/mech/command.py @@ -20,6 +20,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # +"""Handle the mech commands.""" from __future__ import absolute_import @@ -35,6 +36,7 @@ def cmd_usage(doc): + """Show the command usage.""" return doc.replace(NBSP, ' ') @@ -42,6 +44,7 @@ def cmd_usage(doc): def docopt_extras(help, version, options, doc): + """Show the extra help info.""" return docopt_extras_ref(help, version, options, cmd_usage(doc)) @@ -54,6 +57,7 @@ def DocoptExit____init__(self, message=''): def spaced(name): + """Return the name?""" name = re.sub(r'[ _]+', r' ', name) name = re.sub(r'(?<=[^_])([A-Z])', r' \1', name).lower() return re.sub(r'^( *)(.*?)( *)$', r'\2', name) @@ -103,4 +107,5 @@ def __call__(self): return obj def run(self): + """Run the command.""" raise docopt.DocoptExit() diff --git a/mech/mech.py b/mech/mech.py index 0a6949e..57c7284 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -75,10 +75,14 @@ -----END RSA PRIVATE KEY----- """ +MAIN_DIR = os.path.expanduser(os.getcwd()) MECH_DIR = os.path.expanduser(os.getcwd() + '/.mech') class MechCommand(Command): + """Class to hold the Mechfile (as python object) and an active + instance's configuration. + """ mechfile = None active_name = None created = False @@ -89,6 +93,7 @@ class MechCommand(Command): user = None password = None enable_ip_lookup = False + config = {} def activate_mechfile(self): """Load the Mechfile.""" @@ -116,6 +121,7 @@ def deactivate(self): self.user = None self.password = None self.enable_ip_lookup = False + self.config = {} os.chdir(MECH_DIR) def activate(self, name): @@ -172,11 +178,9 @@ def enable_ip_lookup(self): """Enable ip lookup (defaults to False).""" return self.enable_ip_lookup - @property - def config(self): - return self.get('config', {}).get('ssh', {}) +# def config(self): +# return self.config.get('ssh', {}) - @property def config_ssh(self): vmrun = VMrun(self.vmx, user=self.user, password=self.password) lookup = self.enable_ip_lookup @@ -195,7 +199,7 @@ def config_ssh(self): with open(insecure_private_key, 'w') as f: f.write(INSECURE_PRIVATE_KEY) os.chmod(insecure_private_key, 0o400) - config = { + self.config = { "Host": self.active_name, "User": self.user, "Port": "22", @@ -217,11 +221,11 @@ def callback(pat): k = re.sub(r' (\w)', callback, k) if k[0].islower(): k = k[0].upper() + k[1:] - config[k] = v - config.update({ + self.config[k] = v + self.config.update({ "HostName": ip, }) - return config + return self.config class MechBox(MechCommand): @@ -945,13 +949,13 @@ def ssh_config(self, arguments): for instance in instances: self.activate(instance) - print(utils.config_ssh_string(self.config_ssh)) + print(utils.config_ssh_string(self.config_ssh())) def ssh(self, arguments): """ Connects to machine via SSH. - Usage: mech ssh [options] [] [-- ...] + Usage: mech ssh [options] [-- ...] Options: -c, --command COMMAND Execute an SSH command directly @@ -964,59 +968,50 @@ def ssh(self, arguments): instance_name = arguments[''] - if instance_name: - # single instance - instances = [instance_name] - else: - # multiple instances - instances = self.instances() + self.activate(instance_name) - for instance in instances: - self.activate(instance) + config_ssh = self.config_ssh() + fp = tempfile.NamedTemporaryFile(delete=False) + try: + fp.write(utils.config_ssh_string(config_ssh).encode('utf-8')) + fp.close() - 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) + 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): """ 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(':') - # TODO: review this if dst_is_host and src_is_host: puts_err(colored.red("Both src and host are host destinations")) sys.exit(1) @@ -1029,12 +1024,18 @@ def scp(self, arguments): else: src = src_instance - self.instance_name = self.activate(instance_name) + self.activate(instance_name) - config_ssh = self.config_ssh + config_ssh = self.config_ssh() fp = tempfile.NamedTemporaryFile(delete=False) + + # when we activate, we change to the directory where the .vmx file is + # since we are trying to copy files, we need to go back to the + # directory that we started in + os.chdir(MAIN_DIR) + try: - fp.write(utils.config_ssh_string(config_ssh)) + fp.write(utils.config_ssh_string(config_ssh).encode()) fp.close() cmds = ['scp'] @@ -1061,34 +1062,25 @@ def ip(self, arguments): """ Outputs ip of the Mech machine. - Usage: mech ip [options] [] + Usage: mech ip [options] Options: -h, --help Print this help """ instance_name = arguments[''] - if instance_name: - # single instance - instances = [instance_name] - else: - # TODO: Does it make sense to have multiple? - # multiple instances - instances = self.instances() - - for instance in instances: - self.activate(instance) + self.activate(instance_name) - if self.created: - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - lookup = self.enable_ip_lookup - ip = vmrun.getGuestIPAddress(lookup=lookup) - if ip: - puts_err(colored.green(ip)) - else: - puts_err(colored.red("Unknown IP address")) + if self.created: + vmrun = VMrun(self.vmx, user=self.user, password=self.password) + lookup = self.enable_ip_lookup + ip = vmrun.getGuestIPAddress(lookup=lookup) + if ip: + puts_err(colored.green(ip)) else: - puts_err(colored.yellow("VM not created")) + puts_err(colored.red("Unknown IP address")) + else: + puts_err(colored.yellow("VM not created")) def provision(self, arguments): """ diff --git a/mech/utils.py b/mech/utils.py index 24a9e9c..d629d25 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -51,6 +51,7 @@ def makedirs(name, mode=0o777): + """Make directories with mode supplied.""" try: os.makedirs(name, mode) except OSError: @@ -58,6 +59,7 @@ def makedirs(name, mode=0o777): def uncomment(text): + """Uncomment a line of text.""" def e(m): return '\x00%02x' % ord(m.group(1)) e.re = re.compile(r'\\(.)', re.DOTALL | re.MULTILINE) @@ -79,6 +81,7 @@ def u(m): def confirm(prompt, default='y'): + """Confirmation prompt.""" default = default.lower() if default not in ['y', 'n']: default = 'y' @@ -101,6 +104,7 @@ def confirm(prompt, default='y'): def save_mechfile(mechfile, name, path): + """Save the Mechfile.""" multiple_mechfiles = {name: mechfile} with open(os.path.join(path, 'Mechfile'), 'w+') as fp: json.dump(multiple_mechfiles, fp, sort_keys=True, indent=2, separators=(',', ': ')) @@ -108,6 +112,7 @@ def save_mechfile(mechfile, name, path): def locate(path, glob): + """Locate a file in the path provided.""" for root, dirnames, filenames in os.walk(path): for filename in filenames: if fnmatch.fnmatch(filename, glob): @@ -115,6 +120,7 @@ def locate(path, glob): def parse_vmx(path): + """Parse the virtual machine configuration (.vmx) file.""" vmx = collections.OrderedDict() with open(path) as fp: for line in fp: @@ -125,6 +131,9 @@ def parse_vmx(path): def update_vmx(path, numvcpus=None, memsize=None): + """Update the virtual machine configuration (.vmx) + file with desired settings. + """ updated = False vmx = parse_vmx(path) @@ -171,6 +180,7 @@ def update_vmx(path, numvcpus=None, memsize=None): def load_mechfile(): + """Load the Mechfile from disk.""" mechfile_full = os.path.join(os.path.expanduser(os.getcwd()), 'Mechfile') if os.path.isfile(mechfile_full): with open(mechfile_full) as fp: @@ -190,6 +200,7 @@ def load_mechfile(): def build_mechfile(descriptor, name=None, box_version=None, requests_kwargs={}): + """Build the Mechfile from the inputs.""" mechfile = {} if descriptor is None: return mechfile @@ -241,6 +252,7 @@ def build_mechfile(descriptor, name=None, box_version=None, requests_kwargs={}): def catalog_to_mechfile(catalog, name=None, version=None): + """Convert the Hashicorp cloud catalog entry to Mechfile entry.""" mechfile = {} versions = catalog.get('versions', []) for v in versions: @@ -262,6 +274,7 @@ def catalog_to_mechfile(catalog, name=None, version=None): def tar_cmd(*args, **kwargs): + """Build the tar command to be used to extract the box.""" try: startupinfo = None if os.name == "nt": @@ -285,16 +298,13 @@ def tar_cmd(*args, **kwargs): return tar -def init_box( - name, - box, - box_version, - force=False, - save=True, - instance_path=None, - requests_kwargs={}, - numvcpus=None, - memsize=None): +def init_box(name, box, box_version, force=False, save=True, + instance_path=None, requests_kwargs={}, numvcpus=None, + memsize=None): + """Initialize the box. This includes uncompressing the files + from the box file and updating the vmx file with + desired settings. + """ if not locate(instance_path, '*.vmx'): name_version_box = add_box( name=name, @@ -340,11 +350,13 @@ def init_box( sys.exit(1) update_vmx(vmx, numvcpus=numvcpus, memsize=memsize) - return vmx def add_box(name=None, box=None, box_version=None, force=False, save=True, requests_kwargs={}): + """Add a box. + TODO: Not quite sure why we have this function. + """ # build the dict mechfile = build_mechfile( box, @@ -390,6 +402,7 @@ def add_mechfile(mechfile, name=None, box=None, box_version=None, def add_box_url(name, box, box_version, url, force=False, save=True, requests_kwargs={}): + """Add a box using the URL.""" boxname = os.path.basename(url) box_parts = box.split('/') box_dir = os.path.join(*filter(None, (MECH_DIR, 'boxes', @@ -448,6 +461,7 @@ def add_box_url(name, box, box_version, url, force=False, save=True, requests_kw def add_box_file(box, box_version, filename, url=None, force=False, save=True): + """Add a box using a file as the source.""" puts_err(colored.blue("Checking box '{}' integrity filename:{}...".format(box, filename))) if sys.platform == 'win32': @@ -490,6 +504,7 @@ def add_box_file(box, box_version, filename, url=None, force=False, save=True): def init_mechfile(box=None, name=None, box_version=None, requests_kwargs={}): + """Initialize the Mechfile.""" path = os.path.expanduser(os.getcwd()) mechfile = build_mechfile( box, @@ -501,6 +516,7 @@ def init_mechfile(box=None, name=None, box_version=None, requests_kwargs={}): def get_requests_kwargs(arguments): + """Get the requests key word arguments.""" requests_kwargs = {} if arguments['--insecure']: requests_kwargs['verify'] = False @@ -514,10 +530,12 @@ def get_requests_kwargs(arguments): def provision_file(vm, source, destination): + """Provision from file.""" return vm.copyFileFromHostToGuest(source, destination) def provision_shell(vm, inline, path, args=[]): + """Provision from shell.""" tmp_path = vm.createTempfileInGuest() if tmp_path is None: return @@ -569,6 +587,7 @@ def provision_shell(vm, inline, path, args=[]): def config_ssh_string(config_ssh): + """Build the ssh-config string.""" ssh_config = "Host {}".format(config_ssh['Host']) + os.linesep for k, v in config_ssh.items(): if k != 'Host': From 377097d2873fabbcbc1ed6144252978bd7bd40d0 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 2 Feb 2020 11:22:57 -0800 Subject: [PATCH 014/116] test that snapshot add/list/delete work as expected --- mech/mech.py | 70 ++++++++++++++-------------------------------------- 1 file changed, 18 insertions(+), 52 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 57c7284..b2a88ab 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -178,9 +178,6 @@ def enable_ip_lookup(self): """Enable ip lookup (defaults to False).""" return self.enable_ip_lookup -# def config(self): -# return self.config.get('ssh', {}) - def config_ssh(self): vmrun = VMrun(self.vmx, user=self.user, password=self.password) lookup = self.enable_ip_lookup @@ -368,7 +365,7 @@ def delete(self, arguments): """ Delete a snapshot taken previously with snapshot save. - Usage: mech snapshot delete [options] [] + Usage: mech snapshot delete [options] Options: -h, --help Print this help @@ -376,22 +373,13 @@ def delete(self, arguments): name = arguments[''] instance_name = arguments[''] + self.activate(instance_name) - if instance_name: - # single instance - instances = [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: - # multiple instances - instances = self.instances() - - for instance in instances: - self.activate(instance) - - 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))) + puts_err(colored.green("Snapshot {} deleted".format(name))) def list(self, arguments): """ @@ -414,44 +402,33 @@ def list(self, arguments): for instance in instances: self.activate(instance) vmrun = VMrun(self.vmx, user=self.user, password=self.password) + print('Snapshots for instance:{}'.format(instance)) print(vmrun.listSnapshots()) def save(self, arguments): """ Take a snapshot of the current state of the machine. - Usage: mech snapshot save [options] [] + 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. + 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: - -f --force Replace snapshot without confirmation -h, --help Print this help """ name = arguments[''] - instance_name = arguments[''] - if instance_name: - # single instance - instances = [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. Please provide a name.")) else: - # multiple instances - instances = self.instances() - - for instance in instances: - self.activate(instance) - 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))) + puts_err(colored.green("Snapshot {} taken".format(name))) class Mech(MechCommand): @@ -484,7 +461,6 @@ class Mech(MechCommand): 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 For help on any individual command run `mech -h` @@ -657,7 +633,6 @@ def global_status(self, arguments): Usage: mech global-status [options] Options: - --prune Prune invalid entries -h, --help Print this help """ vmrun = VMrun() @@ -667,24 +642,15 @@ def ps(self, arguments): """ List running processes in Guest OS. - Usage: mech ps [options] [] + Usage: mech ps [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: - self.activate(instance) - vmrun = VMrun(self.vmx, self.user, self.password) - print(vmrun.listProcessesInGuest()) + self.activate(instance_name) + vmrun = VMrun(self.vmx, self.user, self.password) + print(vmrun.listProcessesInGuest()) def status(self, arguments): """ From 46333dcfbe07d0ac01489d8d377a7659df7b9326 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 2 Feb 2020 11:46:43 -0800 Subject: [PATCH 015/116] validate mech box add/list/remove work as expected --- mech/mech.py | 78 ++++++++++++++-------------------------------------- 1 file changed, 20 insertions(+), 58 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index b2a88ab..439079e 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -231,10 +231,8 @@ class MechBox(MechCommand): Available subcommands: add add a box to the catalog of available boxes - list list available boxes in the catalog - outdated checks for outdated boxes - remove removes a box that matches the given name - update + (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` """ @@ -243,12 +241,10 @@ def add(self, arguments): """ Add a box to the catalog of available boxes. - Usage: mech box add [options] [] [] + 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. + The location can be a URL, local .box file, or 'bento/ubuntu-18.04'. Options: -f, --force Overwrite an existing box if it exists @@ -261,16 +257,14 @@ def add(self, arguments): --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'] + + location = arguments[''] + box_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) + utils.add_box(name=None, box=location, box_version=box_version, + force=force, requests_kwargs=requests_kwargs) def list(self, arguments): """ @@ -296,57 +290,25 @@ def list(self, arguments): "{}/{}".format(account, box).rjust(35), version.rjust(12), )) + # add alias for 'mech box ls' 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 remove(self, arguments): """ - Remove a box from mech that matches the given name. + Remove a box from mech that matches the given name and version. - Usage: mech box remove [options] + 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 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!")) + name = arguments[''] + box_version = arguments[''] + path = os.path.abspath(os.path.join(MECH_DIR, 'boxes', name, box_version)) + if os.path.exists(path): + shutil.rmtree(path) + # add alias for 'mech box delete' + delelete = remove class MechSnapshot(MechCommand): From e6faeb6111e096c8e123af2f360a8105ea0444a9 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 2 Feb 2020 12:11:34 -0800 Subject: [PATCH 016/116] update help info --- mech/mech.py | 60 ++++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 439079e..84ba7c8 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -273,7 +273,6 @@ def list(self, arguments): Usage: mech box list [options] Options: - -i, --box-info Displays additional information about the boxes -h, --help Print this help """ @@ -308,7 +307,7 @@ def remove(self, arguments): if os.path.exists(path): shutil.rmtree(path) # add alias for 'mech box delete' - delelete = remove + delete = remove class MechSnapshot(MechCommand): @@ -316,8 +315,8 @@ 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 + (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` @@ -342,6 +341,8 @@ def delete(self, arguments): puts_err(colored.red("Cannot delete name")) else: puts_err(colored.green("Snapshot {} deleted".format(name))) + # add alias for 'mech snapshot remove' + remove = delete def list(self, arguments): """ @@ -366,6 +367,8 @@ def list(self, arguments): vmrun = VMrun(self.vmx, user=self.user, password=self.password) print('Snapshots for instance:{}'.format(instance)) print(vmrun.listSnapshots()) + # add alias for 'mech snapshot ls' + ls = list def save(self, arguments): """ @@ -405,23 +408,23 @@ class Mech(MechCommand): 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 + destroy stops and deletes all traces of the instances + (up|start) starts instances (aka virtual machines) + (down|stop|halt) stops the instances + suspend suspends the instances + pause pauses the instances + ssh connects to an instance via SSH + ssh-config outputs OpenSSH valid configuration to connect to the instances + scp copies files to/from the machine via SCP + ip outputs ip of an instance + box manages boxes: add, list remove, etc. + global-status outputs status of all virutal machines on this host + status outputs status of the instances + ps list running processes for an instance 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. + snapshot manages snapshots: save, list, remove, etc. port displays information about guest port mappings For help on any individual command run `mech -h` @@ -430,7 +433,7 @@ class Mech(MechCommand): 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 """ @@ -514,7 +517,6 @@ def up(self, arguments): Options: --gui Start GUI --disable-shared-folder Do not share folder with VM - --provision Enable provisioning --insecure Do not validate SSL certificates --cacert FILE CA certificate for SSL download --capath DIR CA certificate directory for SSL download @@ -590,7 +592,7 @@ def up(self, arguments): def global_status(self, arguments): """ - Outputs mech environments status for this user. + Outputs info about all VMs running on this computer. Usage: mech global-status [options] @@ -616,7 +618,7 @@ def ps(self, arguments): def status(self, arguments): """ - Outputs status of the Mech machine. + Outputs status of the instances. Usage: mech status [options] [] @@ -660,7 +662,7 @@ def status(self, arguments): 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] [] @@ -709,7 +711,7 @@ def destroy(self, arguments): def down(self, arguments): """ - Stops the Mech machine. + Stops the instances. Usage: mech down [options] [] @@ -747,7 +749,7 @@ def down(self, arguments): def pause(self, arguments): """ - Pauses the Mech machine. + Pauses the instances. Usage: mech pause [options] [] @@ -773,12 +775,11 @@ def pause(self, arguments): def resume(self, arguments): """ - Resume a paused/suspended Mech machine. + Resume a paused/suspended instances. Usage: mech resume [options] [] Options: - --provision Enable provisioning -h, --help Print this help """ instance_name = arguments[''] @@ -833,7 +834,7 @@ def resume(self, arguments): def suspend(self, arguments): """ - Suspends the machine. + Suspends instances. Usage: mech suspend [options] [] @@ -1076,7 +1077,6 @@ def reload(self, arguments): Usage: mech reload [options] [] Options: - --provision Enable provisioning -h, --help Print this help """ instance_name = arguments[''] @@ -1133,7 +1133,7 @@ def port(self, arguments): # multiple instances instances = self.instances() - # TODO: implement port forwarding? + # FUTURE: implement port forwarding? for instance in instances: self.activate(instance) From f89d14950c14f5b5efb2e844fc718c051e662ea3 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 2 Feb 2020 22:16:17 -0800 Subject: [PATCH 017/116] add bats and simple int tests --- CONTRIBUTING.md | 6 ++- mech/mech.py | 7 +++ tests/int/simple.bats | 112 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100755 tests/int/simple.bats diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8cc656f..85fbd8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,8 +28,12 @@ pip install flake8 # also optional pip install autopep8 -# use like this: autopep8 --in-place --aggressive --aggressive somefile.py +# use like this: autopep8 --in-place --aggressive --aggressive somefile.py # Configure git to use pre-commit hook flake8 --install-hook git + +# install bats (on mac) +# see https://github.com/bats-core/bats-core +brew install bats-core ``` diff --git a/mech/mech.py b/mech/mech.py index 84ba7c8..1bf4852 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -429,6 +429,8 @@ class Mech(MechCommand): 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: @@ -475,6 +477,9 @@ def init(self, arguments): --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) -h, --help Print this help """ + # TODO: with provision example + # TODO: multiple names? + # TODO: multiple boxes? name = arguments['--name'] box_version = arguments['--box-version'] @@ -535,6 +540,7 @@ def up(self, arguments): numvcpus = arguments['--numvcpus'] memsize = arguments['--memsize'] + # TODO: disabled_shared_folder/cpus/ram from config instance_name = arguments[''] @@ -818,6 +824,7 @@ def resume(self, arguments): lookup = self.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) puts_err(colored.blue("Sharing current folder...")) + # TODO: param vmrun.enableSharedFolders() vmrun.addSharedFolder('mech', os.getcwd(), quiet=True) if ip: diff --git a/tests/int/simple.bats b/tests/int/simple.bats new file mode 100755 index 0000000..8dfdf79 --- /dev/null +++ b/tests/int/simple.bats @@ -0,0 +1,112 @@ +#!/usr/bin/env bats +# +# simple.bats - run some simple tests +# +# Note: must be run from this directory +# like this: ./simple.bats + +@test "help" { + run mech --help + [ "$status" -eq 0 ] + + # cleanup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true +} + +@test "no args" { + run mech + [ "$status" -eq 1 ] + [ "$output" = "Usage: mech [options] [...]" ] + + # cleanup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true +} + +@test "version" { + run mech --version + [ "$status" -eq 0 ] + regex='mech v[0-9\.]+' + [[ "$output" =~ $regex ]] + + # cleanup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true +} + +@test "no Mechfile" { + cd no_mechfile + + run mech ls + [ "$status" -eq 1 ] + regex="Couldn't find a Mechfile" + [[ "$output" =~ $regex ]] + + # cleanup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + cd .. +} + +# Note: Using alpine because the image is the smallest I could find. +# This will download the box from internet. +@test "mech init, up, destroy of alpine" { + cd init_mechfile + + # setup + # ensure there is no Mechfile first + if [ -f Mechfile ]; then + rm -f Mechfile + fi + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + run mech init + [ "$status" -eq 1 ] + regex="Usage: mech init " + [[ "$output" =~ $regex ]] + + run mech init mrlesmithjr/alpine311 + regex1="Initializing" + regex2="Loading metadata" + regex3="has been initialized" + regex4="mech up" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + [[ "$output" =~ $regex4 ]] + [ -f Mechfile ] + + run mech up + regex1="Loading metadata" + regex2="could not be found" + regex3="vmware_desktop" + regex4="integrity filename" + regex5=".vmx" + regex6="Extracting" + regex7="Added network" + regex8="Bringing machine up" + regex9="Getting IP" + regex10="Sharing current folder" + regex11="VM started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + [[ "$output" =~ $regex4 ]] + [[ "$output" =~ $regex5 ]] + [[ "$output" =~ $regex6 ]] + [[ "$output" =~ $regex7 ]] + [[ "$output" =~ $regex8 ]] + [[ "$output" =~ $regex9 ]] + [[ "$output" =~ $regex10 ]] + [[ "$output" =~ $regex11 ]] + + run mech destroy -f + regex1="Deleting" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # clean up + rm Mechfile + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + cd .. +} From 1e263c3886a44700da8ff911d4b1ff28232f40c1 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 2 Feb 2020 22:43:35 -0800 Subject: [PATCH 018/116] add two instances test; added name to up/delete --- mech/mech.py | 6 +-- tests/int/two_ubuntu.bats | 86 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) create mode 100755 tests/int/two_ubuntu.bats diff --git a/mech/mech.py b/mech/mech.py index 1bf4852..77702a0 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -569,7 +569,7 @@ def up(self, arguments): self.created = True vmrun = VMrun(vmx, user=self.user, password=self.password) - puts_err(colored.blue("Bringing machine up...")) + puts_err(colored.blue("Bringing machine ({}) up...".format(instance))) started = vmrun.start(gui=gui) if started is None: puts_err(colored.red("VM not started")) @@ -697,7 +697,7 @@ def destroy(self, arguments): if force or utils.confirm( "Are you sure you want to delete {} at {}".format( self.active_name, instance_path), default='n'): - puts_err(colored.green("Deleting...")) + puts_err(colored.green("Deleting ({})...".format(instance))) vmrun = VMrun(self.vmx, user=self.user, password=self.password) vmrun.stop(mode='hard', quiet=True) time.sleep(3) @@ -713,7 +713,7 @@ def destroy(self, arguments): else: puts_err(colored.red("Deletion aborted")) else: - puts_err(colored.red("The box hasn't been initialized.")) + puts_err(colored.red("The box ({}) hasn't been initialized.".format(instance))) def down(self, arguments): """ diff --git a/tests/int/two_ubuntu.bats b/tests/int/two_ubuntu.bats new file mode 100755 index 0000000..c800f48 --- /dev/null +++ b/tests/int/two_ubuntu.bats @@ -0,0 +1,86 @@ +#!/usr/bin/env bats +# +# two_ubuntu.bats - run some tests with multiple ubuntu instances +# +# Note: must be run from the tests/int directory +# like this: ./two_ubuntu.bats + +@test "mech up/destroy of two ubuntu instances (first and second)" { + cd two_ubuntu + + # setup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + run mech box ls + [ "$status" -eq 0 ] + + run mech box list + [ "$status" -eq 0 ] + + # ensure they can start the first time + run mech up + regex1="first" + regex2="second" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + + # ensure running 'up' again is not an issue + run mech up + regex1="first" + regex2="second" + regex3="already started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + + run mech ls + regex1="first" + regex2="second" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + + run mech status + regex1="first" + regex2="second" + regex3="Tools running" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + + run mech destroy -f + regex1="first" + regex2="second" + regex3="Deleting" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + + run mech box list + regex1="ubuntu" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech box remove bento/ubuntu-18.04 + regex1="Usage: mech box remove " + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + run mech box remove bento/ubuntu-18.04 201912.04.0 + [ "$status" -eq 0 ] + + # make sure there is not ubuntu box + run mech box list + regex1="ubuntu" + [ "$status" -eq 0 ] + ! [[ "$output" =~ $regex1 ]] + + # clean up + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + cd .. +} From 82e800b6658949ee34453b66724563708b4dc358 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 2 Feb 2020 22:54:00 -0800 Subject: [PATCH 019/116] document the int tests --- CONTRIBUTING.md | 5 +++++ tests/int/README.md | 15 +++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 tests/int/README.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85fbd8c..0e833ba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,4 +36,9 @@ flake8 --install-hook git # install bats (on mac) # see https://github.com/bats-core/bats-core brew install bats-core + +# for testing/validation, we have some integration tests +cd tests/int +./simple.bats +./two_ubuntu.bats ``` diff --git a/tests/int/README.md b/tests/int/README.md new file mode 100644 index 0000000..3af520d --- /dev/null +++ b/tests/int/README.md @@ -0,0 +1,15 @@ +# Integration tests for mech + +This directory contains files for integration testing. + +# Notes: +1) Test must be run from this directory. +2) Tests should be able to be run concurrently. +3) If a test fails, there may be some vmx processes still running. +4) See ../CONTRIBUTING.md for getting setup to run tests. +5) These tests will take several minutes to run. They download +files from the internet and start up/stop/destroy VMs. + +# Scripts +./simple.bats - run some simple tests with Alpine image +./two_ubuntu.bats - validate two ubuntu instances work as expected From 57d8021fdde085e2f410ae24cedf6533cb07291f Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 2 Feb 2020 23:25:06 -0800 Subject: [PATCH 020/116] update doc/TODOs --- README.md | 43 +++++++++++++++++++++---------------------- mech/mech.py | 4 +++- tests/int/README.md | 7 ++++--- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 720500e..ac2e293 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # mech One of the authors made this because they don't like VirtualBox and wanted to use vagrant -with VMWare Fusion but was too cheap to buy the Vagrant plugin. +with VMmare Fusion but was too cheap to buy the Vagrant plugin. https://blog.kchung.co/mech-vagrant-with-vmware-integration-for-free/ @@ -16,27 +16,26 @@ 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 + (list|ls) lists all available boxes + init initializes a new Mech environment by creating a Mechfile + destroy stops and deletes all traces of the instances + (up|start) starts instances (aka virtual machines) + (down|stop|halt) stops the instances + suspend suspends the instances + pause pauses the instances + ssh connects to an instance via SSH + ssh-config outputs OpenSSH valid configuration to connect to the instances + scp copies files to/from the machine via SCP + ip outputs ip of an instance + box manages boxes: add, list remove, etc. + global-status outputs status of all virutal machines on this host + status outputs status of the instances + ps list running processes for an instance + provision provisions the Mech machine + reload restarts Mech machine, loads new Mechfile configuration + resume resume a paused/suspended Mech machine + snapshot manages snapshots: save, list, remove, etc. + port displays information about guest port mappings For help on any individual command run `mech -h` diff --git a/mech/mech.py b/mech/mech.py index 77702a0..bdafe48 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -537,6 +537,7 @@ def up(self, arguments): disable_shared_folder = not arguments['--disable-shared-folder'] save = not arguments['--no-cache'] requests_kwargs = utils.get_requests_kwargs(arguments) + # TODO: --no-provision option numvcpus = arguments['--numvcpus'] memsize = arguments['--memsize'] @@ -824,7 +825,7 @@ def resume(self, arguments): lookup = self.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) puts_err(colored.blue("Sharing current folder...")) - # TODO: param + # TODO: honor param vmrun.enableSharedFolders() vmrun.addSharedFolder('mech', os.getcwd(), quiet=True) if ip: @@ -1028,6 +1029,7 @@ def provision(self, arguments): -h, --help Print this help """ instance_name = arguments[''] + # TODO: flip the provisioned flag in Mechfile if instance_name: # single instance diff --git a/tests/int/README.md b/tests/int/README.md index 3af520d..2973784 100644 --- a/tests/int/README.md +++ b/tests/int/README.md @@ -6,10 +6,11 @@ This directory contains files for integration testing. 1) Test must be run from this directory. 2) Tests should be able to be run concurrently. 3) If a test fails, there may be some vmx processes still running. -4) See ../CONTRIBUTING.md for getting setup to run tests. +4) See [CONTRIBUTING](../../CONTRIBUTING.md) for getting setup to run tests. 5) These tests will take several minutes to run. They download files from the internet and start up/stop/destroy VMs. # Scripts -./simple.bats - run some simple tests with Alpine image -./two_ubuntu.bats - validate two ubuntu instances work as expected + +- `./simple.bats` - run some simple tests with Alpine image +- `./two_ubuntu.bats` - validate two ubuntu instances work as expected From 38842c66e0ffab67cde3ca36ce8bcda7908f2dad Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 3 Feb 2020 08:01:46 -0800 Subject: [PATCH 021/116] add direnv --- .envrc | 2 ++ CONTRIBUTING.md | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 .envrc 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/CONTRIBUTING.md b/CONTRIBUTING.md index 0e833ba..0c73554 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,4 +41,7 @@ brew install bats-core cd tests/int ./simple.bats ./two_ubuntu.bats + +# consider installing/using direnv (there is a .envrc in this repo) +# may need to run "direnv allow" ``` From 8109fffba68dac81b11831fa5d102a335a35ac68 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 3 Feb 2020 08:18:56 -0800 Subject: [PATCH 022/116] migrate TODO notes into project board --- mech/mech.py | 10 ---------- mech/utils.py | 4 +--- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index bdafe48..96831e8 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -477,10 +477,6 @@ def init(self, arguments): --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) -h, --help Print this help """ - # TODO: with provision example - # TODO: multiple names? - # TODO: multiple boxes? - name = arguments['--name'] box_version = arguments['--box-version'] box = arguments[''] @@ -537,11 +533,9 @@ def up(self, arguments): disable_shared_folder = not arguments['--disable-shared-folder'] save = not arguments['--no-cache'] requests_kwargs = utils.get_requests_kwargs(arguments) - # TODO: --no-provision option numvcpus = arguments['--numvcpus'] memsize = arguments['--memsize'] - # TODO: disabled_shared_folder/cpus/ram from config instance_name = arguments[''] @@ -825,7 +819,6 @@ def resume(self, arguments): lookup = self.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) puts_err(colored.blue("Sharing current folder...")) - # TODO: honor param vmrun.enableSharedFolders() vmrun.addSharedFolder('mech', os.getcwd(), quiet=True) if ip: @@ -1029,7 +1022,6 @@ def provision(self, arguments): -h, --help Print this help """ instance_name = arguments[''] - # TODO: flip the provisioned flag in Mechfile if instance_name: # single instance @@ -1049,8 +1041,6 @@ def provision(self, arguments): provisioned = 0 for i, provision in enumerate(self.get('provision', [])): - - # TODO: re-review this if provision.get('type') == 'file': source = provision.get('source') destination = provision.get('destination') diff --git a/mech/utils.py b/mech/utils.py index d629d25..6ee9bfe 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -354,9 +354,7 @@ def init_box(name, box, box_version, force=False, save=True, def add_box(name=None, box=None, box_version=None, force=False, save=True, requests_kwargs={}): - """Add a box. - TODO: Not quite sure why we have this function. - """ + """Add a box.""" # build the dict mechfile = build_mechfile( box, From a76f29e24bc54340e6162d38afc7000069b85e2b Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 3 Feb 2020 13:32:18 -0800 Subject: [PATCH 023/116] change debug logging; pass named values; validate creating box from file --- mech/mech.py | 80 ++++++++++++------- mech/utils.py | 141 ++++++++++++++++++++-------------- tests/int/README.md | 5 +- tests/int/init_from_file.bats | 63 +++++++++++++++ tests/int/simple.bats | 4 +- 5 files changed, 205 insertions(+), 88 deletions(-) create mode 100755 tests/int/init_from_file.bats diff --git a/mech/mech.py b/mech/mech.py index 96831e8..34a7dc6 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -43,7 +43,6 @@ logger = logging.getLogger(__name__) -DEFAULT_HOST = 'mech' DEFAULT_USER = 'vagrant' DEFAULT_PASSWORD = 'vagrant' INSECURE_PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY----- @@ -75,9 +74,6 @@ -----END RSA PRIVATE KEY----- """ -MAIN_DIR = os.path.expanduser(os.getcwd()) -MECH_DIR = os.path.expanduser(os.getcwd() + '/.mech') - class MechCommand(Command): """Class to hold the Mechfile (as python object) and an active @@ -89,6 +85,7 @@ class MechCommand(Command): box = None box_version = None url = None + box_file = None vmx = None user = None password = None @@ -98,6 +95,7 @@ class MechCommand(Command): def activate_mechfile(self): """Load the Mechfile.""" self.mechfile = utils.load_mechfile() + logger.debug("loaded mechfile:{}".format(self.mechfile)) def instances(self): """Returns a list of the instances from the Mechfile.""" @@ -108,7 +106,7 @@ def instances(self): @staticmethod def instance_path(name): """Return the path for an instance.""" - return os.path.join(MECH_DIR, name) + return os.path.join(utils.mech_dir(), name) def deactivate(self): """Make it so we do have have an active instance selected.""" @@ -117,12 +115,13 @@ def deactivate(self): self.box = None self.box_version = None self.url = None + self.box_file = None self.vmx = None self.user = None self.password = None self.enable_ip_lookup = False self.config = {} - os.chdir(MECH_DIR) + os.chdir(utils.mech_dir()) def activate(self, name): """Sets the active instance name and changes to the @@ -139,6 +138,7 @@ def activate(self, name): self.box = self.mechfile[name].get('box', None) self.box_version = self.mechfile[name].get('box_version', None) self.url = self.mechfile[name].get('url', None) + self.box_file = self.mechfile[name].get('file', None) self.user = DEFAULT_USER self.password = DEFAULT_PASSWORD path = MechCommand.instance_path(name) @@ -191,7 +191,8 @@ def config_ssh(self): ))) sys.exit(1) - insecure_private_key = os.path.abspath(os.path.join(MECH_DIR, "insecure_private_key")) + insecure_private_key = os.path.abspath(os.path.join( + utils.mech_dir(), "insecure_private_key")) if not os.path.exists(insecure_private_key): with open(insecure_private_key, 'w') as f: f.write(INSECURE_PRIVATE_KEY) @@ -280,7 +281,7 @@ def list(self, arguments): 'BOX'.rjust(35), 'VERSION'.rjust(12), )) - path = os.path.abspath(os.path.join(MECH_DIR, 'boxes')) + path = os.path.abspath(os.path.join(utils.mech_dir(), '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:] @@ -289,6 +290,7 @@ def list(self, arguments): "{}/{}".format(account, box).rjust(35), version.rjust(12), )) + # add alias for 'mech box ls' ls = list @@ -303,9 +305,10 @@ def remove(self, arguments): """ name = arguments[''] box_version = arguments[''] - path = os.path.abspath(os.path.join(MECH_DIR, 'boxes', name, box_version)) + path = os.path.abspath(os.path.join(utils.mech_dir(), 'boxes', name, box_version)) if os.path.exists(path): shutil.rmtree(path) + # add alias for 'mech box delete' delete = remove @@ -341,6 +344,7 @@ def delete(self, arguments): puts_err(colored.red("Cannot delete name")) else: puts_err(colored.green("Snapshot {} deleted".format(name))) + # add alias for 'mech snapshot remove' remove = delete @@ -367,6 +371,7 @@ def list(self, arguments): vmrun = VMrun(self.vmx, user=self.user, password=self.password) print('Snapshots for instance:{}'.format(instance)) print(vmrun.listSnapshots()) + # add alias for 'mech snapshot ls' ls = list @@ -447,7 +452,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']: @@ -460,7 +466,7 @@ def init(self, arguments): """ Initializes a new mech environment by creating a Mechfile. - Usage: mech init [options] + Usage: mech init [options] Example box: bento/ubuntu-18.04 @@ -479,7 +485,8 @@ def init(self, arguments): """ name = arguments['--name'] box_version = arguments['--box-version'] - box = arguments[''] + box = arguments['--box'] + location = arguments[''] if not name or name == "": name = "first" @@ -487,6 +494,9 @@ def init(self, arguments): force = arguments['--force'] requests_kwargs = utils.get_requests_kwargs(arguments) + logger.debug('name:{} box:{} box_version:{} location:{}'.format( + name, box, box_version, location)) + if os.path.exists('Mechfile') and not force: puts_err(colored.red(textwrap.fill( "`Mechfile` already exists in this directory. Remove it " @@ -496,7 +506,8 @@ def init(self, arguments): puts_err(colored.green("Initializing mech")) if utils.init_mechfile( - box, + location=location, + box=box, name=name, box_version=box_version, requests_kwargs=requests_kwargs): @@ -550,10 +561,15 @@ def up(self, arguments): self.activate(instance) instance_path = MechCommand.instance_path(instance) + location = self.url + if not location: + location = self.box_file + vmx = utils.init_box( instance, box=self.box, box_version=self.box_version, + location=location, instance_path=instance_path, requests_kwargs=requests_kwargs, save=save, @@ -579,14 +595,18 @@ def up(self, arguments): vmrun.addSharedFolder('mech', os.getcwd(), quiet=True) if ip: if started: - puts_err(colored.green("VM started on {}".format(ip))) + puts_err(colored.green("VM ({}) started " + "on {}".format(instance, ip))) else: - puts_err(colored.yellow("VM was already started on {}".format(ip))) + puts_err(colored.yellow("VM ({}) was already started " + "on {}".format(instance, ip))) else: if started: - puts_err(colored.green("VM started on an unknown IP address")) + puts_err(colored.green("VM ({}) started on an unknown " + "IP address".format(instance))) else: - puts_err(colored.yellow("VM was already started on an unknown IP address")) + puts_err(colored.yellow("VM ({}) was already started on an " + "unknown IP address".format(instance))) # allows "mech start" to alias to "mech up" start = up @@ -823,15 +843,18 @@ def resume(self, arguments): vmrun.addSharedFolder('mech', os.getcwd(), quiet=True) if ip: if started: - puts_err(colored.green("VM started on {}".format(ip))) + puts_err(colored.green("VM ({}) started on " + "{}".format(instance, ip))) else: - puts_err(colored.yellow("VM already was started on {}".format(ip))) + puts_err(colored.yellow("VM ({}) already was started " + "on {}".format(instance, ip))) else: if started: - puts_err(colored.green("VM started on an unknown IP address")) + puts_err(colored.green("VM ({}) started on an unknown " + "IP address".format(instance))) else: - puts_err(colored.yellow("VM already was started " - "on an unknown IP address")) + puts_err(colored.yellow("VM ({}) already was started " + "on an unknown IP address".format(instance))) def suspend(self, arguments): """ @@ -962,7 +985,7 @@ def scp(self, arguments): # when we activate, we change to the directory where the .vmx file is # since we are trying to copy files, we need to go back to the # directory that we started in - os.chdir(MAIN_DIR) + os.chdir(utils.main_dir()) try: fp.write(utils.config_ssh_string(config_ssh).encode()) @@ -1103,14 +1126,17 @@ def reload(self, arguments): ip = vmrun.getGuestIPAddress(lookup=lookup) if ip: if started: - puts_err(colored.green("VM started on {}".format(ip))) + puts_err(colored.green("VM ({}) started on {}".format(instance, ip))) else: - puts_err(colored.yellow("VM already was started on {}".format(ip))) + puts_err(colored.yellow("VM ({}) already was started on " + "{}".format(instance, ip))) else: if started: - puts_err(colored.green("VM started on an unknown IP address")) + puts_err(colored.green("VM ({}) started on an unknown IP " + "address".format(instance))) else: - puts_err(colored.yellow("VM already was started on an unknown IP address")) + puts_err(colored.yellow("VM ({}) already was started " + "on an unknown IP address".format(instance))) def port(self, arguments): """ diff --git a/mech/utils.py b/mech/utils.py index 6ee9bfe..9154067 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -46,8 +46,17 @@ logger = logging.getLogger(__name__) -MECH_DIR = os.path.expanduser(os.getcwd() + '/.mech') -DATA_DIR = os.path.join(MECH_DIR, 'data') +MAIN_DIR = os.path.expanduser(os.getcwd()) + + +def main_dir(): + """Return the main directory.""" + return MAIN_DIR + + +def mech_dir(): + """Return the mech directory.""" + return os.path.join(MAIN_DIR, '.mech') def makedirs(name, mode=0o777): @@ -106,6 +115,7 @@ def confirm(prompt, default='y'): def save_mechfile(mechfile, name, path): """Save the Mechfile.""" multiple_mechfiles = {name: mechfile} + logger.debug("path:{}".format(path)) with open(os.path.join(path, 'Mechfile'), 'w+') as fp: json.dump(multiple_mechfiles, fp, sort_keys=True, indent=2, separators=(',', ': ')) return True @@ -181,7 +191,8 @@ def update_vmx(path, numvcpus=None, memsize=None): def load_mechfile(): """Load the Mechfile from disk.""" - mechfile_full = os.path.join(os.path.expanduser(os.getcwd()), 'Mechfile') + mechfile_full = os.path.join(main_dir(), 'Mechfile') + logger.debug("mechfile_full:{}".format(mechfile_full)) if os.path.isfile(mechfile_full): with open(mechfile_full) as fp: try: @@ -190,54 +201,57 @@ def load_mechfile(): puts_err(colored.red("Invalid Mechfile." + os.linesep)) else: puts_err(colored.red(textwrap.fill( - "Couldn't find a Mechfile in the current directory any deeper directories. " + "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'd " + "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." ))) sys.exit(1) -def build_mechfile(descriptor, name=None, box_version=None, requests_kwargs={}): +def build_mechfile(location, box=None, name=None, box_version=None, requests_kwargs={}): """Build the Mechfile from the inputs.""" + logger.debug("location:{} name:{} box:{} box_version:{}".format( + location, name, box, box_version)) mechfile = {} - if descriptor is None: + if location is None: return mechfile mechfile['name'] = name - if any(descriptor.startswith(s) for s in ('https://', 'http://', 'ftp://')): - mechfile['url'] = descriptor + if any(location.startswith(s) for s in ('https://', 'http://', 'ftp://')): + mechfile['url'] = location if not name: - name = os.path.splitext(os.path.basename(descriptor))[0] - mechfile['box'] = name + name = 'first' + mechfile['box'] = box if box_version: mechfile['box_version'] = box_version return mechfile - elif descriptor.startswith('file:') or os.path.isfile(re.sub(r'^file:(?://)?', '', descriptor)): - descriptor = re.sub(r'^file:(?://)?', '', descriptor) + elif location.startswith('file:') or os.path.isfile(re.sub(r'^file:(?://)?', '', location)): + location = re.sub(r'^file:(?://)?', '', location) + logger.debug('location:{}'.format(location)) try: - with open(descriptor) as fp: + # see if the location is a json file + with open(location) 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 box_version: - mechfile['box_version'] = box_version - return mechfile + mechfile['file'] = location + if not name: + name = 'first' + mechfile['box'] = box + if box_version: + mechfile['box_version'] = box_version + logger.debug('mechfile:{}'.format(mechfile)) + return mechfile else: try: - account, box, v = (descriptor.split('/', 2) + ['', ''])[:3] + account, box, v = (location.split('/', 2) + ['', ''])[:3] if not account or not box: puts_err(colored.red("Provided box name is not valid")) if v: box_version = v puts_err( - colored.blue( - "Loading metadata for box '{}'{}".format( - descriptor, - " ({})".format(box_version) if box_version else ""))) + 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() @@ -248,16 +262,18 @@ def build_mechfile(descriptor, name=None, box_version=None, requests_kwargs={}): 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, box_version) + logger.debug("catalog:{} name:{} box_version:{}".format(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:{} name:{} box:{} box_version:{}'.format(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: + if not box_version or current_version == box_version: for provider in v['providers']: if 'vmware' in provider['name']: mechfile['name'] = name @@ -268,8 +284,7 @@ def catalog_to_mechfile(catalog, name=None, version=None): puts_err( colored.red( "Couldn't find a VMWare compatible VM for '{}'{}".format( - name, - " ({})".format(version) if version else ""))) + name, " ({})".format(box_version) if box_version else ""))) sys.exit(1) @@ -298,18 +313,21 @@ def tar_cmd(*args, **kwargs): return tar -def init_box(name, box, box_version, force=False, save=True, +def init_box(name, box, box_version, location=None, force=False, save=True, instance_path=None, requests_kwargs={}, numvcpus=None, memsize=None): """Initialize the box. This includes uncompressing the files from the box file and updating the vmx file with desired settings. """ + logger.debug("name:{} box:{} box_version:{} location:{}".format( + 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, requests_kwargs=requests_kwargs) @@ -318,7 +336,7 @@ def init_box(name, box, box_version, force=False, save=True, sys.exit(1) box_parts = box.split('/') - box_dir = os.path.join(*filter(None, (MECH_DIR, 'boxes', + box_dir = os.path.join(*filter(None, (mech_dir(), 'boxes', box_parts[0], box_parts[1], box_version))) box_file = locate(box_dir, '*.box') @@ -353,18 +371,24 @@ def init_box(name, box, box_version, force=False, save=True, return vmx -def add_box(name=None, box=None, box_version=None, force=False, save=True, requests_kwargs={}): +def add_box(name=None, box=None, box_version=None, location=None, + force=False, save=True, requests_kwargs={}): """Add a box.""" # build the dict + logger.debug('name:{} box:{} box_version:{} location:{}'.format( + name, box, box_version, location)) mechfile = build_mechfile( - box, + box=box, name=name, + location=location, box_version=box_version, requests_kwargs=requests_kwargs) return add_mechfile( mechfile, name=name, + box=box, + location=location, box_version=box_version, force=force, save=save, @@ -372,38 +396,37 @@ def add_box(name=None, box=None, box_version=None, force=False, save=True, reque def add_mechfile(mechfile, name=None, box=None, box_version=None, - force=False, save=True, requests_kwargs={}): + location=None, force=False, save=True, requests_kwargs={}): + logger.debug('mechfile:{} name:{} box:{} box_version:{} location:{}'.format( + mechfile, name, box, box_version, location)) + box = mechfile.get('box') name = mechfile.get('name') - version = mechfile.get('box_version') + box_version = mechfile.get('box_version') url = mechfile.get('url') box_file = mechfile.get('file') if box_file: - return add_box_file(box, version, box_file, force=force, save=save) + return add_box_file(box=box, box_version=box_version, filename=box_file, + force=force, save=save) if url: - return add_box_url( - name=name, - box=box, - box_version=box_version, - url=url, - force=force, - save=save, - requests_kwargs=requests_kwargs) + return add_box_url(name=name, box=box, box_version=box_version, + url=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 ""))) + "Could not find a VMWare compatible VM for '{}'{}".format( + name, " ({})".format(box_version) if box_version else ""))) def add_box_url(name, box, box_version, url, force=False, save=True, requests_kwargs={}): """Add a box using the URL.""" + logger.debug('name:{} box:{} box_version:{} url:{}'.format(name, box, box_version, url)) boxname = os.path.basename(url) box_parts = box.split('/') - box_dir = os.path.join(*filter(None, (MECH_DIR, 'boxes', + box_dir = os.path.join(*filter(None, (mech_dir(), 'boxes', box_parts[0], box_parts[1], box_version))) exists = os.path.exists(box_dir) if not exists or force: @@ -446,7 +469,9 @@ def add_box_url(name, box, box_version, url, force=False, save=True, requests_kw requests_kwargs=requests_kwargs) else: # Otherwise it must be a valid box: - return add_box_file(box, box_version, fp.name, url=url, force=force, save=save) + return add_box_file(box=box, box_version=box_version, + filename=fp.name, url=url, force=force, + save=save) finally: os.unlink(fp.name) except requests.HTTPError as exc: @@ -458,7 +483,7 @@ def add_box_url(name, box, box_version, url, force=False, save=True, requests_kw return name, box_version, box -def add_box_file(box, box_version, filename, url=None, force=False, save=True): +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.""" puts_err(colored.blue("Checking box '{}' integrity filename:{}...".format(box, filename))) @@ -491,7 +516,7 @@ def add_box_file(box, box_version, filename, url=None, force=False, save=True): if valid_tar: if save: boxname = os.path.basename(url if url else filename) - box = os.path.join(*filter(None, (MECH_DIR, 'boxes', box, box_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: @@ -501,15 +526,17 @@ def add_box_file(box, box_version, filename, url=None, force=False, save=True): return box, box_version -def init_mechfile(box=None, name=None, box_version=None, requests_kwargs={}): +def init_mechfile(location=None, box=None, name=None, box_version=None, requests_kwargs={}): """Initialize the Mechfile.""" - path = os.path.expanduser(os.getcwd()) + path = main_dir() + logger.debug("name:{} box:{} box_version:{} location:{} path:{}".format( + name, box, box_version, location, path)) mechfile = build_mechfile( - box, + location=location, + box=box, name=name, box_version=box_version, requests_kwargs=requests_kwargs) - print("saving mechfile:{} to path:{}".format(mechfile, path)) return save_mechfile(mechfile, name, path) diff --git a/tests/int/README.md b/tests/int/README.md index 2973784..e3a0745 100644 --- a/tests/int/README.md +++ b/tests/int/README.md @@ -12,5 +12,6 @@ files from the internet and start up/stop/destroy VMs. # Scripts -- `./simple.bats` - run some simple tests with Alpine image -- `./two_ubuntu.bats` - validate two ubuntu instances work as expected +- `./simple.bats` - simple validations +- `./two_ubuntu.bats` - validate two ubuntu instances +- `./init_from_file.bats` - validate create box from file diff --git a/tests/int/init_from_file.bats b/tests/int/init_from_file.bats new file mode 100755 index 0000000..af12f67 --- /dev/null +++ b/tests/int/init_from_file.bats @@ -0,0 +1,63 @@ +#!/usr/bin/env bats +# +# init_from_file.bats - init from a box file +# +# Note: must be run from this directory +# like this: ./init_from_file.bats + +@test "mech init, up, destroy of ubuntu from file" { + cd init_from_file + + ubuntu='ubuntu-18.04' + box_file="/tmp/${ubuntu}.box" + # setup + # ensure there is no Mechfile first + if [ -f Mechfile ]; then + rm -f Mechfile + fi + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + # download the file if we don't have it already + # that way we "cache" the file + if ! [ -f "$box_file" ]; then + wget -O "$box_file" "https://vagrantcloud.com/bento/boxes/${ubuntu}/versions/201912.04.0/providers/vmware_desktop.box" + fi + + run mech init --box "bento/${ubuntu}" "file:${box_file}" + regex1="Initializing" + regex2="has been initialized" + regex3="mech up" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + [ -f Mechfile ] + + run mech up + regex1="Checking box" + regex2="Extracting" + regex3="Added network" + regex4="Bringing machine" + regex5="Getting IP" + regex6="Sharing current folder" + regex7="started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + [[ "$output" =~ $regex4 ]] + [[ "$output" =~ $regex5 ]] + [[ "$output" =~ $regex6 ]] + [[ "$output" =~ $regex7 ]] + + run mech destroy -f + regex1="Deleting" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # clean up + rm Mechfile || true + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + cd .. +} diff --git a/tests/int/simple.bats b/tests/int/simple.bats index 8dfdf79..dbeb0ac 100755 --- a/tests/int/simple.bats +++ b/tests/int/simple.bats @@ -82,10 +82,10 @@ regex5=".vmx" regex6="Extracting" regex7="Added network" - regex8="Bringing machine up" + regex8="Bringing machine" regex9="Getting IP" regex10="Sharing current folder" - regex11="VM started" + regex11="started" [ "$status" -eq 0 ] [[ "$output" =~ $regex1 ]] [[ "$output" =~ $regex2 ]] From 19b917655f58a76e918304047ea0b696ed4bc386 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 3 Feb 2020 14:18:33 -0800 Subject: [PATCH 024/116] improve int testing --- mech/mech.py | 5 +- tests/int/README.md | 11 ++- tests/int/all | 10 +++ tests/int/no_mechfile.bats | 22 ++++++ tests/int/quick.bats | 33 +++++++++ tests/int/simple.bats | 148 +++++++++++++++++++++++++++---------- 6 files changed, 183 insertions(+), 46 deletions(-) create mode 100755 tests/int/all create mode 100755 tests/int/no_mechfile.bats create mode 100755 tests/int/quick.bats diff --git a/mech/mech.py b/mech/mech.py index 34a7dc6..75cce1c 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -396,9 +396,10 @@ def save(self, arguments): 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. Please provide a name.")) + puts_err(colored.red("Warning: Could not take snapshot.")) + sys.exit(1) else: - puts_err(colored.green("Snapshot {} taken".format(name))) + puts_err(colored.green("Snapshot ({}) on VM ({}) taken".format(name, instance_name))) class Mech(MechCommand): diff --git a/tests/int/README.md b/tests/int/README.md index e3a0745..925b096 100644 --- a/tests/int/README.md +++ b/tests/int/README.md @@ -1,17 +1,22 @@ # Integration tests for mech This directory contains files for integration testing. +You can run them all by running `./all`. # Notes: 1) Test must be run from this directory. -2) Tests should be able to be run concurrently. +2) Tests *should* be able to be run concurrently. (but no promises) 3) If a test fails, there may be some vmx processes still running. + You should be able to change to that directory's test and inspect + the last state. 4) See [CONTRIBUTING](../../CONTRIBUTING.md) for getting setup to run tests. 5) These tests will take several minutes to run. They download files from the internet and start up/stop/destroy VMs. # Scripts -- `./simple.bats` - simple validations +- `./init_from_file.bats` - create box from file +- `./no_mechfile.bats` - simple validations without a Mechfile +- `./quick.bats` - quick validations (ex: help, version) +- `./simple.bats` - simple validations of most basic functionality - `./two_ubuntu.bats` - validate two ubuntu instances -- `./init_from_file.bats` - validate create box from file diff --git a/tests/int/all b/tests/int/all new file mode 100755 index 0000000..b8b2621 --- /dev/null +++ b/tests/int/all @@ -0,0 +1,10 @@ +# all - run all integration tests +# +# Note: must be run from this directory +# like this: ./all + +./quick.bats +./no_mechfile.bats +./simple.bats +./two_ubuntu.bats +./init_from_file.bats diff --git a/tests/int/no_mechfile.bats b/tests/int/no_mechfile.bats new file mode 100755 index 0000000..4235708 --- /dev/null +++ b/tests/int/no_mechfile.bats @@ -0,0 +1,22 @@ +#!/usr/bin/env bats +# +# no_mechfile.bats - test without any Mechfile +# +# Note: must be run from this directory +# like this: ./no_mechfile.bats + +@test "no Mechfile" { + if ! [ -d no_mechfile ]; then + mkdir no_mechfile + fi + cd no_mechfile + + run mech ls + [ "$status" -eq 1 ] + regex="Couldn't find a Mechfile" + [[ "$output" =~ $regex ]] + + # cleanup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + cd .. +} diff --git a/tests/int/quick.bats b/tests/int/quick.bats new file mode 100755 index 0000000..3c010fd --- /dev/null +++ b/tests/int/quick.bats @@ -0,0 +1,33 @@ +#!/usr/bin/env bats +# +# quick.bats - run some quick tests +# +# Note: must be run from this directory +# like this: ./quick.bats + +@test "help" { + run mech --help + [ "$status" -eq 0 ] + + # cleanup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true +} + +@test "no args" { + run mech + [ "$status" -eq 1 ] + [ "$output" = "Usage: mech [options] [...]" ] + + # cleanup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true +} + +@test "version" { + run mech --version + [ "$status" -eq 0 ] + regex='mech v[0-9\.]+' + [[ "$output" =~ $regex ]] + + # cleanup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true +} diff --git a/tests/int/simple.bats b/tests/int/simple.bats index dbeb0ac..85c3dfd 100755 --- a/tests/int/simple.bats +++ b/tests/int/simple.bats @@ -5,50 +5,13 @@ # Note: must be run from this directory # like this: ./simple.bats -@test "help" { - run mech --help - [ "$status" -eq 0 ] - - # cleanup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true -} - -@test "no args" { - run mech - [ "$status" -eq 1 ] - [ "$output" = "Usage: mech [options] [...]" ] - - # cleanup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true -} - -@test "version" { - run mech --version - [ "$status" -eq 0 ] - regex='mech v[0-9\.]+' - [[ "$output" =~ $regex ]] - - # cleanup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true -} - -@test "no Mechfile" { - cd no_mechfile - - run mech ls - [ "$status" -eq 1 ] - regex="Couldn't find a Mechfile" - [[ "$output" =~ $regex ]] - - # cleanup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - cd .. -} - # Note: Using alpine because the image is the smallest I could find. # This will download the box from internet. @test "mech init, up, destroy of alpine" { - cd init_mechfile + if ! [ -d simple ]; then + mkdir simple + fi + cd simple # setup # ensure there is no Mechfile first @@ -99,6 +62,109 @@ [[ "$output" =~ $regex10 ]] [[ "$output" =~ $regex11 ]] + run mech list + regex1="first" + regex2="alpine" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + + # validate alias works, too + run mech ls + regex1="first" + regex2="alpine" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + + run mech box list + regex1="alpine" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # validate alias works, too + run mech box list + regex1="alpine" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + ## snapshots + run mech snapshot list + regex1="Total snapshots: 0" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # validate alias works, too + run mech snapshot ls + regex1="Total snapshots: 0" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # make sure params are used + run mech snapshot + regex1="Usage: mech snapshot " + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + # make sure params are used + run mech snapshot save + regex1="Usage: mech snapshot save" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + # make sure params are used + run mech snapshot save snap1 + regex1="Usage: mech snapshot save" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + run mech snapshot save snap1 first + regex1="taken" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # re-run with same params, should err + run mech snapshot save snap1 first + regex1="A snapshot with the name already exists" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + run mech snapshot ls + regex1="Total snapshots: 1" + regex2="snap1" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + + # make sure params are used + run mech snapshot delete + regex1="Usage: mech snapshot delete" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + # make sure params are used (alias) + run mech snapshot remove + regex1="Usage: mech snapshot delete" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + # make sure params are used + run mech snapshot delete snap1 + regex1="Usage: mech snapshot delete" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + # make sure params are valid + run mech snapshot delete snap1 first1 + regex1="Usage: mech snapshot delete" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + run mech snapshot delete snap1 first + regex1=" deleted" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + run mech destroy -f regex1="Deleting" [ "$status" -eq 0 ] From 122395194123e90fb8a57f5a377252b7421571f3 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 3 Feb 2020 14:49:16 -0800 Subject: [PATCH 025/116] add int testing for most functionality --- tests/int/simple.bats | 141 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/tests/int/simple.bats b/tests/int/simple.bats index 85c3dfd..7700434 100755 --- a/tests/int/simple.bats +++ b/tests/int/simple.bats @@ -62,6 +62,53 @@ [[ "$output" =~ $regex10 ]] [[ "$output" =~ $regex11 ]] + # make sure we can re-run 'up' + run mech up + regex1="Bringing machine" + regex2="Getting IP" + regex3="Sharing current folder" + regex4="was already started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + [[ "$output" =~ $regex4 ]] + + # make sure we can re-run 'up' with alias 'start' + run mech start + regex1="Bringing machine" + regex2="Getting IP" + regex3="Sharing current folder" + regex4="was already started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + [[ "$output" =~ $regex4 ]] + + # validate ps required arg + run mech ps + regex1="Usage: mech ps" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + run mech ps first + regex1="cmd=/sbin/init" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech status + regex1="first" + regex2="Tools running" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + + run mech global-status + regex1="simple/.mech/first/" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + run mech list regex1="first" regex2="alpine" @@ -77,6 +124,100 @@ [[ "$output" =~ $regex1 ]] [[ "$output" =~ $regex2 ]] + run mech stop + regex1="Stopped" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # try to stop a Stopped instance + run mech stop + regex1="Not stopped" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech start + regex1="started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech pause + regex1="Paused" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech resume + regex1="resumed" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech suspend + regex1="Suspended" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech resume + regex1="started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # ssh: incomplete args + run mech ssh + regex1="Usage: mech ssh" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + run mech ssh -c 'uptime' first + regex1="load average" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # other form of command + run mech ssh --command 'uptime' first + regex1="load average" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # scp: incomplete args + run mech scp + regex1="Usage: mech scp" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + date > now + run mech scp now first:/tmp + regex1="100%" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + rm now + + run mech scp first:/tmp/now . + regex1="100%" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + rm now + + # ip: incomplete args + run mech ip + regex1="Usage: mech ip" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + run mech ip first + regex1="^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # ip: invalid arg + run mech ip first2 + regex1="not found in the Mechfile" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + run mech port + regex1="Total port forwardings: 0" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + run mech box list regex1="alpine" [ "$status" -eq 0 ] From e014e3783a21cfd2e702f0fbe609aec1d3fed20c Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 3 Feb 2020 16:45:36 -0800 Subject: [PATCH 026/116] validate provisioning works as expected --- mech/mech.py | 57 +++++++++++++++++------ mech/utils.py | 11 +++-- tests/int/README.md | 1 + tests/int/all | 1 + tests/int/provision.bats | 82 ++++++++++++++++++++++++++++++++++ tests/int/provision/Mechfile | 52 +++++++++++++++++++++ tests/int/provision/file1.sh | 1 + tests/int/provision/file1.txt | 1 + tests/int/provision/file2.sh | 1 + tests/int/provision/file2.txt | 1 + tests/int/provision/readme.txt | 5 +++ tests/int/two_ubuntu/Mechfile | 14 ++++++ 12 files changed, 211 insertions(+), 16 deletions(-) create mode 100755 tests/int/provision.bats create mode 100644 tests/int/provision/Mechfile create mode 100644 tests/int/provision/file1.sh create mode 100644 tests/int/provision/file1.txt create mode 100644 tests/int/provision/file2.sh create mode 100644 tests/int/provision/file2.txt create mode 100644 tests/int/provision/readme.txt create mode 100644 tests/int/two_ubuntu/Mechfile diff --git a/mech/mech.py b/mech/mech.py index 75cce1c..55307bd 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -86,6 +86,7 @@ class MechCommand(Command): box_version = None url = None box_file = None + provision = None vmx = None user = None password = None @@ -116,6 +117,7 @@ def deactivate(self): self.box_version = None self.url = None self.box_file = None + self.provision = None self.vmx = None self.user = None self.password = None @@ -139,6 +141,7 @@ def activate(self, name): self.box_version = self.mechfile[name].get('box_version', None) self.url = self.mechfile[name].get('url', None) self.box_file = self.mechfile[name].get('file', None) + self.provision = self.mechfile[name].get('provision', None) self.user = DEFAULT_USER self.password = DEFAULT_PASSWORD path = MechCommand.instance_path(name) @@ -154,6 +157,15 @@ def activate(self, name): # change to the path of the instance os.chdir(path) + def __repr__(self): + """Return a representation of this object.""" + return ('active_name:{} created:{} box:{} box_version:{} ' + 'url:{} box_file:{} provision:{} vmx:{} user:{} password:{} ' + 'enable_ip_lookup:{} config:{}'.format(self.active_name, + self.created, self.box, self.box_version, self.url, self.box_file, + self.provision, self.vmx, self.user, self.password, + self.enable_ip_lookup, self.config)) + def vmx(self): """Get the fully qualified path to the vmx file.""" return self.vmx @@ -1043,8 +1055,10 @@ def provision(self, arguments): Usage: mech provision [options] [] Options: + -s, --show-only Show the provisioning info (do not run) -h, --help Print this help """ + show = arguments['--show-only'] instance_name = arguments[''] if instance_name: @@ -1057,41 +1071,58 @@ def provision(self, arguments): for instance in instances: self.activate(instance) - vmrun = VMrun(self.vmx, self.user, self.password) + puts_err(colored.green('Provisioning instance:{}'.format(instance))) + # cannot run provisioning if vmware tools are not installed + 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': + for i, provision in enumerate(self.provision): + provision_type = provision.get('type') + if provision_type == 'file': source = provision.get('source') + # Note: When we activate the instance, we change down to where the .vmx file + # is. For the source to "work", we need to pre-pend the main_dir(). + source = os.path.join(utils.main_dir(), source) destination = provision.get('destination') - if utils.provision_file(vmrun, source, destination) is None: - puts_err(colored.red("Not Provisioned")) - return + if show: + puts_err(colored.green(" instance:{} provision_type:{} source:{} destination:{}".format( + instance, provision_type, source, destination))) + else: + if utils.provision_file(vmrun, source, destination) is None: + puts_err(colored.red("Not Provisioned")) + return provisioned += 1 - elif provision.get('type') == 'shell': + elif provision_type == 'shell': inline = provision.get('inline') path = provision.get('path') + if path: + # Note: When we activate the instance, we change down to where the .vmx file + # is. For the source to "work", we need to pre-pend the main_dir(). + path = os.path.join(utils.main_dir(), 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 + if show: + puts_err(colored.green(" instance:{} provision_type:{} inline:{} path:{} args:{}".format( + instance, provision_type, inline, path, args))) + else: + 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 else: - puts_err(colored.green("Provisioned {} entries".format(provisioned))) - return + puts_err(colored.green("VM ({}) Provision {} entries".format(instance, provisioned))) - puts_err(colored.red("Not Provisioned ({}".format(i))) def reload(self, arguments): """ diff --git a/mech/utils.py b/mech/utils.py index 9154067..3051c44 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -555,13 +555,18 @@ def get_requests_kwargs(arguments): def provision_file(vm, source, destination): - """Provision from file.""" + """Provision from file. + This simply copies a file from host to guest. + """ + puts_err(colored.blue("Copying ({}) to ({})".format(source, destination))) return vm.copyFileFromHostToGuest(source, destination) def provision_shell(vm, inline, path, args=[]): """Provision from shell.""" tmp_path = vm.createTempfileInGuest() + logger.debug('inline:{} path:{} args:{} tmp_path:{}'.format( + inline, path, args, tmp_path)) if tmp_path is None: return @@ -590,10 +595,10 @@ def provision_shell(vm, inline, path, args=[]): puts_err(colored.red("No script to execute")) return - puts_err(colored.blue("Configuring script...")) + puts_err(colored.blue("Configuring script to run inline...")) fp = tempfile.NamedTemporaryFile(delete=False) try: - fp.write(inline) + fp.write(str.encode(inline)) fp.close() if vm.copyFileFromHostToGuest(fp.name, tmp_path) is None: return diff --git a/tests/int/README.md b/tests/int/README.md index 925b096..2b6eeeb 100644 --- a/tests/int/README.md +++ b/tests/int/README.md @@ -17,6 +17,7 @@ files from the internet and start up/stop/destroy VMs. - `./init_from_file.bats` - create box from file - `./no_mechfile.bats` - simple validations without a Mechfile +- `./provision.bats` - provision validation - `./quick.bats` - quick validations (ex: help, version) - `./simple.bats` - simple validations of most basic functionality - `./two_ubuntu.bats` - validate two ubuntu instances diff --git a/tests/int/all b/tests/int/all index b8b2621..773d0be 100755 --- a/tests/int/all +++ b/tests/int/all @@ -8,3 +8,4 @@ ./simple.bats ./two_ubuntu.bats ./init_from_file.bats +./provision.bats diff --git a/tests/int/provision.bats b/tests/int/provision.bats new file mode 100755 index 0000000..258bb89 --- /dev/null +++ b/tests/int/provision.bats @@ -0,0 +1,82 @@ +#!/usr/bin/env bats +# +# provision.bats - run provision tests +# +# Note: must be run from this directory +# like this: ./provision.bats + +@test "provision testing" { + cd provision + + # setup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + run mech up + regex1="VM (first) started" + regex2="VM (second) started" + regex3="VM (third) started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + + # show the provisioning + run mech provision -s + regex1="VM (first) Provision 2 entries" + regex2="VM (second) Provision 3 entries" + regex3="VM (third) Provision 0 entries" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + + # there should not be any files before file provisioning + run mech ssh -c "ls -al /tmp/file1.txt" + regex1="No such file or directory" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + # provision all + run mech provision + regex1="VM (first) Provision 2 entries" + regex2="VM (second) Provision 3 entries" + regex3="VM (third) Provision 0 entries" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + + # validate files were copied + run mech ssh -c "ls -al /tmp/file1.txt" first + regex1="vagrant" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # provision first (again) + run mech provision first + regex1="VM (first) Provision 2 entries" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # provision second (again) + run mech provision second + regex1="VM (second) Provision 3 entries" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # provision third (again) + run mech provision third + regex1="VM (third) Provision 0 entries" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech destroy -f + regex1="Deleting" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # clean up + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + cd .. +} 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/readme.txt b/tests/int/provision/readme.txt new file mode 100644 index 0000000..73bc030 --- /dev/null +++ b/tests/int/provision/readme.txt @@ -0,0 +1,5 @@ +This test exercises mech provisioning. + +file provisioning just copies over files + +shell provisioning executes the files/inline code diff --git a/tests/int/two_ubuntu/Mechfile b/tests/int/two_ubuntu/Mechfile new file mode 100644 index 0000000..573f088 --- /dev/null +++ b/tests/int/two_ubuntu/Mechfile @@ -0,0 +1,14 @@ +{ + "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" + } +} From 166d406e54aa07b4d88f3e96c907581b6c8289dd Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 4 Feb 2020 12:10:11 -0800 Subject: [PATCH 027/116] validate shared_folders functionality --- mech/mech.py | 122 ++++++++++++++++++------------ tests/int/README.md | 1 + tests/int/all | 1 + tests/int/provision.bats | 2 +- tests/int/shared_folders.bats | 78 +++++++++++++++++++ tests/int/shared_folders/Mechfile | 8 ++ 6 files changed, 162 insertions(+), 50 deletions(-) create mode 100755 tests/int/shared_folders.bats create mode 100644 tests/int/shared_folders/Mechfile diff --git a/mech/mech.py b/mech/mech.py index 55307bd..b4d7cb8 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -160,11 +160,12 @@ def activate(self, name): def __repr__(self): """Return a representation of this object.""" return ('active_name:{} created:{} box:{} box_version:{} ' - 'url:{} box_file:{} provision:{} vmx:{} user:{} password:{} ' - 'enable_ip_lookup:{} config:{}'.format(self.active_name, - self.created, self.box, self.box_version, self.url, self.box_file, - self.provision, self.vmx, self.user, self.password, - self.enable_ip_lookup, self.config)) + 'url:{} box_file:{} provision:{} vmx:{} user:{} ' + 'password:{} enable_ip_lookup:{} config:{}'. + format(self.active_name, self.created, self.box, + self.box_version, self.url, self.box_file, + self.provision, self.vmx, self.user, self.password, + self.enable_ip_lookup, self.config)) def vmx(self): """Get the fully qualified path to the vmx file.""" @@ -541,7 +542,7 @@ def up(self, arguments): Options: --gui Start GUI - --disable-shared-folder Do not share folder with VM + --disable-shared-folders Do not share folders with VM --insecure Do not validate SSL certificates --cacert FILE CA certificate for SSL download --capath DIR CA certificate directory for SSL download @@ -554,7 +555,7 @@ def up(self, arguments): -h, --help Print this help """ gui = arguments['--gui'] - disable_shared_folder = not arguments['--disable-shared-folder'] + disable_shared_folders = arguments['--disable-shared-folders'] save = not arguments['--no-cache'] requests_kwargs = utils.get_requests_kwargs(arguments) @@ -563,6 +564,9 @@ def up(self, arguments): instance_name = arguments[''] + logger.debug('gui:{} disable_shared_folders:{} save:{} numvcpus:{} memsize:{}'.format( + gui, disable_shared_folders, save, numvcpus, memsize)) + if instance_name: # single instance instances = [instance_name] @@ -602,10 +606,10 @@ def up(self, arguments): puts_err(colored.blue("Getting IP address...")) lookup = self.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) - puts_err(colored.blue("Sharing current folder...")) - if not disable_shared_folder: - vmrun.enableSharedFolders() - vmrun.addSharedFolder('mech', os.getcwd(), quiet=True) + if not disable_shared_folders: + puts_err(colored.blue("Sharing current folder...")) + vmrun.enableSharedFolders(quiet=False) + vmrun.addSharedFolder('mech', utils.main_dir(), quiet=True) if ip: if started: puts_err(colored.green("VM ({}) started " @@ -814,9 +818,14 @@ def resume(self, arguments): 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:{} ' + 'disable_shared_folders:{}'.format(instance_name, disable_shared_folders)) if instance_name: # single instance @@ -827,47 +836,61 @@ def resume(self, arguments): for instance in instances: self.activate(instance) + logger.debug('instance:{} self.vmx:{}'.format(instance, self.vmx)) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + # if we have started this instance before, try to unpause + if self.vmx: - # Try to unpause - if vmrun.unpause(quiet=True) is not None: - time.sleep(1) - puts_err(colored.blue("Getting IP address...")) - lookup = self.enable_ip_lookup - ip = vmrun.getGuestIPAddress(lookup=lookup) - if ip: - puts_err(colored.green("VM resumed on {}".format(ip))) - else: - puts_err(colored.green("VM resumed on an unknown IP address")) + vmrun = VMrun(self.vmx, user=self.user, password=self.password) - # Otherwise try starting - else: - started = vmrun.start() - if started is None: - puts_err(colored.red("VM not started")) - else: - time.sleep(3) + if vmrun.unpause(quiet=True) is not None: + time.sleep(1) puts_err(colored.blue("Getting IP address...")) lookup = self.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) - puts_err(colored.blue("Sharing current folder...")) - vmrun.enableSharedFolders() - vmrun.addSharedFolder('mech', os.getcwd(), quiet=True) + if not disable_shared_folders: + puts_err(colored.blue("Sharing current folder...")) + vmrun.enableSharedFolders(quiet=False) + vmrun.addSharedFolder('mech', utils.main_dir(), quiet=True) + else: + puts_err(colored.blue("Disabling shared folders...")) + vmrun.disableSharedFolders(quiet=False) if ip: - if started: - puts_err(colored.green("VM ({}) started on " - "{}".format(instance, ip))) - else: - puts_err(colored.yellow("VM ({}) already was started " - "on {}".format(instance, ip))) + puts_err(colored.green("VM resumed on {}".format(ip))) else: - if started: - puts_err(colored.green("VM ({}) started on an unknown " - "IP address".format(instance))) + puts_err(colored.green("VM resumed on an unknown IP address")) + + else: + # Otherwise try starting + vmrun = VMrun(self.vmx, user=self.user, password=self.password) + 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.enable_ip_lookup + ip = vmrun.getGuestIPAddress(lookup=lookup) + if not disable_shared_folders: + puts_err(colored.blue("Sharing current folder...")) + vmrun.enableSharedFolders(quiet=False) + vmrun.addSharedFolder('mech', utils.main_dir(), quiet=True) + if ip: + if started: + puts_err(colored.green("VM ({}) started on " + "{}".format(instance, ip))) + else: + puts_err(colored.yellow("VM ({}) already was started " + "on {}".format(instance, ip))) else: - puts_err(colored.yellow("VM ({}) already was started " - "on an unknown IP address".format(instance))) + if started: + puts_err(colored.green("VM ({}) started on an unknown " + "IP address".format(instance))) + else: + puts_err(colored.yellow("VM ({}) already was started " + "on an unknown IP address".format(instance))) + else: + puts_err(colored.red("Need to start VM first")) def suspend(self, arguments): """ @@ -1089,8 +1112,9 @@ def provision(self, arguments): source = os.path.join(utils.main_dir(), source) destination = provision.get('destination') if show: - puts_err(colored.green(" instance:{} provision_type:{} source:{} destination:{}".format( - instance, provision_type, source, destination))) + puts_err(colored.green(" instance:{} provision_type:{} source:{} " + "destination:{}".format(instance, provision_type, + source, destination))) else: if utils.provision_file(vmrun, source, destination) is None: puts_err(colored.red("Not Provisioned")) @@ -1109,8 +1133,8 @@ def provision(self, arguments): if not isinstance(args, list): args = [args] if show: - puts_err(colored.green(" instance:{} provision_type:{} inline:{} path:{} args:{}".format( - instance, provision_type, inline, path, args))) + puts_err(colored.green(" instance:{} provision_type:{} inline:{} path:{} " + "args:{}".format(instance, provision_type, inline, path, args))) else: if utils.provision_shell(vmrun, inline, path, args) is None: puts_err(colored.red("Not Provisioned")) @@ -1121,8 +1145,8 @@ def provision(self, arguments): puts_err(colored.red("Not Provisioned ({}".format(i))) return else: - puts_err(colored.green("VM ({}) Provision {} entries".format(instance, provisioned))) - + puts_err(colored.green("VM ({}) Provision {} " + "entries".format(instance, provisioned))) def reload(self, arguments): """ diff --git a/tests/int/README.md b/tests/int/README.md index 2b6eeeb..6c9e445 100644 --- a/tests/int/README.md +++ b/tests/int/README.md @@ -21,3 +21,4 @@ files from the internet and start up/stop/destroy VMs. - `./quick.bats` - quick validations (ex: help, version) - `./simple.bats` - simple validations of most basic functionality - `./two_ubuntu.bats` - validate two ubuntu instances +- `./shared_folders.bats` - validate shared folders functionality diff --git a/tests/int/all b/tests/int/all index 773d0be..66dfbc8 100755 --- a/tests/int/all +++ b/tests/int/all @@ -9,3 +9,4 @@ ./two_ubuntu.bats ./init_from_file.bats ./provision.bats +./shared_folders.bats diff --git a/tests/int/provision.bats b/tests/int/provision.bats index 258bb89..0387bac 100755 --- a/tests/int/provision.bats +++ b/tests/int/provision.bats @@ -31,7 +31,7 @@ [[ "$output" =~ $regex3 ]] # there should not be any files before file provisioning - run mech ssh -c "ls -al /tmp/file1.txt" + run mech ssh -c "ls -al /tmp/file1.txt" first regex1="No such file or directory" [ "$status" -eq 1 ] [[ "$output" =~ $regex1 ]] diff --git a/tests/int/shared_folders.bats b/tests/int/shared_folders.bats new file mode 100755 index 0000000..f7356a8 --- /dev/null +++ b/tests/int/shared_folders.bats @@ -0,0 +1,78 @@ +#!/usr/bin/env bats +# +# shared_folders.bats - run shared_folders tests +# +# Note: must be run from this directory +# like this: ./shared_folders.bats + +@test "shared folders testing" { + cd provision + + # setup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + run mech up + regex1="VM (first) started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first + regex1="Mechfile" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech stop + regex1="Stopped" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech up --disable-shared-folders + regex1="VM (first) started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first + regex1="No such file or directory" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + run mech pause + regex1="Paused" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech resume --disable-shared-folders + regex1="VM (first) started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first + regex1="No such file or directory" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + run mech pause + regex1="Paused" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech resume + regex1="VM (first) started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first + regex1="Mechfile" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech destroy -f + regex1="Deleting" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # clean up + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + cd .. +} diff --git a/tests/int/shared_folders/Mechfile b/tests/int/shared_folders/Mechfile new file mode 100644 index 0000000..6777f02 --- /dev/null +++ b/tests/int/shared_folders/Mechfile @@ -0,0 +1,8 @@ +{ + "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" + } +} \ No newline at end of file From 58c2b3295e81cc1ed0c7f43d5d8bcaeb0a89c209 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 4 Feb 2020 12:40:33 -0800 Subject: [PATCH 028/116] provision on start; add option to disable provisioining --- mech/mech.py | 66 ++++++--------------------------------- mech/utils.py | 67 ++++++++++++++++++++++++++++++++++++++++ tests/int/provision.bats | 12 ++++++- 3 files changed, 88 insertions(+), 57 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index b4d7cb8..90ffa4b 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -543,6 +543,7 @@ def up(self, arguments): Options: --gui Start GUI --disable-shared-folders Do not share folders with VM + --disable-provisioning Do not provision --insecure Do not validate SSL certificates --cacert FILE CA certificate for SSL download --capath DIR CA certificate directory for SSL download @@ -556,6 +557,7 @@ def up(self, arguments): """ 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) @@ -564,8 +566,10 @@ def up(self, arguments): instance_name = arguments[''] - logger.debug('gui:{} disable_shared_folders:{} save:{} numvcpus:{} memsize:{}'.format( - gui, disable_shared_folders, save, numvcpus, memsize)) + logger.debug('gui:{} disable_shared_folders:{} disable_provisioning:{} ' + 'save:{} numvcpus:{} memsize:{}'.format(gui, disable_shared_folders, + disable_provisioning, save, + numvcpus, memsize)) if instance_name: # single instance @@ -624,6 +628,9 @@ def up(self, arguments): else: puts_err(colored.yellow("VM ({}) was already started on an " "unknown IP address".format(instance))) + if not disable_provisioning: + utils.provision(instance, self.vmx, self.user, self.password, + self.provision, show=False) # allows "mech start" to alias to "mech up" start = up @@ -1093,60 +1100,7 @@ def provision(self, arguments): for instance in instances: self.activate(instance) - - puts_err(colored.green('Provisioning instance:{}'.format(instance))) - - # cannot run provisioning if vmware tools are not installed - 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.provision): - provision_type = provision.get('type') - if provision_type == 'file': - source = provision.get('source') - # Note: When we activate the instance, we change down to where the .vmx file - # is. For the source to "work", we need to pre-pend the main_dir(). - source = os.path.join(utils.main_dir(), source) - destination = provision.get('destination') - if show: - puts_err(colored.green(" instance:{} provision_type:{} source:{} " - "destination:{}".format(instance, provision_type, - source, destination))) - else: - if utils.provision_file(vmrun, source, destination) is None: - puts_err(colored.red("Not Provisioned")) - return - provisioned += 1 - - elif provision_type == 'shell': - inline = provision.get('inline') - path = provision.get('path') - if path: - # Note: When we activate the instance, we change down to where the .vmx file - # is. For the source to "work", we need to pre-pend the main_dir(). - path = os.path.join(utils.main_dir(), path) - - args = provision.get('args') - if not isinstance(args, list): - args = [args] - if show: - puts_err(colored.green(" instance:{} provision_type:{} inline:{} path:{} " - "args:{}".format(instance, provision_type, inline, path, args))) - else: - 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 - else: - puts_err(colored.green("VM ({}) Provision {} " - "entries".format(instance, provisioned))) + utils.provision(instance, self.vmx, self.user, self.password, self.provision, show) def reload(self, arguments): """ diff --git a/mech/utils.py b/mech/utils.py index 3051c44..c31e3ee 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -42,6 +42,7 @@ from clint.textui import progress from .compat import raw_input, b2s +from .vmrun import VMrun logger = logging.getLogger(__name__) @@ -554,6 +555,72 @@ def get_requests_kwargs(arguments): return requests_kwargs +def provision(instance, vmx, user, password, provision, show): + """Provision an instance.""" + + if instance == '': + puts_err(colored.red("Need to provide an instance to provision().")) + return + + if not vmx or not user or not password: + puts_err(colored.red("Need provide vmx/user/password to provision().")) + return + + puts_err(colored.green('Provisioning instance:{}'.format(instance))) + + vmrun = VMrun(vmx, user, password) + # cannot run provisioning if vmware tools are not installed + if not vmrun.installedTools(): + puts_err(colored.red("Cannot provision if VMware Tools are not installed.")) + return + + provisioned = 0 + for i, provision in enumerate(provision): + provision_type = provision.get('type') + if provision_type == 'file': + source = provision.get('source') + # Note: When we activate the instance, we change down to where the .vmx file + # is. For the source to "work", we need to pre-pend the main_dir(). + source = os.path.join(main_dir(), source) + destination = provision.get('destination') + if show: + puts_err(colored.green(" instance:{} provision_type:{} source:{} " + "destination:{}".format(instance, provision_type, + source, destination))) + else: + if provision_file(vmrun, source, destination) is None: + puts_err(colored.red("Not Provisioned")) + return + provisioned += 1 + + elif provision_type == 'shell': + inline = provision.get('inline') + path = provision.get('path') + if path: + # Note: When we activate the instance, we change down to where the .vmx file + # is. For the source to "work", we need to pre-pend the main_dir(). + path = os.path.join(main_dir(), path) + + args = provision.get('args') + if not isinstance(args, list): + args = [args] + if show: + puts_err(colored.green(" instance:{} provision_type:{} inline:{} path:{} " + "args:{}".format(instance, provision_type, inline, path, args))) + else: + if 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 + else: + puts_err(colored.green("VM ({}) Provision {} " + "entries".format(instance, provisioned))) + + def provision_file(vm, source, destination): """Provision from file. This simply copies a file from host to guest. diff --git a/tests/int/provision.bats b/tests/int/provision.bats index 0387bac..1008b87 100755 --- a/tests/int/provision.bats +++ b/tests/int/provision.bats @@ -11,7 +11,7 @@ # setup find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - run mech up + run mech up --disable-provisioning regex1="VM (first) started" regex2="VM (second) started" regex3="VM (third) started" @@ -70,6 +70,16 @@ [ "$status" -eq 0 ] [[ "$output" =~ $regex1 ]] + # ensure that provision runs on 'mech up' + run mech up + regex1="VM (first) Provision 2 entries" + regex2="VM (second) Provision 3 entries" + regex3="VM (third) Provision 0 entries" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + run mech destroy -f regex1="Deleting" [ "$status" -eq 0 ] From cb6fa814783e8f16298a46c733c9e7084cf85721 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 5 Feb 2020 09:35:03 -0800 Subject: [PATCH 029/116] fix bugs that running the int tests would have caught --- mech/mech.py | 1 + mech/utils.py | 81 ++++++++++++++++++++++++++------------------------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 90ffa4b..105a536 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -80,6 +80,7 @@ class MechCommand(Command): instance's configuration. """ mechfile = None + active_name = None created = False box = None diff --git a/mech/utils.py b/mech/utils.py index c31e3ee..b504092 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -575,50 +575,53 @@ def provision(instance, vmx, user, password, provision, show): return provisioned = 0 - for i, provision in enumerate(provision): - provision_type = provision.get('type') - if provision_type == 'file': - source = provision.get('source') - # Note: When we activate the instance, we change down to where the .vmx file - # is. For the source to "work", we need to pre-pend the main_dir(). - source = os.path.join(main_dir(), source) - destination = provision.get('destination') - if show: - puts_err(colored.green(" instance:{} provision_type:{} source:{} " - "destination:{}".format(instance, provision_type, - source, destination))) - else: - if provision_file(vmrun, source, destination) is None: - puts_err(colored.red("Not Provisioned")) - return - provisioned += 1 - - elif provision_type == 'shell': - inline = provision.get('inline') - path = provision.get('path') - if path: + if provision: + for i, p in enumerate(provision): + provision_type = p.get('type') + if provision_type == 'file': + source = p.get('source') # Note: When we activate the instance, we change down to where the .vmx file # is. For the source to "work", we need to pre-pend the main_dir(). - path = os.path.join(main_dir(), path) - - args = provision.get('args') - if not isinstance(args, list): - args = [args] - if show: - puts_err(colored.green(" instance:{} provision_type:{} inline:{} path:{} " - "args:{}".format(instance, provision_type, inline, path, args))) - else: - if provision_shell(vmrun, inline, path, args) is None: - puts_err(colored.red("Not Provisioned")) - return - provisioned += 1 + source = os.path.join(main_dir(), source) + destination = p.get('destination') + if show: + puts_err(colored.green(" instance:{} provision_type:{} source:{} " + "destination:{}".format(instance, provision_type, + source, destination))) + else: + if provision_file(vmrun, source, destination) is None: + puts_err(colored.red("Not Provisioned")) + return + provisioned += 1 + + elif provision_type == 'shell': + inline = p.get('inline') + path = p.get('path') + if path: + # Note: When we activate the instance, we change down to where the .vmx file + # is. For the source to "work", we need to pre-pend the main_dir(). + path = os.path.join(main_dir(), path) + + args = p.get('args') + if not isinstance(args, list): + args = [args] + if show: + puts_err(colored.green(" instance:{} provision_type:{} inline:{} path:{} " + "args:{}".format(instance, provision_type, inline, path, args))) + else: + if 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 else: - puts_err(colored.red("Not Provisioned ({}".format(i))) - return + puts_err(colored.green("VM ({}) Provision {} " + "entries".format(instance, provisioned))) else: - puts_err(colored.green("VM ({}) Provision {} " - "entries".format(instance, provisioned))) + puts_err(colored.blue("Nothing to provision")) def provision_file(vm, source, destination): From 6185a4468626ea34babf0fff5f81cac32ee416f5 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 5 Feb 2020 10:19:49 -0800 Subject: [PATCH 030/116] refactor to a MechInstance class --- mech/mech.py | 297 +++++++++++++++++++++++---------------------------- 1 file changed, 135 insertions(+), 162 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 105a536..5539822 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -75,74 +75,28 @@ """ -class MechCommand(Command): - """Class to hold the Mechfile (as python object) and an active - instance's configuration. - """ - mechfile = None - - active_name = None - created = False - box = None - box_version = None - url = None - box_file = None - provision = None - vmx = None - user = None - password = None - enable_ip_lookup = False - config = {} - - def activate_mechfile(self): - """Load the Mechfile.""" - self.mechfile = utils.load_mechfile() - logger.debug("loaded mechfile:{}".format(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) - - @staticmethod - def instance_path(name): - """Return the path for an instance.""" - return os.path.join(utils.mech_dir(), name) - - def deactivate(self): - """Make it so we do have have an active instance selected.""" - self.active_name = None - self.created = False - self.box = None - self.box_version = None - self.url = None - self.box_file = None - self.provision = None - self.vmx = None - self.user = None - self.password = None - self.enable_ip_lookup = False - self.config = {} - os.chdir(utils.mech_dir()) +class MechInstance(): - def activate(self, name): + def __init__(self, name, mechfile=None): """Sets the active instance name and changes to the directory for that instance.""" - if not self.mechfile: - self.activate_mechfile() if name == "": - raise AttributeError("Must activate with name.") - if self.mechfile.get(name, None): - self.active_name = name + raise AttributeError("Must provide a instance name.") + if not mechfile: + mechfile = utils.load_mechfile() + logger.debug("loaded mechfile:{}".format(mechfile)) + if mechfile.get(name, None): + self.name = name else: puts_err(colored.red("Instance ({}) was not found in the Mechfile".format(name))) sys.exit(1) - self.box = self.mechfile[name].get('box', None) - self.box_version = self.mechfile[name].get('box_version', None) - self.url = self.mechfile[name].get('url', None) - self.box_file = self.mechfile[name].get('file', None) - self.provision = self.mechfile[name].get('provision', None) + 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.user = DEFAULT_USER self.password = DEFAULT_PASSWORD path = MechCommand.instance_path(name) @@ -160,38 +114,14 @@ def activate(self, name): def __repr__(self): """Return a representation of this object.""" - return ('active_name:{} created:{} box:{} box_version:{} ' + return ('name:{} created:{} box:{} box_version:{} ' 'url:{} box_file:{} provision:{} vmx:{} user:{} ' 'password:{} enable_ip_lookup:{} config:{}'. - format(self.active_name, self.created, self.box, + format(self.name, self.created, self.box, self.box_version, self.url, self.box_file, self.provision, self.vmx, self.user, self.password, self.enable_ip_lookup, self.config)) - def vmx(self): - """Get the fully qualified path to the vmx file.""" - return self.vmx - - def box(self): - """Get the box (ex: 'bento/ubuntu-18.04').""" - return self.box - - def box_version(self): - """Get the box_version (ex: '2020-01-16').""" - return self.box_version - - def user(self): - """Get the username (ex: 'vagrant').""" - return self.user - - def password(self): - """Get the password (ex: 'temp123').""" - return self.password - - def enable_ip_lookup(self): - """Enable ip lookup (defaults to False).""" - return self.enable_ip_lookup - def config_ssh(self): vmrun = VMrun(self.vmx, user=self.user, password=self.password) lookup = self.enable_ip_lookup @@ -212,7 +142,7 @@ def config_ssh(self): f.write(INSECURE_PRIVATE_KEY) os.chmod(insecure_private_key, 0o400) self.config = { - "Host": self.active_name, + "Host": self.name, "User": self.user, "Port": "22", "UserKnownHostsFile": "/dev/null", @@ -240,6 +170,53 @@ def callback(pat): return self.config +class MechCommand(Command): + """Class to hold the Mechfile (as python object) and an active + instance's configuration. + """ + mechfile = None + + def activate_mechfile(self): + """Load the Mechfile.""" + self.mechfile = utils.load_mechfile() + logger.debug("loaded mechfile:{}".format(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) + + @staticmethod + def instance_path(name): + """Return the path for an instance.""" + return os.path.join(utils.mech_dir(), name) + + def vmx(self): + """Get the fully qualified path to the vmx file.""" + return self.vmx + + def box(self): + """Get the box (ex: 'bento/ubuntu-18.04').""" + return self.box + + def box_version(self): + """Get the box_version (ex: '2020-01-16').""" + return self.box_version + + def user(self): + """Get the username (ex: 'vagrant').""" + return self.user + + def password(self): + """Get the password (ex: 'temp123').""" + return self.password + + def enable_ip_lookup(self): + """Enable ip lookup (defaults to False).""" + return self.enable_ip_lookup + + class MechBox(MechCommand): """ Usage: mech box [...] @@ -350,10 +327,10 @@ def delete(self, arguments): """ name = arguments[''] - instance_name = arguments[''] - self.activate(instance_name) + instance = arguments[''] + inst = MechInstance(instance) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) if vmrun.deleteSnapshot(name) is None: puts_err(colored.red("Cannot delete name")) else: @@ -381,8 +358,8 @@ def list(self, arguments): instances = self.instances() for instance in instances: - self.activate(instance) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + inst = MechInstance(instance) + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) print('Snapshots for instance:{}'.format(instance)) print(vmrun.listSnapshots()) @@ -405,15 +382,15 @@ def save(self, arguments): -h, --help Print this help """ name = arguments[''] - instance_name = arguments[''] + instance = arguments[''] - self.activate(instance_name) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + inst = MechInstance(instance) + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) if vmrun.snapshot(name) is None: puts_err(colored.red("Warning: Could not take snapshot.")) sys.exit(1) else: - puts_err(colored.green("Snapshot ({}) on VM ({}) taken".format(name, instance_name))) + puts_err(colored.green("Snapshot ({}) on VM ({}) taken".format(name, instance))) class Mech(MechCommand): @@ -580,17 +557,17 @@ def up(self, arguments): instances = self.instances() for instance in instances: - self.activate(instance) + inst = MechInstance(instance) instance_path = MechCommand.instance_path(instance) - location = self.url + location = inst.url if not location: - location = self.box_file + location = inst.box_file vmx = utils.init_box( instance, - box=self.box, - box_version=self.box_version, + box=inst.box, + box_version=inst.box_version, location=location, instance_path=instance_path, requests_kwargs=requests_kwargs, @@ -598,10 +575,10 @@ def up(self, arguments): numvcpus=numvcpus, memsize=memsize) if vmx: - self.vmx = vmx - self.created = True + inst.vmx = vmx + inst.created = True - vmrun = VMrun(vmx, user=self.user, password=self.password) + vmrun = VMrun(vmx, user=inst.user, password=inst.password) puts_err(colored.blue("Bringing machine ({}) up...".format(instance))) started = vmrun.start(gui=gui) if started is None: @@ -609,7 +586,7 @@ def up(self, arguments): else: time.sleep(3) puts_err(colored.blue("Getting IP address...")) - lookup = self.enable_ip_lookup + lookup = inst.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) if not disable_shared_folders: puts_err(colored.blue("Sharing current folder...")) @@ -630,8 +607,8 @@ def up(self, arguments): puts_err(colored.yellow("VM ({}) was already started on an " "unknown IP address".format(instance))) if not disable_provisioning: - utils.provision(instance, self.vmx, self.user, self.password, - self.provision, show=False) + utils.provision(instance, inst.vmx, inst.user, inst.password, + inst.provision, show=False) # allows "mech start" to alias to "mech up" start = up @@ -657,9 +634,9 @@ def ps(self, arguments): Options: -h, --help Print this help """ - instance_name = arguments[''] - self.activate(instance_name) - vmrun = VMrun(self.vmx, self.user, self.password) + instance = arguments[''] + inst = MechInstance(instance) + vmrun = VMrun(inst.vmx, inst.user, inst.password) print(vmrun.listProcessesInGuest()) def status(self, arguments): @@ -681,11 +658,11 @@ def status(self, arguments): instances = self.instances() for instance in instances: - self.activate(instance) + inst = MechInstance(instance) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - lookup = self.enable_ip_lookup + lookup = inst.enable_ip_lookup ip = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) state = vmrun.checkToolsState(quiet=True) @@ -694,7 +671,7 @@ def status(self, arguments): ip = "poweroff" elif not ip: ip = "unknown" - print("%s\t%s\t%s\t(VMware Tools %s)" % (self.active_name, self.box, ip, state)) + print("%s\t%s\t%s\t(VMware Tools %s)" % (inst.name, inst.box, ip, state)) if ip == "poweroff": print(os.linesep + "The VM is powered off. To restart the VM, " @@ -728,24 +705,20 @@ def destroy(self, arguments): instances = self.instances() for instance in instances: - self.activate(instance) + inst = MechInstance(instance) - if self.active_name: - instance_path = MechCommand.instance_path(instance) + instance_path = MechCommand.instance_path(instance) if os.path.exists(instance_path): if force or utils.confirm( "Are you sure you want to delete {} at {}".format( - self.active_name, instance_path), default='n'): + inst.name, instance_path), default='n'): puts_err(colored.green("Deleting ({})...".format(instance))) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) vmrun.stop(mode='hard', quiet=True) time.sleep(3) vmrun.deleteVM() - # change out of this directory - self.deactivate() - if os.path.exists(instance_path): shutil.rmtree(instance_path) else: @@ -777,9 +750,9 @@ def down(self, arguments): instances = self.instances() for instance in instances: - self.activate(instance) + inst = MechInstance(instance) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) if not force and vmrun.installedTools(): stopped = vmrun.stop() else: @@ -812,8 +785,8 @@ def pause(self, arguments): instances = self.instances() for instance in instances: - self.activate(instance) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + inst = MechInstance(instance) + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) if vmrun.pause() is None: puts_err(colored.red("Not paused", vmrun)) else: @@ -843,18 +816,18 @@ def resume(self, arguments): instances = self.instances() for instance in instances: - self.activate(instance) - logger.debug('instance:{} self.vmx:{}'.format(instance, self.vmx)) + inst = MechInstance(instance) + logger.debug('instance:{} self.vmx:{}'.format(instance, inst.vmx)) # if we have started this instance before, try to unpause - if self.vmx: + if inst.vmx: - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) if vmrun.unpause(quiet=True) is not None: time.sleep(1) puts_err(colored.blue("Getting IP address...")) - lookup = self.enable_ip_lookup + lookup = inst.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) if not disable_shared_folders: puts_err(colored.blue("Sharing current folder...")) @@ -870,14 +843,14 @@ def resume(self, arguments): else: # Otherwise try starting - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) 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.enable_ip_lookup + lookup = inst.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) if not disable_shared_folders: puts_err(colored.blue("Sharing current folder...")) @@ -919,8 +892,8 @@ def suspend(self, arguments): instances = self.instances() for instance in instances: - self.activate(instance) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + inst = MechInstance(instance) + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) if vmrun.suspend() is None: puts_err(colored.red("Not suspended", vmrun)) else: @@ -945,8 +918,8 @@ def ssh_config(self, arguments): instances = self.instances() for instance in instances: - self.activate(instance) - print(utils.config_ssh_string(self.config_ssh())) + inst = MechInstance(instance) + print(utils.config_ssh_string(inst.config_ssh())) def ssh(self, arguments): """ @@ -963,11 +936,11 @@ def ssh(self, arguments): extra = arguments[''] command = arguments['--command'] - instance_name = arguments[''] + instance = arguments[''] - self.activate(instance_name) + inst = MechInstance(instance) - config_ssh = self.config_ssh() + config_ssh = inst.config_ssh() fp = tempfile.NamedTemporaryFile(delete=False) try: fp.write(utils.config_ssh_string(config_ssh).encode('utf-8')) @@ -1013,17 +986,17 @@ def scp(self, arguments): puts_err(colored.red("Both src and host are host destinations")) sys.exit(1) if dst_is_host: - instance_name = dst_instance + instance = dst_instance else: dst = dst_instance if src_is_host: - instance_name = src_instance + instance = src_instance else: src = src_instance - self.activate(instance_name) + inst = MechInstance(instance) - config_ssh = self.config_ssh() + config_ssh = inst.config_ssh() fp = tempfile.NamedTemporaryFile(delete=False) # when we activate, we change to the directory where the .vmx file is @@ -1064,12 +1037,12 @@ def ip(self, arguments): Options: -h, --help Print this help """ - instance_name = arguments[''] + instance = arguments[''] - self.activate(instance_name) + inst = MechInstance(instance) - if self.created: - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + if inst.created: + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) lookup = self.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) if ip: @@ -1100,8 +1073,8 @@ def provision(self, arguments): instances = self.instances() for instance in instances: - self.activate(instance) - utils.provision(instance, self.vmx, self.user, self.password, self.provision, show) + inst = MechInstance(instance) + utils.provision(instance, inst.vmx, inst.user, inst.password, inst.provision, show) def reload(self, arguments): """ @@ -1122,9 +1095,9 @@ def reload(self, arguments): instances = self.instances() for instance in instances: - self.activate(instance) + inst = MechInstance(instance) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) puts_err(colored.blue("Reloading machine...")) started = vmrun.reset() @@ -1133,7 +1106,7 @@ def reload(self, arguments): else: time.sleep(3) puts_err(colored.blue("Getting IP address...")) - lookup = self.enable_ip_lookup + lookup = inst.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) if ip: if started: @@ -1171,9 +1144,9 @@ def port(self, arguments): # FUTURE: implement port forwarding? for instance in instances: - self.activate(instance) + inst = MechInstance(instance) - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) for network in vmrun.listHostNetworks().split('\n'): network = network.split() if len(network) > 2 and network[2] == 'nat': @@ -1201,10 +1174,10 @@ def list(self, arguments): 'VERSION'.rjust(12) )) for name in self.mechfile: - self.activate(name) - if self.created: - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - lookup = self.enable_ip_lookup + inst = MechInstance(name, self.mechfile) + if inst.created: + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + lookup = inst.enable_ip_lookup ip = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) if ip is None: ip = colored.yellow("poweroff") @@ -1218,8 +1191,8 @@ def list(self, arguments): print("{}\t{}\t{}\t{}".format( colored.green(name.rjust(20)), ip.rjust(15), - self.box.rjust(35), - self.box_version.rjust(12) + inst.box.rjust(35), + inst.box_version.rjust(12) )) # allow 'mech ls' as alias to 'mech list' From a11eef91bae37c0fa6198ca6d6a456e160b85178 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 5 Feb 2020 10:31:23 -0800 Subject: [PATCH 031/116] no need to change dirs --- mech/mech.py | 13 ++----------- mech/utils.py | 7 ------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 5539822..c2e198b 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -78,10 +78,9 @@ class MechInstance(): def __init__(self, name, mechfile=None): - """Sets the active instance name and changes to the - directory for that instance.""" + """Class to hold instance (aka virtual machine).""" if name == "": - raise AttributeError("Must provide a instance name.") + raise AttributeError("Must provide a name for the instance.") if not mechfile: mechfile = utils.load_mechfile() logger.debug("loaded mechfile:{}".format(mechfile)) @@ -108,9 +107,6 @@ def __init__(self, name, mechfile=None): else: self.vmx = None self.created = False - if os.path.exists(path): - # change to the path of the instance - os.chdir(path) def __repr__(self): """Return a representation of this object.""" @@ -999,11 +995,6 @@ def scp(self, arguments): config_ssh = inst.config_ssh() fp = tempfile.NamedTemporaryFile(delete=False) - # when we activate, we change to the directory where the .vmx file is - # since we are trying to copy files, we need to go back to the - # directory that we started in - os.chdir(utils.main_dir()) - try: fp.write(utils.config_ssh_string(config_ssh).encode()) fp.close() diff --git a/mech/utils.py b/mech/utils.py index b504092..0996cba 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -580,9 +580,6 @@ def provision(instance, vmx, user, password, provision, show): provision_type = p.get('type') if provision_type == 'file': source = p.get('source') - # Note: When we activate the instance, we change down to where the .vmx file - # is. For the source to "work", we need to pre-pend the main_dir(). - source = os.path.join(main_dir(), source) destination = p.get('destination') if show: puts_err(colored.green(" instance:{} provision_type:{} source:{} " @@ -597,10 +594,6 @@ def provision(instance, vmx, user, password, provision, show): elif provision_type == 'shell': inline = p.get('inline') path = p.get('path') - if path: - # Note: When we activate the instance, we change down to where the .vmx file - # is. For the source to "work", we need to pre-pend the main_dir(). - path = os.path.join(main_dir(), path) args = p.get('args') if not isinstance(args, list): From 7be0e95c218c30a923aa1b5e33f03d829185afda Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 5 Feb 2020 12:03:44 -0800 Subject: [PATCH 032/116] add detailed list for instances --- mech/mech.py | 60 +++++++++++++++++++++++++++++++++------------------ mech/utils.py | 20 ++++++++--------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index c2e198b..c3adbeb 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -109,14 +109,20 @@ def __init__(self, name, mechfile=None): self.created = False def __repr__(self): - """Return a representation of this object.""" - return ('name:{} created:{} box:{} box_version:{} ' - 'url:{} box_file:{} provision:{} vmx:{} user:{} ' - 'password:{} enable_ip_lookup:{} config:{}'. - format(self.name, self.created, self.box, - self.box_version, self.url, self.box_file, - self.provision, self.vmx, self.user, self.password, - self.enable_ip_lookup, self.config)) + """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}'. + 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, + sep=sep)) def config_ssh(self): vmrun = VMrun(self.vmx, user=self.user, password=self.password) @@ -554,6 +560,7 @@ def up(self, arguments): for instance in instances: inst = MechInstance(instance) + # TODO: refactor instance_path = MechCommand.instance_path(instance) location = inst.url @@ -1148,22 +1155,29 @@ def port(self, arguments): def list(self, arguments): """ - Lists all available boxes. + Lists all available boxes from Mechfile. Usage: mech list [options] Options: + -d, --detail Print detailed info -h, --help Print this help """ + detail = arguments['--detail'] self.activate_mechfile() - print("{}\t{}\t{}\t{}".format( - 'NAME'.rjust(20), - 'ADDRESS'.rjust(15), - 'BOX'.rjust(35), - 'VERSION'.rjust(12) - )) + 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: @@ -1179,12 +1193,16 @@ def list(self, arguments): else: ip = "notcreated" - print("{}\t{}\t{}\t{}".format( - colored.green(name.rjust(20)), - ip.rjust(15), - inst.box.rjust(35), - inst.box_version.rjust(12) - )) + if detail: + print(inst) + print() + else: + print("{}\t{}\t{}\t{}".format( + colored.green(name.rjust(20)), + ip.rjust(15), + inst.box.rjust(35), + inst.box_version.rjust(12) + )) # allow 'mech ls' as alias to 'mech list' ls = list diff --git a/mech/utils.py b/mech/utils.py index 0996cba..f356eeb 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -113,12 +113,13 @@ def confirm(prompt, default='y'): return False -def save_mechfile(mechfile, name, path): +def save_mechfile(mechfile, name): """Save the Mechfile.""" - multiple_mechfiles = {name: mechfile} - logger.debug("path:{}".format(path)) - with open(os.path.join(path, 'Mechfile'), 'w+') as fp: - json.dump(multiple_mechfiles, fp, sort_keys=True, indent=2, separators=(',', ': ')) + logger.debug('mechfile:{} name:{}'.format(mechfile, name)) + mechfiles = {name: mechfile} + logger.debug("mechfiles:{}".format(mechfiles)) + with open(os.path.join(main_dir(), 'Mechfile'), 'w+') as fp: + json.dump(mechfiles, fp, sort_keys=True, indent=2, separators=(',', ': ')) return True @@ -314,7 +315,7 @@ def tar_cmd(*args, **kwargs): return tar -def init_box(name, box, box_version, location=None, force=False, save=True, +def init_box(name, box=None, box_version=None, location=None, force=False, save=True, instance_path=None, requests_kwargs={}, numvcpus=None, memsize=None): """Initialize the box. This includes uncompressing the files @@ -529,16 +530,15 @@ def add_box_file(box=None, box_version=None, filename=None, url=None, force=Fals def init_mechfile(location=None, box=None, name=None, box_version=None, requests_kwargs={}): """Initialize the Mechfile.""" - path = main_dir() - logger.debug("name:{} box:{} box_version:{} location:{} path:{}".format( - name, box, box_version, location, path)) + logger.debug("name:{} box:{} box_version:{} location:{}".format( + name, box, box_version, location)) mechfile = build_mechfile( location=location, box=box, name=name, box_version=box_version, requests_kwargs=requests_kwargs) - return save_mechfile(mechfile, name, path) + return save_mechfile(mechfile, name) def get_requests_kwargs(arguments): From f0f5818d81f4444dcff79a93ab5e26ad02bce380 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 5 Feb 2020 13:52:29 -0800 Subject: [PATCH 033/116] add/remove instances from Mechfile --- mech/mech.py | 80 +++++++++++++- mech/utils.py | 137 ++++++++++++++++-------- tests/int/README.md | 1 + tests/int/add_and_remove_instances.bats | 125 +++++++++++++++++++++ tests/int/all | 1 + 5 files changed, 296 insertions(+), 48 deletions(-) create mode 100755 tests/int/add_and_remove_instances.bats diff --git a/mech/mech.py b/mech/mech.py index c3adbeb..47b58eb 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -496,7 +496,7 @@ def init(self, arguments): "`Mechfile` already exists in this directory. Remove it " "before running `mech init`." ))) - return + sys.exit(1) puts_err(colored.green("Initializing mech")) if utils.init_mechfile( @@ -512,6 +512,83 @@ def init(self, arguments): else: puts_err(colored.red("Couldn't initialize mech")) + def add(self, arguments): + """ + Add instance to the Mechfile. + + Usage: mech add [options] + + Example box: bento/ubuntu-18.04 + + Options: + --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) + --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) + -h, --help Print this help + """ + name = arguments[''] + box_version = arguments['--box-version'] + box = arguments['--box'] + location = arguments[''] + + if not name or name == "": + puts_err(colored.red("Need to provide a name for the instance to add to the Mechfile.")) + sys.exit(1) + + requests_kwargs = utils.get_requests_kwargs(arguments) + + logger.debug('name:{} box:{} box_version:{} location:{}'.format( + name, box, box_version, location)) + + puts_err(colored.green("Adding ({}) to the Mechfile.".format(name))) + + if utils.add_to_mechfile( + location=location, + box=box, + name=name, + box_version=box_version, + requests_kwargs=requests_kwargs): + puts_err(colored.green("Added to the Mechfile.")) + else: + puts_err(colored.red("Could not add {} to the Mechfile".format(name))) + + 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 == "": + puts_err(colored.red("Need to provide a name to be removed from the Mechfile.")) + sys.exit(1) + + logger.debug('name:{}'.format(name)) + + self.activate_mechfile() + inst = self.mechfile.get(name, None) + if inst: + puts_err(colored.green("Removing ({}) from the Mechfile.".format(name))) + if utils.remove_mechfile_entry(name=name): + puts_err(colored.green("Removed from the Mechfile.")) + else: + puts_err(colored.red("Could not remove {} from the Mechfile".format(name))) + else: + puts_err(colored.red("There is no instance called ({}) in the Mechfile.".format(name))) + sys.exit(1) + + # add alias for 'mech delete' + delete = remove + def up(self, arguments): """ Starts and provisions the mech environment. @@ -1165,6 +1242,7 @@ def list(self, arguments): """ detail = arguments['--detail'] + self.activate_mechfile() if detail: diff --git a/mech/utils.py b/mech/utils.py index f356eeb..088ff28 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -113,13 +113,36 @@ def confirm(prompt, default='y'): return False -def save_mechfile(mechfile, name): - """Save the Mechfile.""" - logger.debug('mechfile:{} name:{}'.format(mechfile, name)) - mechfiles = {name: mechfile} - logger.debug("mechfiles:{}".format(mechfiles)) +def save_mechfile_entry(mechfile_entry, name, mechfile_should_exist=False): + """Save the entry to the Mechfile.""" + logger.debug('mechfile_entry:{} name:{} ' + 'mechfile_should_exist:{}'.format(mechfile_entry, name, + mechfile_should_exist)) + mechfile = load_mechfile(mechfile_should_exist) + + mechfile[name] = mechfile_entry + + logger.debug("after adding name:{} mechfile:{}".format(name, mechfile)) + return save_mechfile(mechfile) + + +def remove_mechfile_entry(name, mechfile_should_exist=True): + """Removed the entry from the Mechfile.""" + logger.debug('name:{} mechfile_should_exist:{}'.format(name, mechfile_should_exist)) + mechfile = load_mechfile(mechfile_should_exist) + + if mechfile.get(name): + del mechfile[name] + + logger.debug("after removing name:{} mechfile:{}".format(name, mechfile)) + return save_mechfile(mechfile) + + +def save_mechfile(mechfile): + """Save the mechfile object to a file called 'Mechfile'.""" + logger.debug('mechfile:{}'.format(mechfile)) with open(os.path.join(main_dir(), 'Mechfile'), 'w+') as fp: - json.dump(mechfiles, fp, sort_keys=True, indent=2, separators=(',', ': ')) + json.dump(mechfile, fp, sort_keys=True, indent=2, separators=(',', ': ')) return True @@ -191,43 +214,48 @@ def update_vmx(path, numvcpus=None, memsize=None): # vmrun.upgradevm() -def load_mechfile(): - """Load the Mechfile from disk.""" - mechfile_full = os.path.join(main_dir(), 'Mechfile') - logger.debug("mechfile_full:{}".format(mechfile_full)) - if os.path.isfile(mechfile_full): - with open(mechfile_full) as fp: +def load_mechfile(should_exist=True): + """Load the Mechfile from disk and return the object.""" + mechfile_fullpath = os.path.join(main_dir(), 'Mechfile') + logger.debug("mechfile_fullpath:{}".format(mechfile_fullpath)) + if os.path.isfile(mechfile_fullpath): + with open(mechfile_fullpath) as fp: try: - return json.loads(uncomment(fp.read())) + mechfile = json.loads(uncomment(fp.read())) + logger.debug('mechfile:{}'.format(mechfile)) + return mechfile except ValueError: puts_err(colored.red("Invalid Mechfile." + os.linesep)) else: - puts_err(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." - ))) - sys.exit(1) + if should_exist: + puts_err(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." + ))) + sys.exit(1) + else: + return {} -def build_mechfile(location, box=None, name=None, box_version=None, requests_kwargs={}): +def build_mechfile_entry(location, box=None, name=None, box_version=None, requests_kwargs={}): """Build the Mechfile from the inputs.""" logger.debug("location:{} name:{} box:{} box_version:{}".format( location, name, box, box_version)) - mechfile = {} + mechfile_entry = {} if location is None: - return mechfile - mechfile['name'] = name + return mechfile_entry + mechfile_entry['name'] = name if any(location.startswith(s) for s in ('https://', 'http://', 'ftp://')): - mechfile['url'] = location + mechfile_entry['url'] = location if not name: name = 'first' - mechfile['box'] = box + mechfile_entry['box'] = box if box_version: - mechfile['box_version'] = box_version - return mechfile + mechfile_entry['box_version'] = box_version + return mechfile_entry elif location.startswith('file:') or os.path.isfile(re.sub(r'^file:(?://)?', '', location)): location = re.sub(r'^file:(?://)?', '', location) logger.debug('location:{}'.format(location)) @@ -236,14 +264,14 @@ def build_mechfile(location, box=None, name=None, box_version=None, requests_kwa with open(location) as fp: catalog = json.loads(uncomment(fp.read())) except Exception: - mechfile['file'] = location + mechfile_entry['file'] = location if not name: name = 'first' - mechfile['box'] = box + mechfile_entry['box'] = box if box_version: - mechfile['box_version'] = box_version - logger.debug('mechfile:{}'.format(mechfile)) - return mechfile + mechfile_entry['box_version'] = box_version + logger.debug('mechfile_entry:{}'.format(mechfile_entry)) + return mechfile_entry else: try: account, box, v = (location.split('/', 2) + ['', ''])[:3] @@ -379,7 +407,7 @@ def add_box(name=None, box=None, box_version=None, location=None, # build the dict logger.debug('name:{} box:{} box_version:{} location:{}'.format( name, box, box_version, location)) - mechfile = build_mechfile( + mechfile_entry = build_mechfile_entry( box=box, name=name, location=location, @@ -387,7 +415,7 @@ def add_box(name=None, box=None, box_version=None, location=None, requests_kwargs=requests_kwargs) return add_mechfile( - mechfile, + mechfile_entry, name=name, box=box, location=location, @@ -397,17 +425,17 @@ def add_box(name=None, box=None, box_version=None, location=None, requests_kwargs=requests_kwargs) -def add_mechfile(mechfile, name=None, box=None, box_version=None, +def add_mechfile(mechfile_entry, name=None, box=None, box_version=None, location=None, force=False, save=True, requests_kwargs={}): - logger.debug('mechfile:{} name:{} box:{} box_version:{} location:{}'.format( - mechfile, name, box, box_version, location)) + logger.debug('mechfile_entry:{} name:{} box:{} box_version:{} location:{}'.format( + mechfile_entry, name, box, box_version, location)) - box = mechfile.get('box') - name = mechfile.get('name') - box_version = mechfile.get('box_version') + box = mechfile_entry.get('box') + name = mechfile_entry.get('name') + box_version = mechfile_entry.get('box_version') - url = mechfile.get('url') - box_file = mechfile.get('file') + 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, @@ -486,7 +514,7 @@ def add_box_url(name, box, box_version, url, force=False, save=True, requests_kw 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.""" + """Add a box using a file as the source. Returns box and box_version.""" puts_err(colored.blue("Checking box '{}' integrity filename:{}...".format(box, filename))) if sys.platform == 'win32': @@ -532,13 +560,28 @@ def init_mechfile(location=None, box=None, name=None, box_version=None, requests """Initialize the Mechfile.""" logger.debug("name:{} box:{} box_version:{} location:{}".format( name, box, box_version, location)) - mechfile = build_mechfile( + mechfile_entry = build_mechfile_entry( + location=location, + box=box, + name=name, + box_version=box_version, + requests_kwargs=requests_kwargs) + logger.debug('mechfile_entry:{}'.format(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, requests_kwargs={}): + """Add entry to the Mechfile.""" + logger.debug("name:{} box:{} box_version:{} location:{}".format( + name, box, box_version, location)) + this_mech_entry = build_mechfile_entry( location=location, box=box, name=name, box_version=box_version, requests_kwargs=requests_kwargs) - return save_mechfile(mechfile, name) + logger.debug('this_mech_entry:{}'.format(this_mech_entry)) + return save_mechfile_entry(this_mech_entry, name, mechfile_should_exist=False) def get_requests_kwargs(arguments): diff --git a/tests/int/README.md b/tests/int/README.md index 6c9e445..7dd531b 100644 --- a/tests/int/README.md +++ b/tests/int/README.md @@ -22,3 +22,4 @@ files from the internet and start up/stop/destroy VMs. - `./simple.bats` - simple validations of most basic functionality - `./two_ubuntu.bats` - validate two ubuntu instances - `./shared_folders.bats` - validate shared folders functionality +- `./add_and_remove_instances.bats` - validate add/removing instance from Mechfile diff --git a/tests/int/add_and_remove_instances.bats b/tests/int/add_and_remove_instances.bats new file mode 100755 index 0000000..d5263b0 --- /dev/null +++ b/tests/int/add_and_remove_instances.bats @@ -0,0 +1,125 @@ +#!/usr/bin/env bats +# +# shared_folders.bats - run shared_folders tests +# +# Note: must be run from this directory +# like this: ./shared_folders.bats + +@test "add and remove instances tests" { + if ! [ -d add_and_remove_instances ]; then + mkdir add_and_remove_instances + fi + cd add_and_remove_instances + + # setup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + if [ -f Mechfile ]; then + rm Mechfile + fi + + run mech ls + regex1="Could not find a Mechfile" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + run mech remove one + regex1="Could not find a Mechfile" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + # check required params + run mech add + regex1="Usage: mech add " + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + # check required params + run mech add mrlesmithjr/alpine311 + regex1="Usage: mech add " + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + # verify with add + run mech add one mrlesmithjr/alpine311 + regex1="Added to the Mechfile" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech ls + regex1=" one " + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # verify with init + rm Mechfile + run mech init mrlesmithjr/alpine311 + regex1="Added to the Mechfile" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech ls + regex1=" one " + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # verify re-adding is not an issue + run mech add one mrlesmithjr/alpine311 + regex1="Added to the Mechfile" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # verify init with a Mechfile fails + run mech init mrlesmithjr/alpine311 + regex1="already exists" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + # add a 2nd instance + run mech add two mrlesmithjr/alpine311 + regex1="Added to the Mechfile" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech ls + regex1=" one " + regex2=" two " + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + + # remove one + run mech remove one + regex1="Removed" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech ls + regex1=" one " + regex2=" two " + [ "$status" -eq 0 ] + ! [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + + # try to remove one again + run mech remove one + regex1="There is no instance" + [ "$status" -eq 1 ] + [[ "$output" =~ $regex1 ]] + + run mech remove two + regex1="Removed" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech ls + regex1=" one " + regex2=" two " + [ "$status" -eq 0 ] + ! [[ "$output" =~ $regex1 ]] + ! [[ "$output" =~ $regex2 ]] + + # clean up + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + cd .. +} diff --git a/tests/int/all b/tests/int/all index 66dfbc8..de00476 100755 --- a/tests/int/all +++ b/tests/int/all @@ -10,3 +10,4 @@ ./init_from_file.bats ./provision.bats ./shared_folders.bats +./add_and_remove_instances.bats From 70829d75cd6f24cd1ad7628d536acfbc92d27a00 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 5 Feb 2020 14:40:12 -0800 Subject: [PATCH 034/116] remove unused code; refactor path; improve comments --- mech/mech.py | 25 ++++++++----------------- mech/utils.py | 44 +++++++++++++------------------------------- 2 files changed, 21 insertions(+), 48 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 47b58eb..49278cc 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -98,8 +98,8 @@ def __init__(self, name, mechfile=None): self.config = {} self.user = DEFAULT_USER self.password = DEFAULT_PASSWORD - path = MechCommand.instance_path(name) - vmx = utils.locate(path, '*.vmx') + 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 @@ -189,11 +189,6 @@ def instances(self): self.activate_mechfile() return list(self.mechfile) - @staticmethod - def instance_path(name): - """Return the path for an instance.""" - return os.path.join(utils.mech_dir(), name) - def vmx(self): """Get the fully qualified path to the vmx file.""" return self.vmx @@ -637,8 +632,6 @@ def up(self, arguments): for instance in instances: inst = MechInstance(instance) - # TODO: refactor - instance_path = MechCommand.instance_path(instance) location = inst.url if not location: @@ -649,7 +642,7 @@ def up(self, arguments): box=inst.box, box_version=inst.box_version, location=location, - instance_path=instance_path, + instance_path=inst.path, requests_kwargs=requests_kwargs, save=save, numvcpus=numvcpus, @@ -787,22 +780,20 @@ def destroy(self, arguments): for instance in instances: inst = MechInstance(instance) - instance_path = MechCommand.instance_path(instance) - - if os.path.exists(instance_path): + if os.path.exists(inst.path): if force or utils.confirm( "Are you sure you want to delete {} at {}".format( - inst.name, instance_path), default='n'): + inst.name, inst.path), default='n'): puts_err(colored.green("Deleting ({})...".format(instance))) vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) vmrun.stop(mode='hard', quiet=True) time.sleep(3) vmrun.deleteVM() - if os.path.exists(instance_path): - shutil.rmtree(instance_path) + if os.path.exists(inst.path): + shutil.rmtree(inst.path) else: - logger.debug("{} was not found.".format(instance_path)) + logger.debug("{} was not found.".format(inst.path)) else: puts_err(colored.red("Deletion aborted")) else: diff --git a/mech/utils.py b/mech/utils.py index 088ff28..949d9c5 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -68,28 +68,6 @@ def makedirs(name, mode=0o777): pass -def uncomment(text): - """Uncomment a line of 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() @@ -155,7 +133,9 @@ def locate(path, glob): def parse_vmx(path): - """Parse the virtual machine configuration (.vmx) file.""" + """Parse the virtual machine configuration (.vmx) file and return an + ordered dictionary. + """ vmx = collections.OrderedDict() with open(path) as fp: for line in fp: @@ -209,10 +189,6 @@ def update_vmx(path, numvcpus=None, memsize=None): 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 object.""" @@ -221,7 +197,7 @@ def load_mechfile(should_exist=True): if os.path.isfile(mechfile_fullpath): with open(mechfile_fullpath) as fp: try: - mechfile = json.loads(uncomment(fp.read())) + mechfile = json.loads(fp.read()) logger.debug('mechfile:{}'.format(mechfile)) return mechfile except ValueError: @@ -262,7 +238,7 @@ def build_mechfile_entry(location, box=None, name=None, box_version=None, reques try: # see if the location is a json file with open(location) as fp: - catalog = json.loads(uncomment(fp.read())) + catalog = json.loads(fp.read()) except Exception: mechfile_entry['file'] = location if not name: @@ -348,7 +324,7 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= memsize=None): """Initialize the box. This includes uncompressing the files from the box file and updating the vmx file with - desired settings. + desired settings. Return the full path to the vmx file. """ logger.debug("name:{} box:{} box_version:{} location:{}".format( name, box, box_version, location)) @@ -599,7 +575,13 @@ def get_requests_kwargs(arguments): def provision(instance, vmx, user, password, provision, show): - """Provision an instance.""" + """Provision an instance. + + provision types: + file: copies files to instances + shell: executes scripts + + """ if instance == '': puts_err(colored.red("Need to provide an instance to provision().")) From 4f36fb84a5f07bbeddd1889898451c8a9053b810 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 5 Feb 2020 15:11:19 -0800 Subject: [PATCH 035/116] remove unused methods --- mech/mech.py | 32 +++----------------------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 49278cc..afedba7 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -173,9 +173,7 @@ def callback(pat): class MechCommand(Command): - """Class to hold the Mechfile (as python object) and an active - instance's configuration. - """ + """Class to hold the Mechfile (as python object).""" mechfile = None def activate_mechfile(self): @@ -189,30 +187,6 @@ def instances(self): self.activate_mechfile() return list(self.mechfile) - def vmx(self): - """Get the fully qualified path to the vmx file.""" - return self.vmx - - def box(self): - """Get the box (ex: 'bento/ubuntu-18.04').""" - return self.box - - def box_version(self): - """Get the box_version (ex: '2020-01-16').""" - return self.box_version - - def user(self): - """Get the username (ex: 'vagrant').""" - return self.user - - def password(self): - """Get the password (ex: 'temp123').""" - return self.password - - def enable_ip_lookup(self): - """Enable ip lookup (defaults to False).""" - return self.enable_ip_lookup - class MechBox(MechCommand): """ @@ -888,7 +862,7 @@ def resume(self, arguments): for instance in instances: inst = MechInstance(instance) - logger.debug('instance:{} self.vmx:{}'.format(instance, inst.vmx)) + logger.debug('instance:{} inst.vmx:{}'.format(instance, inst.vmx)) # if we have started this instance before, try to unpause if inst.vmx: @@ -1109,7 +1083,7 @@ def ip(self, arguments): if inst.created: vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - lookup = self.enable_ip_lookup + lookup = inst.enable_ip_lookup ip = vmrun.getGuestIPAddress(lookup=lookup) if ip: puts_err(colored.green(ip)) From 0d09ad677dd8c4226698d241ad37e6c268857f8f Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Thu, 6 Feb 2020 15:15:34 -0800 Subject: [PATCH 036/116] initial unit tests --- CONTRIBUTING.md | 2 +- mech/test_utils.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++ mech/utils.py | 6 ++-- 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 mech/test_utils.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c73554..35469bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ source venv/bin/activate python setup.py install # if doing development -pip install flake8 +pip install flake8 pytest pytest_mock mock # also optional pip install autopep8 diff --git a/mech/test_utils.py b/mech/test_utils.py new file mode 100644 index 0000000..e5196f3 --- /dev/null +++ b/mech/test_utils.py @@ -0,0 +1,87 @@ +import os + +from unittest.mock import patch, mock_open + +import mech.utils + + +def test_main_dir(): + main = mech.utils.main_dir() + assert main.startswith('/') + + +def test_mech_dir(): + mech_dir = mech.utils.mech_dir() + assert mech_dir.startswith('/') + assert mech_dir.endswith('/.mech') + + +def test_save_mechfile_empty_config(): + filename = os.path.join(mech.utils.main_dir(), 'Mechfile') + m = mock_open() + with patch('builtins.open', m, create=True): + assert mech.utils.save_mechfile({}) + m.assert_called_once_with(filename, 'w+') + m.return_value.write.assert_called_once_with('{}') + + +def test_save_mechfile_simple_config(): + 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" + } +}''' + filename = os.path.join(mech.utils.main_dir(), 'Mechfile') + m = mock_open() + with patch('builtins.open', m, create=True): + assert mech.utils.save_mechfile(first_dict) + m.assert_called_once_with(filename, 'w+') + + # print('calls to the file:\n', m.mock_calls, end ='\n\n') + + # TODO: Is there a better way to get data written? + written = '' + for call in m.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 + assert written == first_json + + +def test_tar_cmd(): + """Note: not really a unit test per se, as it calls out.""" + assert ["tar"] == mech.utils.tar_cmd() + + +def test_config_ssh_string_empty(): + ssh_string = mech.utils.config_ssh_string({}) + assert ssh_string == "Host \n" + + +def test_config_ssh_string_simple(): + 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' diff --git a/mech/utils.py b/mech/utils.py index 949d9c5..6186c29 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -117,7 +117,9 @@ def remove_mechfile_entry(name, mechfile_should_exist=True): def save_mechfile(mechfile): - """Save the mechfile object to a file called 'Mechfile'.""" + """Save the mechfile object to a file called 'Mechfile'. + Return True if save was successful. + """ logger.debug('mechfile:{}'.format(mechfile)) with open(os.path.join(main_dir(), 'Mechfile'), 'w+') as fp: json.dump(mechfile, fp, sort_keys=True, indent=2, separators=(',', ': ')) @@ -706,7 +708,7 @@ def provision_shell(vm, inline, path, args=[]): def config_ssh_string(config_ssh): """Build the ssh-config string.""" - ssh_config = "Host {}".format(config_ssh['Host']) + os.linesep + ssh_config = "Host {}".format(config_ssh.get('Host', '')) + os.linesep for k, v in config_ssh.items(): if k != 'Host': ssh_config += " {} {}".format(k, v) + os.linesep From 2ff29344bba06b302747c82e773e0aabd9142f83 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Thu, 6 Feb 2020 15:25:35 -0800 Subject: [PATCH 037/116] ignore these long lines --- mech/test_utils.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/mech/test_utils.py b/mech/test_utils.py index e5196f3..644025f 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -26,12 +26,18 @@ def test_save_mechfile_empty_config(): def test_save_mechfile_simple_config(): - 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_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": { @@ -40,7 +46,7 @@ def test_save_mechfile_simple_config(): "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') m = mock_open() with patch('builtins.open', m, create=True): @@ -84,4 +90,4 @@ def test_config_ssh_string_simple(): "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' + 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 From 8388f4b125912070eaacde275f153f33ed509d06 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Fri, 7 Feb 2020 08:44:03 -0800 Subject: [PATCH 038/116] add some initial tests --- mech/test_utils.py | 72 ++++++++++++++++++++++++++++++++++++++-------- test_mech.py | 15 ++++++++++ 2 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 test_mech.py diff --git a/mech/test_utils.py b/mech/test_utils.py index 644025f..9861fd3 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -25,7 +25,20 @@ def test_save_mechfile_empty_config(): m.return_value.write.assert_called_once_with('{}') -def test_save_mechfile_simple_config(): +# TODO: Is there a better way to get data written? +def _get_data_written(m): + written = '' + for call in m.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 + + +def test_save_mechfile_one(): first_dict = { 'first': { 'name': @@ -52,19 +65,54 @@ def test_save_mechfile_simple_config(): with patch('builtins.open', m, create=True): assert mech.utils.save_mechfile(first_dict) m.assert_called_once_with(filename, 'w+') + assert first_json == _get_data_written(m) - # print('calls to the file:\n', m.mock_calls, end ='\n\n') - # TODO: Is there a better way to get data written? - written = '' - for call in m.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 - assert written == first_json +def test_save_mechfile_two(): + 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') + m = mock_open() + with patch('builtins.open', m, create=True): + assert mech.utils.save_mechfile(two_dict) + m.assert_called_once_with(filename, 'w+') + assert two_json == _get_data_written(m) def test_tar_cmd(): diff --git a/test_mech.py b/test_mech.py new file mode 100644 index 0000000..0f2f91a --- /dev/null +++ b/test_mech.py @@ -0,0 +1,15 @@ +"""Test the mech cli. """ +import subprocess +import re + + +def test_version(): + rc, out = subprocess.getstatusoutput('mech --version') + assert re.match(r'mech v[0-9]+\.[0-9]+\.[0-9]', out) + assert rc == 0 + + +def test_help(): + rc, out = subprocess.getstatusoutput('mech --help') + assert re.match(r'Usage: mech ', out) + assert rc == 0 From 4add67a9ef4f3879ad50bf85ebc7593a57a58bf2 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Fri, 7 Feb 2020 09:53:23 -0800 Subject: [PATCH 039/116] add code coverage; add more tests --- .gitignore | 1 + CONTRIBUTING.md | 10 +++++-- mech/test_utils.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 523f091..23528ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +.coverage *.pyc .idea/ build/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35469bf..adb19b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ source venv/bin/activate python setup.py install # if doing development -pip install flake8 pytest pytest_mock mock +pip install flake8 pytest pytest_mock mock pytest-cov # also optional pip install autopep8 @@ -37,7 +37,13 @@ flake8 --install-hook git # see https://github.com/bats-core/bats-core brew install bats-core -# for testing/validation, we have some integration tests +# for testing: +pytest + +# for code coverage +pytest --cov mech + +# for testing/validation, we have also some integration tests cd tests/int ./simple.bats ./two_ubuntu.bats diff --git a/mech/test_utils.py b/mech/test_utils.py index 9861fd3..42a9bb5 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -1,6 +1,7 @@ import os from unittest.mock import patch, mock_open +from collections import OrderedDict import mech.utils @@ -139,3 +140,76 @@ def test_config_ssh_string_simple(): } 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 + + +@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): + 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): + 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): + 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): + 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): + 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): + assert mech.utils.remove_mechfile_entry('first', True) + load_mock.assert_called_once() + save_mock.assert_called_once() + + +def 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"') + ]) + m = mock_open(read_data=partial_vmx) + with patch('builtins.open', m): + assert mech.utils.parse_vmx(partial_vmx) == expected_vmx + m.assert_called() From d03fdc63e300e8bdfcb81c2c5f1aaf5ea7dc0086 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Fri, 7 Feb 2020 15:11:55 -0800 Subject: [PATCH 040/116] figured out how to *unit test* mech list --- mech/test_mech.py | 80 +++++++++++++++++++++++++++++++++++++++++++++++ test_mech.py | 15 --------- 2 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 mech/test_mech.py delete mode 100644 test_mech.py diff --git a/mech/test_mech.py b/mech/test_mech.py new file mode 100644 index 0000000..efa2f1a --- /dev/null +++ b/mech/test_mech.py @@ -0,0 +1,80 @@ +"""Test the mech cli. """ +import subprocess +import re + +from unittest.mock import patch + +import mech.command +import mech.mech + + +def test_version(): + rc, out = subprocess.getstatusoutput('mech --version') + assert re.match(r'mech v[0-9]+\.[0-9]+\.[0-9]', out) + assert rc == 0 + + +def test_help(): + rc, out = subprocess.getstatusoutput('mech --help') + assert re.match(r'Usage: mech ', out) + assert rc == 0 + + +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' + } +} +@patch('mech.utils.load_mechfile', return_value=first_dict) +def test_mech_list_with_one(mock_load_mechfile, capfd): + global_arguments = {'--debug': False} + m = mech.mech.Mech(arguments=global_arguments) + list_arguments = {'--detail': False} + m.list(list_arguments) + out, _ = capfd.readouterr() + mock_load_mechfile.assert_called() + assert re.search(r'first\s+notcreated', out, re.MULTILINE) + + +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' + } +} +@patch('mech.utils.load_mechfile', return_value=two_dict) +def test_mech_list_with_two(mock_load_mechfile, capfd): + global_arguments = {'--debug': False} + m = mech.mech.Mech(arguments=global_arguments) + list_arguments = {'--detail': False} + m.list(list_arguments) + out, _ = capfd.readouterr() + 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) diff --git a/test_mech.py b/test_mech.py deleted file mode 100644 index 0f2f91a..0000000 --- a/test_mech.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Test the mech cli. """ -import subprocess -import re - - -def test_version(): - rc, out = subprocess.getstatusoutput('mech --version') - assert re.match(r'mech v[0-9]+\.[0-9]+\.[0-9]', out) - assert rc == 0 - - -def test_help(): - rc, out = subprocess.getstatusoutput('mech --help') - assert re.match(r'Usage: mech ', out) - assert rc == 0 From 0b5e76da483491fdba38e239a02f24c95006c3f2 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Fri, 7 Feb 2020 16:26:09 -0800 Subject: [PATCH 041/116] disable deprecated warnings for now; mock out the utils.locate function --- mech/test_mech.py | 22 ++++++++++++---------- pytest.ini | 4 ++++ 2 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 pytest.ini diff --git a/mech/test_mech.py b/mech/test_mech.py index efa2f1a..06cf2c4 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -6,6 +6,7 @@ import mech.command import mech.mech +import mech.vmrun def test_version(): @@ -20,31 +21,30 @@ def test_help(): assert rc == 0 -first_dict = { +mechfile_one_entry = { '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' + '201912.04.0' } } -@patch('mech.utils.load_mechfile', return_value=first_dict) -def test_mech_list_with_one(mock_load_mechfile, capfd): +@patch('mech.utils.load_mechfile', return_value=mechfile_one_entry) +@patch('mech.utils.locate', return_value=None) +def test_mech_list_with_one(mock_locate, mock_load_mechfile, capfd): global_arguments = {'--debug': False} m = mech.mech.Mech(arguments=global_arguments) list_arguments = {'--detail': False} m.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) -two_dict = { +mechfile_two_entries = { 'first': { 'name': 'first', @@ -68,13 +68,15 @@ def test_mech_list_with_one(mock_load_mechfile, capfd): 'versions/201912.04.0/providers/vmware_desktop.box' } } -@patch('mech.utils.load_mechfile', return_value=two_dict) -def test_mech_list_with_two(mock_load_mechfile, capfd): +@patch('mech.utils.load_mechfile', return_value=mechfile_two_entries) +@patch('mech.utils.locate', return_value=None) +def test_mech_list_with_two(mock_locate, mock_load_mechfile, capfd): global_arguments = {'--debug': False} m = mech.mech.Mech(arguments=global_arguments) list_arguments = {'--detail': False} m.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) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..50bd45c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] + +filterwarnings = + ignore::DeprecationWarning From 3673cfed1925b8d77e2fc5329cdcd5391753d790 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 8 Feb 2020 10:14:47 -0800 Subject: [PATCH 042/116] added unit tests for mech port --- mech/mech.py | 13 +++++---- mech/test_mech.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index afedba7..a29a906 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -87,6 +87,7 @@ def __init__(self, name, mechfile=None): if mechfile.get(name, None): self.name = name else: + # TODO: review if we want to change to print stderr puts_err(colored.red("Instance ({}) was not found in the Mechfile".format(name))) sys.exit(1) self.box = mechfile[name].get('box', None) @@ -1186,14 +1187,16 @@ def port(self, arguments): for instance in instances: inst = MechInstance(instance) + print('Instance ({}):'. format(instance)) + nat_found = False vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - for network in vmrun.listHostNetworks().split('\n'): - network = network.split() + for line in vmrun.listHostNetworks().split('\n'): + network = line.split() if len(network) > 2 and network[2] == 'nat': print(vmrun.listPortForwardings(network[1])) - break - else: - puts_err(colored.red("Cannot find a nat network")) + nat_found = True + if not nat_found: + print(colored.red("Cannot find a nat network"), file=sys.stderr) def list(self, arguments): """ diff --git a/mech/test_mech.py b/mech/test_mech.py index 06cf2c4..55798b7 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -80,3 +80,71 @@ def test_mech_list_with_two(mock_locate, mock_load_mechfile, capfd): 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) + + +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.listPortForwardings', return_value='Total port forwardings: 0') +@patch('mech.vmrun.VMrun.listHostNetworks', return_value=host_networks) +@patch('mech.utils.load_mechfile', return_value=mechfile_one_entry) +@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): + global_arguments = {'--debug': False} + m = mech.mech.Mech(arguments=global_arguments) + port_arguments = {} + port_arguments = {'': None} + m.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.listPortForwardings', return_value='Total port forwardings: 0') +@patch('mech.vmrun.VMrun.listHostNetworks', return_value=host_networks) +@patch('mech.utils.load_mechfile', return_value=mechfile_two_entries) +@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): + global_arguments = {'--debug': False} + m = mech.mech.Mech(arguments=global_arguments) + port_arguments = {} + port_arguments = {'': None} + m.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.listHostNetworks', return_value=host_networks_without_nat) +@patch('mech.utils.load_mechfile', return_value=mechfile_one_entry) +@patch('mech.utils.locate', return_value=None) +def test_mech_port_without_nat(mock_locate, mock_load_mechfile, mock_list_host_networks, capfd): + global_arguments = {'--debug': False} + m = mech.mech.Mech(arguments=global_arguments) + port_arguments = {} + port_arguments = {'': None} + m.port(port_arguments) + out, 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) From 5dea760fc8bbe8d04422bb04deb495ca0a5f5e8d Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 8 Feb 2020 12:42:40 -0800 Subject: [PATCH 043/116] add mech box list unit tests --- mech/test_mech_box.py | 58 +++++++++++++++++++++++++++++++++++++++++++ mech/test_utils.py | 17 ++++++++----- mech/utils.py | 7 ++---- 3 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 mech/test_mech_box.py diff --git a/mech/test_mech_box.py b/mech/test_mech_box.py new file mode 100644 index 0000000..2688ebc --- /dev/null +++ b/mech/test_mech_box.py @@ -0,0 +1,58 @@ +"""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_empty_mechdir(mock_os_getcwd, capfd): + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + m = mech.mech.MechBox(arguments=global_arguments) + with patch('os.walk') as mock_walk: + # root, dirs, files + mock_walk.return_value = [('./tmp', [], []), ] + m.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): + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + m = mech.mech.MechBox(arguments=global_arguments) + with patch('os.walk') as mock_walk: + # root, dirs, files + mock_walk.return_value = [('/tmp', ['boxes', ], []), ] + m.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): + mock_os_getcwd.return_value = '/tmp' + global_arguments = {'--debug': False} + m = 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']), + ] + m.list({}) + mock_walk.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'ubuntu-18.04', out, re.MULTILINE) diff --git a/mech/test_utils.py b/mech/test_utils.py index 42a9bb5..c397070 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -6,15 +6,20 @@ import mech.utils -def test_main_dir(): +@patch('os.getcwd') +def test_main_dir(mock_os_getcwd): + mock_os_getcwd.return_value = '/tmp' main = mech.utils.main_dir() - assert main.startswith('/') + mock_os_getcwd.assert_called() + assert main == '/tmp' -def test_mech_dir(): - mech_dir = mech.utils.mech_dir() - assert mech_dir.startswith('/') - assert mech_dir.endswith('/.mech') +@patch('os.getcwd') +def test_mech_dir(mock_os_getcwd): + mock_os_getcwd.return_value = '/tmp' + mechdir = mech.utils.mech_dir() + mock_os_getcwd.assert_called() + assert mechdir == '/tmp/.mech' def test_save_mechfile_empty_config(): diff --git a/mech/utils.py b/mech/utils.py index 6186c29..1361b1d 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -47,17 +47,14 @@ logger = logging.getLogger(__name__) -MAIN_DIR = os.path.expanduser(os.getcwd()) - - def main_dir(): """Return the main directory.""" - return MAIN_DIR + return os.getcwd() def mech_dir(): """Return the mech directory.""" - return os.path.join(MAIN_DIR, '.mech') + return os.path.join(main_dir(), '.mech') def makedirs(name, mode=0o777): From ccfe1957e7b6a28c88e4a4025fa3079f2d9097b3 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 8 Feb 2020 14:41:20 -0800 Subject: [PATCH 044/116] pylint fixes --- CONTRIBUTING.md | 2 +- mech/mech.py | 210 ++++++++++++++++++++++--------------------- mech/utils.py | 232 +++++++++++++++++++++++++----------------------- mech/vmrun.py | 8 ++ 4 files changed, 240 insertions(+), 212 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index adb19b6..ba231b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ source venv/bin/activate python setup.py install # if doing development -pip install flake8 pytest pytest_mock mock pytest-cov +pip install flake8 pytest pytest_mock mock pytest-cov pylint # also optional pip install autopep8 diff --git a/mech/mech.py b/mech/mech.py index a29a906..9397c11 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -21,6 +21,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # +"""Mech classes.""" from __future__ import print_function, absolute_import @@ -41,7 +42,7 @@ from .vmrun import VMrun from .command import Command -logger = logging.getLogger(__name__) +LOGGER = logging.getLogger(__name__) DEFAULT_USER = 'vagrant' DEFAULT_PASSWORD = 'vagrant' @@ -76,18 +77,18 @@ class MechInstance(): + """Class to hold a mech instance (aka virtual machine).""" def __init__(self, name, mechfile=None): - """Class to hold instance (aka virtual machine).""" + """Constructor for the mech instance.""" if name == "": raise AttributeError("Must provide a name for the instance.") if not mechfile: mechfile = utils.load_mechfile() - logger.debug("loaded mechfile:{}".format(mechfile)) + LOGGER.debug("loaded mechfile:%s", mechfile) if mechfile.get(name, None): self.name = name else: - # TODO: review if we want to change to print stderr puts_err(colored.red("Instance ({}) was not found in the Mechfile".format(name))) sys.exit(1) self.box = mechfile[name].get('box', None) @@ -126,10 +127,12 @@ def __repr__(self): sep=sep)) def config_ssh(self): + """Configure ssh to work. Create a insecure private key file for ssh/scp.""" vmrun = VMrun(self.vmx, user=self.user, password=self.password) lookup = self.enable_ip_lookup - ip = vmrun.getGuestIPAddress(wait=False, lookup=lookup) if vmrun.installedTools() else None - if not ip: + ip_address = vmrun.getGuestIPAddress(wait=False, + lookup=lookup) if vmrun.installedTools() else None + if not ip_address: 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. " @@ -141,8 +144,8 @@ def config_ssh(self): insecure_private_key = os.path.abspath(os.path.join( utils.mech_dir(), "insecure_private_key")) if not os.path.exists(insecure_private_key): - with open(insecure_private_key, 'w') as f: - f.write(INSECURE_PRIVATE_KEY) + with open(insecure_private_key, 'w') as the_file: + the_file.write(INSECURE_PRIVATE_KEY) os.chmod(insecure_private_key, 0o400) self.config = { "Host": self.name, @@ -155,32 +158,32 @@ def config_ssh(self): "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) + 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() - k = re.sub(r' (\w)', callback, k) - if k[0].islower(): - k = k[0].upper() + k[1:] - self.config[k] = v + 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, + "HostName": ip_address, }) return self.config class MechCommand(Command): - """Class to hold the Mechfile (as python object).""" + """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:{}".format(self.mechfile)) + LOGGER.debug("loaded mechfile:%s", self.mechfile) def instances(self): """Returns a list of the instances from the Mechfile.""" @@ -201,7 +204,7 @@ class MechBox(MechCommand): For help on any individual subcommand run `mech box -h` """ - def add(self, arguments): + def add(self, arguments): # pylint: disable=no-self-use """ Add a box to the catalog of available boxes. @@ -230,7 +233,7 @@ def add(self, arguments): utils.add_box(name=None, box=location, box_version=box_version, force=force, requests_kwargs=requests_kwargs) - def list(self, arguments): + def list(self, arguments): # pylint: disable=no-self-use """ List all available boxes in the catalog. @@ -245,7 +248,7 @@ def list(self, arguments): 'VERSION'.rjust(12), )) path = os.path.abspath(os.path.join(utils.mech_dir(), 'boxes')) - for root, dirnames, filenames in os.walk(path): + 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] @@ -257,7 +260,7 @@ def list(self, arguments): # add alias for 'mech box ls' ls = list - def remove(self, arguments): + def remove(self, arguments): # pylint: disable=no-self-use """ Remove a box from mech that matches the given name and version. @@ -288,7 +291,7 @@ class MechSnapshot(MechCommand): For help on any individual subcommand run `mech snapshot -h` """ - def delete(self, arguments): + def delete(self, arguments): # pylint: disable=no-self-use """ Delete a snapshot taken previously with snapshot save. @@ -338,7 +341,7 @@ def list(self, arguments): # add alias for 'mech snapshot ls' ls = list - def save(self, arguments): + def save(self, arguments): # pylint: disable=no-self-use """ Take a snapshot of the current state of the machine. @@ -426,7 +429,7 @@ 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. @@ -458,8 +461,7 @@ def init(self, arguments): force = arguments['--force'] requests_kwargs = utils.get_requests_kwargs(arguments) - logger.debug('name:{} box:{} box_version:{} location:{}'.format( - name, box, box_version, location)) + 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( @@ -482,7 +484,7 @@ def init(self, arguments): else: puts_err(colored.red("Couldn't initialize mech")) - def add(self, arguments): + def add(self, arguments): # pylint: disable=no-self-use """ Add instance to the Mechfile. @@ -512,8 +514,7 @@ def add(self, arguments): requests_kwargs = utils.get_requests_kwargs(arguments) - logger.debug('name:{} box:{} box_version:{} location:{}'.format( - name, box, box_version, location)) + LOGGER.debug('name:%s box:%s box_version:%s location:%s', name, box, box_version, location) puts_err(colored.green("Adding ({}) to the Mechfile.".format(name))) @@ -542,7 +543,7 @@ def remove(self, arguments): puts_err(colored.red("Need to provide a name to be removed from the Mechfile.")) sys.exit(1) - logger.debug('name:{}'.format(name)) + LOGGER.debug('name:%s', name) self.activate_mechfile() inst = self.mechfile.get(name, None) @@ -559,7 +560,7 @@ def remove(self, arguments): # add alias for 'mech delete' delete = remove - def up(self, arguments): + def up(self, arguments): # pylint: disable=invalid-name """ Starts and provisions the mech environment. @@ -593,10 +594,9 @@ def up(self, arguments): instance_name = arguments[''] - logger.debug('gui:{} disable_shared_folders:{} disable_provisioning:{} ' - 'save:{} numvcpus:{} memsize:{}'.format(gui, disable_shared_folders, - disable_provisioning, save, - numvcpus, memsize)) + LOGGER.debug('gui:%s disable_shared_folders:%s disable_provisioning:%s ' + 'save:%s numvcpus:%s memsize:%s', gui, disable_shared_folders, + disable_provisioning, save, numvcpus, memsize) if instance_name: # single instance @@ -635,22 +635,22 @@ def up(self, arguments): time.sleep(3) puts_err(colored.blue("Getting IP address...")) lookup = inst.enable_ip_lookup - ip = vmrun.getGuestIPAddress(lookup=lookup) + ip_address = vmrun.getGuestIPAddress(lookup=lookup) if not disable_shared_folders: puts_err(colored.blue("Sharing current folder...")) vmrun.enableSharedFolders(quiet=False) vmrun.addSharedFolder('mech', utils.main_dir(), quiet=True) - if ip: + if ip_address: if started: - puts_err(colored.green("VM ({}) started " - "on {}".format(instance, ip))) + puts_err(colored.green("VM ({})" + "started on {}".format(instance, ip_address))) else: puts_err(colored.yellow("VM ({}) was already started " - "on {}".format(instance, ip))) + "on {}".format(instance, ip_address))) else: if started: puts_err(colored.green("VM ({}) started on an unknown " - "IP address".format(instance))) + "IP address".format(instance))) else: puts_err(colored.yellow("VM ({}) was already started on an " "unknown IP address".format(instance))) @@ -661,7 +661,7 @@ def up(self, arguments): # 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 """ Outputs info about all VMs running on this computer. @@ -673,7 +673,7 @@ def global_status(self, arguments): 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. @@ -687,6 +687,9 @@ def ps(self, arguments): vmrun = VMrun(inst.vmx, inst.user, inst.password) print(vmrun.listProcessesInGuest()) + # alias "mech process_status" to "mech ps" + process_status = ps + def status(self, arguments): """ Outputs status of the instances. @@ -711,20 +714,20 @@ def status(self, arguments): vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) lookup = inst.enable_ip_lookup - ip = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) + ip_address = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) state = vmrun.checkToolsState(quiet=True) print("Current machine state:" + os.linesep) - if ip is None: - ip = "poweroff" - elif not ip: - ip = "unknown" - print("%s\t%s\t%s\t(VMware Tools %s)" % (inst.name, inst.box, ip, state)) + 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 == "poweroff": + if ip_address == "poweroff": print(os.linesep + "The VM is powered off. To restart the VM, " "simply run `mech up {}`".format(instance)) - elif ip == "unknown": + 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"): @@ -756,9 +759,8 @@ def destroy(self, arguments): 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'): + if force or utils.confirm("Are you sure you want to delete {} " + "at {}".format(inst.name, inst.path), default='n'): puts_err(colored.green("Deleting ({})...".format(instance))) vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) vmrun.stop(mode='hard', quiet=True) @@ -768,7 +770,7 @@ def destroy(self, arguments): if os.path.exists(inst.path): shutil.rmtree(inst.path) else: - logger.debug("{} was not found.".format(inst.path)) + LOGGER.debug("%s was not found.", inst.path) else: puts_err(colored.red("Deletion aborted")) else: @@ -851,8 +853,8 @@ def resume(self, arguments): instance_name = arguments[''] disable_shared_folders = arguments['--disable-shared-folders'] - logger.debug('instance_name:{} ' - 'disable_shared_folders:{}'.format(instance_name, disable_shared_folders)) + LOGGER.debug('instance_name:%s ' + 'disable_shared_folders:%s', instance_name, disable_shared_folders) if instance_name: # single instance @@ -863,7 +865,7 @@ def resume(self, arguments): for instance in instances: inst = MechInstance(instance) - logger.debug('instance:{} inst.vmx:{}'.format(instance, inst.vmx)) + LOGGER.debug('instance:%s inst.vmx:%s', instance, inst.vmx) # if we have started this instance before, try to unpause if inst.vmx: @@ -874,7 +876,7 @@ def resume(self, arguments): time.sleep(1) puts_err(colored.blue("Getting IP address...")) lookup = inst.enable_ip_lookup - ip = vmrun.getGuestIPAddress(lookup=lookup) + ip_address = vmrun.getGuestIPAddress(lookup=lookup) if not disable_shared_folders: puts_err(colored.blue("Sharing current folder...")) vmrun.enableSharedFolders(quiet=False) @@ -882,8 +884,8 @@ def resume(self, arguments): else: puts_err(colored.blue("Disabling shared folders...")) vmrun.disableSharedFolders(quiet=False) - if ip: - puts_err(colored.green("VM resumed on {}".format(ip))) + if ip_address: + puts_err(colored.green("VM resumed on {}".format(ip_address))) else: puts_err(colored.green("VM resumed on an unknown IP address")) @@ -897,25 +899,25 @@ def resume(self, arguments): time.sleep(3) puts_err(colored.blue("Getting IP address...")) lookup = inst.enable_ip_lookup - ip = vmrun.getGuestIPAddress(lookup=lookup) + ip_address = vmrun.getGuestIPAddress(lookup=lookup) if not disable_shared_folders: puts_err(colored.blue("Sharing current folder...")) vmrun.enableSharedFolders(quiet=False) vmrun.addSharedFolder('mech', utils.main_dir(), quiet=True) - if ip: + if ip_address: if started: puts_err(colored.green("VM ({}) started on " - "{}".format(instance, ip))) + "{}".format(instance, ip_address))) else: puts_err(colored.yellow("VM ({}) already was started " - "on {}".format(instance, ip))) + "on {}".format(instance, ip_address))) else: if started: puts_err(colored.green("VM ({}) started on an unknown " - "IP address".format(instance))) + "IP address".format(instance))) else: - puts_err(colored.yellow("VM ({}) already was started " - "on an unknown IP address".format(instance))) + puts_err(colored.yellow("VM ({}) already was started on an " + "unknown IP address".format(instance))) else: puts_err(colored.red("Need to start VM first")) @@ -967,7 +969,7 @@ def ssh_config(self, arguments): inst = MechInstance(instance) print(utils.config_ssh_string(inst.config_ssh())) - def ssh(self, arguments): + def ssh(self, arguments): # pylint: disable=no-self-use """ Connects to machine via SSH. @@ -987,14 +989,14 @@ def ssh(self, arguments): inst = MechInstance(instance) config_ssh = inst.config_ssh() - fp = tempfile.NamedTemporaryFile(delete=False) + temp_file = tempfile.NamedTemporaryFile(delete=False) try: - fp.write(utils.config_ssh_string(config_ssh).encode('utf-8')) - fp.close() + temp_file.write(utils.config_ssh_string(config_ssh).encode('utf-8')) + temp_file.close() cmds = ['ssh'] if not plain: - cmds.extend(('-F', fp.name)) + cmds.extend(('-F', temp_file.name)) if extra: cmds.extend(extra) if not plain: @@ -1002,7 +1004,7 @@ def ssh(self, arguments): if command: cmds.extend(('--', command)) - logger.debug( + LOGGER.debug( " ".join( "'{}'".format( c.replace( @@ -1010,9 +1012,9 @@ def ssh(self, arguments): "\\'")) if ' ' in c else c for c in cmds)) return subprocess.call(cmds) finally: - os.unlink(fp.name) + os.unlink(temp_file.name) - def scp(self, arguments): + def scp(self, arguments): # pylint: disable=no-self-use """ Copies files to and from the machine via SCP. @@ -1043,14 +1045,14 @@ def scp(self, arguments): inst = MechInstance(instance) config_ssh = inst.config_ssh() - fp = tempfile.NamedTemporaryFile(delete=False) + temp_file = tempfile.NamedTemporaryFile(delete=False) try: - fp.write(utils.config_ssh_string(config_ssh).encode()) - fp.close() + temp_file.write(utils.config_ssh_string(config_ssh).encode()) + temp_file.close() cmds = ['scp'] - cmds.extend(('-F', fp.name)) + cmds.extend(('-F', temp_file.name)) if extra: cmds.extend(extra) @@ -1059,7 +1061,7 @@ def scp(self, arguments): src = '{}:{}'.format(host, src) if src_is_host else src cmds.extend((src, dst)) - logger.debug( + LOGGER.debug( " ".join( "'{}'".format( c.replace( @@ -1067,9 +1069,9 @@ def scp(self, arguments): "\\'")) if ' ' in c else c for c in cmds)) return subprocess.call(cmds) finally: - os.unlink(fp.name) + os.unlink(temp_file.name) - def ip(self, arguments): + def ip(self, arguments): # pylint: disable=invalid-name,no-self-use """ Outputs ip of the Mech machine. @@ -1085,14 +1087,17 @@ def ip(self, arguments): if inst.created: vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) lookup = inst.enable_ip_lookup - ip = vmrun.getGuestIPAddress(lookup=lookup) - if ip: - puts_err(colored.green(ip)) + ip_address = vmrun.getGuestIPAddress(lookup=lookup) + if ip_address: + puts_err(colored.green(ip_address)) else: puts_err(colored.red("Unknown IP address")) else: puts_err(colored.yellow("VM not created")) + # alias 'mech ip_address' to 'mech ip' + ip_address = ip + def provision(self, arguments): """ Provisions the Mech machine. @@ -1148,20 +1153,21 @@ def reload(self, arguments): time.sleep(3) puts_err(colored.blue("Getting IP address...")) lookup = inst.enable_ip_lookup - ip = vmrun.getGuestIPAddress(lookup=lookup) - if ip: + ip_address = vmrun.getGuestIPAddress(lookup=lookup) + if ip_address: if started: - puts_err(colored.green("VM ({}) started on {}".format(instance, ip))) + puts_err(colored.green("VM ({}) started " + "on {}".format(instance, ip_address))) else: puts_err(colored.yellow("VM ({}) already was started on " - "{}".format(instance, ip))) + "{}".format(instance, ip_address))) else: if started: puts_err(colored.green("VM ({}) started on an unknown IP " - "address".format(instance))) + "address".format(instance))) else: puts_err(colored.yellow("VM ({}) already was started " - "on an unknown IP address".format(instance))) + "on an unknown IP address".format(instance))) def port(self, arguments): """ @@ -1229,15 +1235,15 @@ def list(self, arguments): if inst.created: vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) lookup = inst.enable_ip_lookup - ip = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) - if ip is None: - ip = colored.yellow("poweroff") - elif not ip: - ip = colored.green("running") + ip_address = vmrun.getGuestIPAddress(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 = colored.green(ip) + ip_address = colored.green(ip_address) else: - ip = "notcreated" + ip_address = "notcreated" if detail: print(inst) @@ -1245,7 +1251,7 @@ def list(self, arguments): else: print("{}\t{}\t{}\t{}".format( colored.green(name.rjust(20)), - ip.rjust(15), + ip_address.rjust(15), inst.box.rjust(35), inst.box_version.rjust(12) )) diff --git a/mech/utils.py b/mech/utils.py index 1361b1d..3fc0a53 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -22,6 +22,8 @@ # IN THE SOFTWARE. # +"""Mech utility functions.""" + from __future__ import division, absolute_import import os @@ -44,7 +46,7 @@ from .compat import raw_input, b2s from .vmrun import VMrun -logger = logging.getLogger(__name__) +LOGGER = logging.getLogger(__name__) def main_dir(): @@ -74,42 +76,41 @@ 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_entry(mechfile_entry, name, mechfile_should_exist=False): """Save the entry to the Mechfile.""" - logger.debug('mechfile_entry:{} name:{} ' - 'mechfile_should_exist:{}'.format(mechfile_entry, name, - mechfile_should_exist)) + 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:{} mechfile:{}".format(name, mechfile)) + 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:{} mechfile_should_exist:{}'.format(name, mechfile_should_exist)) + 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:{} mechfile:{}".format(name, mechfile)) + LOGGER.debug("after removing name:%s mechfile:%s", name, mechfile) return save_mechfile(mechfile) @@ -117,15 +118,15 @@ def save_mechfile(mechfile): """Save the mechfile object to a file called 'Mechfile'. Return True if save was successful. """ - logger.debug('mechfile:{}'.format(mechfile)) - with open(os.path.join(main_dir(), 'Mechfile'), 'w+') as fp: - json.dump(mechfile, fp, sort_keys=True, indent=2, separators=(',', ': ')) + 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): """Locate a file in the path provided.""" - for root, dirnames, filenames in os.walk(path): + 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)) @@ -136,8 +137,8 @@ def parse_vmx(path): 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() @@ -192,12 +193,12 @@ def update_vmx(path, numvcpus=None, memsize=None): def load_mechfile(should_exist=True): """Load the Mechfile from disk and return the object.""" mechfile_fullpath = os.path.join(main_dir(), 'Mechfile') - logger.debug("mechfile_fullpath:{}".format(mechfile_fullpath)) + LOGGER.debug("mechfile_fullpath:%s", mechfile_fullpath) if os.path.isfile(mechfile_fullpath): - with open(mechfile_fullpath) as fp: + with open(mechfile_fullpath) as the_file: try: - mechfile = json.loads(fp.read()) - logger.debug('mechfile:{}'.format(mechfile)) + mechfile = json.loads(the_file.read()) + LOGGER.debug('mechfile:%s', mechfile) return mechfile except ValueError: puts_err(colored.red("Invalid Mechfile." + os.linesep)) @@ -215,10 +216,11 @@ def load_mechfile(should_exist=True): return {} -def build_mechfile_entry(location, box=None, name=None, box_version=None, requests_kwargs={}): +def build_mechfile_entry(location, box=None, name=None, box_version=None, requests_kwargs=None): """Build the Mechfile from the inputs.""" - logger.debug("location:{} name:{} box:{} box_version:{}".format( - location, name, box, box_version)) + LOGGER.debug("location:%s name:%s box:%s box_version:%s", location, name, box, box_version) + if requests_kwargs is None: + requests_kwargs = {} mechfile_entry = {} if location is None: return mechfile_entry @@ -233,53 +235,53 @@ def build_mechfile_entry(location, box=None, name=None, box_version=None, reques return mechfile_entry elif location.startswith('file:') or os.path.isfile(re.sub(r'^file:(?://)?', '', location)): location = re.sub(r'^file:(?://)?', '', location) - logger.debug('location:{}'.format(location)) + LOGGER.debug('location:%s', location) try: # see if the location is a json file - with open(location) as fp: - catalog = json.loads(fp.read()) - except Exception: + with open(location) as the_file: + catalog = json.loads(the_file.read()) + except ValueError: # includes simplejson.decoder.JSONDecodeError mechfile_entry['file'] = location if not name: name = 'first' mechfile_entry['box'] = box if box_version: mechfile_entry['box_version'] = box_version - logger.debug('mechfile_entry:{}'.format(mechfile_entry)) + LOGGER.debug('mechfile_entry:%s', mechfile_entry) return mechfile_entry else: try: - account, box, v = (location.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: - box_version = v + if ver: + box_version = ver puts_err( colored.blue("Loading metadata for box '{}'{}".format( - location, " ({})".format(box_version) if box_version else ""))) + 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, **requests_kwargs) + 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) except requests.ConnectionError: puts_err(colored.red("Couldn't connect to HashiCorp's Vagrant Cloud API")) sys.exit(1) - logger.debug("catalog:{} name:{} box_version:{}".format(catalog, name, box_version)) + 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, box=None, box_version=None): """Convert the Hashicorp cloud catalog entry to Mechfile entry.""" - logger.debug('catalog:{} name:{} box:{} box_version:{}'.format(catalog, name, box, box_version)) + 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'] + for ver in versions: + current_version = ver['version'] if not box_version or current_version == box_version: - for provider in v['providers']: + for provider in ver['providers']: if 'vmware' in provider['name']: mechfile['name'] = name mechfile['box'] = catalog['name'] @@ -306,7 +308,7 @@ def tar_cmd(*args, **kwargs): 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') @@ -319,14 +321,15 @@ def tar_cmd(*args, **kwargs): def init_box(name, box=None, box_version=None, location=None, force=False, save=True, - instance_path=None, requests_kwargs={}, numvcpus=None, + instance_path=None, requests_kwargs=None, numvcpus=None, memsize=None): """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:{} box:{} box_version:{} location:{}".format( - name, box, box_version, location)) + if requests_kwargs is None: + requests_kwargs = {} + 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, @@ -342,7 +345,7 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= box_parts = box.split('/') box_dir = os.path.join(*filter(None, (mech_dir(), 'boxes', - box_parts[0], box_parts[1], box_version))) + box_parts[0], box_parts[1], box_version))) box_file = locate(box_dir, '*.box') puts_err(colored.blue("Extracting box '{}'...".format(box_file))) @@ -377,11 +380,13 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= def add_box(name=None, box=None, box_version=None, location=None, - force=False, save=True, requests_kwargs={}): + force=False, save=True, requests_kwargs=None): """Add a box.""" + if requests_kwargs is None: + requests_kwargs = {} # build the dict - logger.debug('name:{} box:{} box_version:{} location:{}'.format( - name, box, box_version, location)) + 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, @@ -401,9 +406,12 @@ def add_box(name=None, box=None, box_version=None, location=None, def add_mechfile(mechfile_entry, name=None, box=None, box_version=None, - location=None, force=False, save=True, requests_kwargs={}): - logger.debug('mechfile_entry:{} name:{} box:{} box_version:{} location:{}'.format( - mechfile_entry, name, box, box_version, location)) + location=None, force=False, save=True, requests_kwargs=None): + """Add a mechfile entry.""" + if requests_kwargs is None: + requests_kwargs = {} + 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') @@ -426,44 +434,46 @@ def add_mechfile(mechfile_entry, name=None, box=None, box_version=None, name, " ({})".format(box_version) if box_version else ""))) -def add_box_url(name, box, box_version, url, force=False, save=True, requests_kwargs={}): +def add_box_url(name, box, box_version, url, force=False, save=True, requests_kwargs=None): """Add a box using the URL.""" - logger.debug('name:{} box:{} box_version:{} url:{}'.format(name, box, box_version, url)) + if requests_kwargs is None: + requests_kwargs = {} + LOGGER.debug('name:%s box:%s box_version:%s url:%s', name, box, box_version, url) boxname = os.path.basename(url) box_parts = box.split('/') box_dir = os.path.join(*filter(None, (mech_dir(), 'boxes', - box_parts[0], box_parts[1], box_version))) + box_parts[0], box_parts[1], box_version))) exists = os.path.exists(box_dir) if not exists or force: if exists: puts_err(colored.blue("Attempting to download box '{}'...".format(box))) else: puts_err(colored.blue("Box '{}' could not be found. " - "Attempting to download...".format(box))) + "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() + response = requests.get(url, stream=True, **requests_kwargs) + 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( + 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) + catalog = json.load(the_file.name) mechfile = catalog_to_mechfile(catalog, name, box, box_version) return add_mechfile( mechfile, @@ -475,10 +485,10 @@ def add_box_url(name, box, box_version, url, force=False, save=True, requests_kw else: # Otherwise it must be a valid box: return add_box_file(box=box, box_version=box_version, - filename=fp.name, url=url, force=force, + 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) @@ -531,31 +541,33 @@ def add_box_file(box=None, box_version=None, filename=None, url=None, force=Fals return box, box_version -def init_mechfile(location=None, box=None, name=None, box_version=None, requests_kwargs={}): +def init_mechfile(location=None, box=None, name=None, box_version=None, requests_kwargs=None): """Initialize the Mechfile.""" - logger.debug("name:{} box:{} box_version:{} location:{}".format( - name, box, box_version, location)) + if requests_kwargs is None: + requests_kwargs = {} + LOGGER.debug("name:%s box:%s box_version:%s location:%s", name, box, box_version, location) mechfile_entry = build_mechfile_entry( location=location, box=box, name=name, box_version=box_version, requests_kwargs=requests_kwargs) - logger.debug('mechfile_entry:{}'.format(mechfile_entry)) + 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, requests_kwargs={}): +def add_to_mechfile(location=None, box=None, name=None, box_version=None, requests_kwargs=None): """Add entry to the Mechfile.""" - logger.debug("name:{} box:{} box_version:{} location:{}".format( - name, box, box_version, location)) + if requests_kwargs is None: + requests_kwargs = {} + LOGGER.debug("name:%s box:%s box_version:%s location:%s", name, box, box_version, location) this_mech_entry = build_mechfile_entry( location=location, box=box, name=name, box_version=box_version, requests_kwargs=requests_kwargs) - logger.debug('this_mech_entry:{}'.format(this_mech_entry)) + LOGGER.debug('this_mech_entry:%s', this_mech_entry) return save_mechfile_entry(this_mech_entry, name, mechfile_should_exist=False) @@ -573,7 +585,7 @@ def get_requests_kwargs(arguments): return requests_kwargs -def provision(instance, vmx, user, password, provision, show): +def provision(instance, vmx, user, password, provision_config, show): """Provision an instance. provision types: @@ -599,16 +611,16 @@ def provision(instance, vmx, user, password, provision, show): return provisioned = 0 - if provision: - for i, p in enumerate(provision): - provision_type = p.get('type') + if provision_config: + for i, pro in enumerate(provision_config): + provision_type = pro.get('type') if provision_type == 'file': - source = p.get('source') - destination = p.get('destination') + source = pro.get('source') + destination = pro.get('destination') if show: puts_err(colored.green(" instance:{} provision_type:{} source:{} " - "destination:{}".format(instance, provision_type, - source, destination))) + "destination:{}".format(instance, provision_type, + source, destination))) else: if provision_file(vmrun, source, destination) is None: puts_err(colored.red("Not Provisioned")) @@ -616,15 +628,16 @@ def provision(instance, vmx, user, password, provision, show): provisioned += 1 elif provision_type == 'shell': - inline = p.get('inline') - path = p.get('path') + inline = pro.get('inline') + path = pro.get('path') - args = p.get('args') + args = pro.get('args') if not isinstance(args, list): args = [args] if show: puts_err(colored.green(" instance:{} provision_type:{} inline:{} path:{} " - "args:{}".format(instance, provision_type, inline, path, args))) + "args:{}".format(instance, provision_type, + inline, path, args))) else: if provision_shell(vmrun, inline, path, args) is None: puts_err(colored.red("Not Provisioned")) @@ -636,40 +649,41 @@ def provision(instance, vmx, user, password, provision, show): return else: puts_err(colored.green("VM ({}) Provision {} " - "entries".format(instance, provisioned))) + "entries".format(instance, provisioned))) else: puts_err(colored.blue("Nothing to provision")) -def provision_file(vm, source, destination): +def provision_file(virtual_machine, source, destination): """Provision from file. This simply copies a file from host to guest. """ puts_err(colored.blue("Copying ({}) to ({})".format(source, destination))) - return vm.copyFileFromHostToGuest(source, destination) + return virtual_machine.copyFileFromHostToGuest(source, destination) -def provision_shell(vm, inline, path, args=[]): +def provision_shell(virtual_machine, inline, path, args=None): """Provision from shell.""" - tmp_path = vm.createTempfileInGuest() - logger.debug('inline:{} path:{} args:{} tmp_path:{}'.format( - inline, path, args, tmp_path)) + if args is None: + args = [] + tmp_path = virtual_machine.createTempfileInGuest() + LOGGER.debug('inline:%s path:%s args:%s tmp_path:%s', inline, path, args, tmp_path) if tmp_path is None: 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: + if virtual_machine.copyFileFromHostToGuest(path, tmp_path) is None: return else: if path: if any(path.startswith(s) for s in ('https://', 'http://', 'ftp://')): puts_err(colored.blue("Downloading {}...".format(path))) try: - r = requests.get(path) - r.raise_for_status() - inline = r.read() + response = requests.get(path) + response.raise_for_status() + inline = response.read() except requests.HTTPError: return except requests.ConnectionError: @@ -683,30 +697,30 @@ def provision_shell(vm, inline, path, args=[]): return puts_err(colored.blue("Configuring script to run inline...")) - fp = tempfile.NamedTemporaryFile(delete=False) + the_file = tempfile.NamedTemporaryFile(delete=False) try: - fp.write(str.encode(inline)) - fp.close() - if vm.copyFileFromHostToGuest(fp.name, tmp_path) is None: + the_file.write(str.encode(inline)) + the_file.close() + if virtual_machine.copyFileFromHostToGuest(the_file.name, tmp_path) is None: return finally: - os.unlink(fp.name) + os.unlink(the_file.name) puts_err(colored.blue("Configuring environment...")) - if vm.runScriptInGuest('/bin/sh', "chmod +x '{}'".format(tmp_path)) is None: + if virtual_machine.runScriptInGuest('/bin/sh', "chmod +x '{}'".format(tmp_path)) is None: return puts_err(colored.blue("Executing program...")) - return vm.runProgramInGuest(tmp_path, args) + return virtual_machine.runProgramInGuest(tmp_path, args) finally: - vm.deleteFileInGuest(tmp_path, quiet=True) + virtual_machine.deleteFileInGuest(tmp_path, quiet=True) def config_ssh_string(config_ssh): """Build the ssh-config string.""" ssh_config = "Host {}".format(config_ssh.get('Host', '')) + os.linesep - for k, v in config_ssh.items(): - if k != 'Host': - ssh_config += " {} {}".format(k, v) + os.linesep + for key, value in config_ssh.items(): + if key != 'Host': + ssh_config += " {} {}".format(key, value) + os.linesep return ssh_config diff --git a/mech/vmrun.py b/mech/vmrun.py index f341f24..b160d4f 100644 --- a/mech/vmrun.py +++ b/mech/vmrun.py @@ -35,6 +35,7 @@ def get_fallback_executable(): + """Get a fallback executable for the command line tool 'vmrun'.""" if 'PATH' in os.environ: for path in os.environ['PATH'].split(os.pathsep): vmrun = os.path.join(path, 'vmrun') @@ -46,6 +47,7 @@ def get_fallback_executable(): 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 @@ -53,6 +55,7 @@ def get_darwin_executable(): def get_win32_executable(): + """Get the full path for the 'vmrun' command on a Windows host.""" if PY3: import winreg else: @@ -105,6 +108,9 @@ def get_provider(vmrun_exe): class VMrun(object): + """Interface class for the 'vmrun' command. + The 'vmrun' command is used to interact with VMware. + """ if sys.platform == 'darwin': default_executable = get_darwin_executable() elif sys.platform == 'win32': @@ -114,6 +120,7 @@ class VMrun(object): default_provider = get_provider(default_executable) def __init__(self, vmx_file=None, user=None, password=None, executable=None, provider=None): + """Constructof for the instance. Set some sane defaults.""" self.vmx_file = vmx_file self.user = user self.password = password @@ -121,6 +128,7 @@ def __init__(self, vmx_file=None, user=None, password=None, executable=None, pro self.provider = provider or self.default_provider def vmrun(self, cmd, *args, **kwargs): + """Execute a 'vmrun' command.""" quiet = kwargs.pop('quiet', False) arguments = kwargs.pop('arguments', ()) From 28e9a05ce04016a1ff891f267146036215178faf Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 8 Feb 2020 15:10:27 -0800 Subject: [PATCH 045/116] pylint the tests --- mech/test_mech.py | 63 ++++++++++++++++++++++++------------------- mech/test_mech_box.py | 17 +++++++----- mech/test_utils.py | 55 +++++++++++++++++++++++-------------- setup.py | 6 +++-- 4 files changed, 84 insertions(+), 57 deletions(-) diff --git a/mech/test_mech.py b/mech/test_mech.py index 55798b7..22fc90e 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -10,18 +10,20 @@ def test_version(): - rc, out = subprocess.getstatusoutput('mech --version') + """Test '--version'.""" + return_value, out = subprocess.getstatusoutput('mech --version') assert re.match(r'mech v[0-9]+\.[0-9]+\.[0-9]', out) - assert rc == 0 + assert return_value == 0 def test_help(): - rc, out = subprocess.getstatusoutput('mech --help') + """Test '--help'.""" + return_value, out = subprocess.getstatusoutput('mech --help') assert re.match(r'Usage: mech ', out) - assert rc == 0 + assert return_value == 0 -mechfile_one_entry = { +MECHFILE_ONE_ENTRY = { 'first': { 'name': 'first', @@ -31,20 +33,21 @@ def test_help(): '201912.04.0' } } -@patch('mech.utils.load_mechfile', return_value=mechfile_one_entry) +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) @patch('mech.utils.locate', return_value=None) def test_mech_list_with_one(mock_locate, mock_load_mechfile, capfd): + """Test 'mech list' with one entry.""" global_arguments = {'--debug': False} - m = mech.mech.Mech(arguments=global_arguments) + a_mech = mech.mech.Mech(arguments=global_arguments) list_arguments = {'--detail': False} - m.list(list_arguments) + 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) -mechfile_two_entries = { +MECHFILE_TWO_ENTRIES = { 'first': { 'name': 'first', @@ -68,13 +71,14 @@ def test_mech_list_with_one(mock_locate, mock_load_mechfile, capfd): 'versions/201912.04.0/providers/vmware_desktop.box' } } -@patch('mech.utils.load_mechfile', return_value=mechfile_two_entries) +@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) @patch('mech.utils.locate', return_value=None) def test_mech_list_with_two(mock_locate, mock_load_mechfile, capfd): + """Test 'mech list' with two entries.""" global_arguments = {'--debug': False} - m = mech.mech.Mech(arguments=global_arguments) + a_mech = mech.mech.Mech(arguments=global_arguments) list_arguments = {'--detail': False} - m.list(list_arguments) + a_mech.list(list_arguments) out, _ = capfd.readouterr() mock_locate.assert_called() mock_load_mechfile.assert_called() @@ -82,22 +86,23 @@ def test_mech_list_with_two(mock_locate, mock_load_mechfile, capfd): assert re.search(r'second\s+notcreated', out, re.MULTILINE) -host_networks = """Total host networks: 3 +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.listPortForwardings', return_value='Total port forwardings: 0') -@patch('mech.vmrun.VMrun.listHostNetworks', return_value=host_networks) -@patch('mech.utils.load_mechfile', return_value=mechfile_one_entry) +@patch('mech.vmrun.VMrun.listHostNetworks', return_value=HOST_NETWORKS) +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) @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): + """Test 'mech port' with nat networking.""" global_arguments = {'--debug': False} - m = mech.mech.Mech(arguments=global_arguments) + a_mech = mech.mech.Mech(arguments=global_arguments) port_arguments = {} port_arguments = {'': None} - m.port(port_arguments) + a_mech.port(port_arguments) out, _ = capfd.readouterr() mock_locate.assert_called() mock_load_mechfile.assert_called() @@ -106,22 +111,23 @@ def test_mech_port_with_nat(mock_locate, mock_load_mechfile, mock_list_host_netw assert re.search(r'Total port forwardings: 0', out, re.MULTILINE) -host_networks = """Total host networks: 3 +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.listPortForwardings', return_value='Total port forwardings: 0') -@patch('mech.vmrun.VMrun.listHostNetworks', return_value=host_networks) -@patch('mech.utils.load_mechfile', return_value=mechfile_two_entries) +@patch('mech.vmrun.VMrun.listHostNetworks', return_value=HOST_NETWORKS) +@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) @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): + """Test 'mech port' with nat networking and two instances.""" global_arguments = {'--debug': False} - m = mech.mech.Mech(arguments=global_arguments) + a_mech = mech.mech.Mech(arguments=global_arguments) port_arguments = {} port_arguments = {'': None} - m.port(port_arguments) + a_mech.port(port_arguments) out, _ = capfd.readouterr() mock_locate.assert_called() mock_load_mechfile.assert_called() @@ -130,20 +136,21 @@ def test_mech_port_with_nat_two_hosts(mock_locate, mock_load_mechfile, mock_list assert re.search(r'Total port forwardings: 0', out, re.MULTILINE) -host_networks_without_nat = """Total host networks: 2 +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.listHostNetworks', return_value=host_networks_without_nat) -@patch('mech.utils.load_mechfile', return_value=mechfile_one_entry) +@patch('mech.vmrun.VMrun.listHostNetworks', return_value=HOST_NETWORKS_WITHOUT_NAT) +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) @patch('mech.utils.locate', return_value=None) def test_mech_port_without_nat(mock_locate, mock_load_mechfile, mock_list_host_networks, capfd): + """Test 'mech port' without nat.""" global_arguments = {'--debug': False} - m = mech.mech.Mech(arguments=global_arguments) + a_mech = mech.mech.Mech(arguments=global_arguments) port_arguments = {} port_arguments = {'': None} - m.port(port_arguments) - out, err = capfd.readouterr() + a_mech.port(port_arguments) + _, err = capfd.readouterr() mock_locate.assert_called() mock_load_mechfile.assert_called() mock_list_host_networks.assert_called() diff --git a/mech/test_mech_box.py b/mech/test_mech_box.py index 2688ebc..fc0e9fe 100644 --- a/mech/test_mech_box.py +++ b/mech/test_mech_box.py @@ -9,14 +9,15 @@ @patch('os.getcwd') -def test_mech_box_list_empty_mechdir(mock_os_getcwd, capfd): +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} - m = mech.mech.MechBox(arguments=global_arguments) + a_mech = mech.mech.MechBox(arguments=global_arguments) with patch('os.walk') as mock_walk: # root, dirs, files mock_walk.return_value = [('./tmp', [], []), ] - m.list({}) + a_mech.list({}) mock_walk.assert_called() out, _ = capfd.readouterr() # ensure a header prints out @@ -25,13 +26,14 @@ def test_mech_box_list_empty_mechdir(mock_os_getcwd, capfd): @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' driectory.""" mock_os_getcwd.return_value = '/tmp' global_arguments = {'--debug': False} - m = mech.mech.MechBox(arguments=global_arguments) + a_mech = mech.mech.MechBox(arguments=global_arguments) with patch('os.walk') as mock_walk: # root, dirs, files mock_walk.return_value = [('/tmp', ['boxes', ], []), ] - m.list({}) + a_mech.list({}) mock_walk.assert_called() out, _ = capfd.readouterr() # ensure a header prints out @@ -40,9 +42,10 @@ def test_mech_box_list_empty_boxes_dir(mock_os_getcwd, capfd): @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} - m = mech.mech.MechBox(arguments=global_arguments) + 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 = [ @@ -52,7 +55,7 @@ def test_mech_box_list_one_box(mock_os_getcwd, capfd): ('/tmp/boxes/bento/ubuntu-18.04', ['201912.04.0'], []), ('/tmp/boxes/bento/ubuntu-18.04/201912.04.0', [], ['vmware_desktop.box']), ] - m.list({}) + a_mech.list({}) mock_walk.assert_called() out, _ = capfd.readouterr() assert re.search(r'ubuntu-18.04', out, re.MULTILINE) diff --git a/mech/test_utils.py b/mech/test_utils.py index c397070..24e0f9e 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -1,3 +1,4 @@ +"""Test mech utils.""" import os from unittest.mock import patch, mock_open @@ -8,6 +9,7 @@ @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() @@ -16,6 +18,7 @@ def test_main_dir(mock_os_getcwd): @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() @@ -23,18 +26,19 @@ def test_mech_dir(mock_os_getcwd): def test_save_mechfile_empty_config(): + """Test save_mechfile with empty configuration.""" filename = os.path.join(mech.utils.main_dir(), 'Mechfile') - m = mock_open() - with patch('builtins.open', m, create=True): + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): assert mech.utils.save_mechfile({}) - m.assert_called_once_with(filename, 'w+') - m.return_value.write.assert_called_once_with('{}') + a_mock.assert_called_once_with(filename, 'w+') + a_mock.return_value.write.assert_called_once_with('{}') -# TODO: Is there a better way to get data written? -def _get_data_written(m): +def _get_data_written(a_mock): + """Helper function to get the data written to a mocked file.""" written = '' - for call in m.mock_calls: + for call in a_mock.mock_calls: tmp = '{}'.format(call) if tmp.startswith('call().write('): line = tmp.replace("call().write('", '') @@ -45,6 +49,7 @@ def _get_data_written(m): def test_save_mechfile_one(): + """Test save_mechfile with one entry.""" first_dict = { 'first': { 'name': @@ -67,14 +72,15 @@ def test_save_mechfile_one(): } }''' # noqa: 501 filename = os.path.join(mech.utils.main_dir(), 'Mechfile') - m = mock_open() - with patch('builtins.open', m, create=True): + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): assert mech.utils.save_mechfile(first_dict) - m.assert_called_once_with(filename, 'w+') - assert first_json == _get_data_written(m) + a_mock.assert_called_once_with(filename, 'w+') + assert first_json == _get_data_written(a_mock) def test_save_mechfile_two(): + """Test save_mechfile with two entries.""" two_dict = { 'first': { 'name': @@ -114,11 +120,11 @@ def test_save_mechfile_two(): } }''' # noqa: 501 filename = os.path.join(mech.utils.main_dir(), 'Mechfile') - m = mock_open() - with patch('builtins.open', m, create=True): + a_mock = mock_open() + with patch('builtins.open', a_mock, create=True): assert mech.utils.save_mechfile(two_dict) - m.assert_called_once_with(filename, 'w+') - assert two_json == _get_data_written(m) + a_mock.assert_called_once_with(filename, 'w+') + assert two_json == _get_data_written(a_mock) def test_tar_cmd(): @@ -127,11 +133,13 @@ def test_tar_cmd(): 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", @@ -144,12 +152,13 @@ def test_config_ssh_string_simple(): "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 + 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() @@ -159,6 +168,7 @@ def test_save_mechfile_entry_with_empty_mechfile(load_mock, save_mock): @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() @@ -167,7 +177,8 @@ def test_save_mechfile_entry_with_blank_name(load_mock, save_mock): @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): +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() @@ -177,6 +188,7 @@ def test_save_mechfile_entry_with_name_as_None(load_mock, save_mock): @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() @@ -187,6 +199,7 @@ def test_save_mechfile_entry_twice(load_mock, save_mock): @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() @@ -195,12 +208,14 @@ def test_remove_mechfile_entry_with_empty_mechfile(load_mock, save_mock): @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 = "" @@ -214,7 +229,7 @@ def test_parse_vmx(): ('cleanshutdown', '"FALSE"'), ('config.version', '"8"') ]) - m = mock_open(read_data=partial_vmx) - with patch('builtins.open', m): + a_mock = mock_open(read_data=partial_vmx) + with patch('builtins.open', a_mock): assert mech.utils.parse_vmx(partial_vmx) == expected_vmx - m.assert_called() + a_mock.assert_called() 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 '' From 2ca6d3a539cbf11fd8c523c4418e508205224ec5 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 9 Feb 2020 10:34:52 -0800 Subject: [PATCH 046/116] pylint fixes --- mech/__init__.py | 1 + mech/command.py | 11 ++-- mech/compat.py | 1 + mech/mech.py | 48 +++++++------- mech/test_mech.py | 10 +-- mech/utils.py | 16 ++--- mech/vmrun.py | 162 +++++++++++++++++++++++----------------------- 7 files changed, 128 insertions(+), 121 deletions(-) diff --git a/mech/__init__.py b/mech/__init__.py index 5209036..d9d5dd5 100644 --- a/mech/__init__.py +++ b/mech/__init__.py @@ -21,6 +21,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.7' VERSION = "{} v{}".format(__name__, __version__) diff --git a/mech/command.py b/mech/command.py index af36d18..c3ae923 100644 --- a/mech/command.py +++ b/mech/command.py @@ -20,7 +20,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -"""Handle the mech commands.""" +"""Handle the mech options using docopt.""" from __future__ import absolute_import @@ -44,11 +44,12 @@ def cmd_usage(doc): def docopt_extras(help, version, options, doc): - """Show the extra help info.""" + """Show the "Extra" help info.""" return docopt_extras_ref(help, version, options, cmd_usage(doc)) def DocoptExit____init__(self, message=''): + """Constructor for docopt.""" SystemExit.__init__(self, (message + '\n' + cmd_usage(self.usage)).strip()) @@ -57,13 +58,13 @@ def DocoptExit____init__(self, message=''): def spaced(name): - """Return the 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 [...] """ @@ -73,6 +74,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)) @@ -83,6 +85,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('-', '_') diff --git a/mech/compat.py b/mech/compat.py index 6ff6d8d..3007817 100644 --- a/mech/compat.py +++ b/mech/compat.py @@ -21,6 +21,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 diff --git a/mech/mech.py b/mech/mech.py index 9397c11..b2bac15 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -130,8 +130,8 @@ def config_ssh(self): """Configure ssh to work. Create a insecure private key file for ssh/scp.""" vmrun = VMrun(self.vmx, user=self.user, password=self.password) lookup = self.enable_ip_lookup - ip_address = vmrun.getGuestIPAddress(wait=False, - lookup=lookup) if vmrun.installedTools() else None + ip_address = vmrun.get_guest_ip_address(wait=False, + lookup=lookup) if vmrun.installed_tools() else None if not ip_address: puts_err(colored.red(textwrap.fill( "This Mech machine is reporting that it is not yet ready for SSH. " @@ -306,7 +306,7 @@ def delete(self, arguments): # pylint: disable=no-self-use inst = MechInstance(instance) vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - if vmrun.deleteSnapshot(name) is None: + if vmrun.delete_snapshot(name) is None: puts_err(colored.red("Cannot delete name")) else: puts_err(colored.green("Snapshot {} deleted".format(name))) @@ -336,7 +336,7 @@ def list(self, arguments): inst = MechInstance(instance) vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) print('Snapshots for instance:{}'.format(instance)) - print(vmrun.listSnapshots()) + print(vmrun.list_snapshots()) # add alias for 'mech snapshot ls' ls = list @@ -635,11 +635,11 @@ def up(self, arguments): # pylint: disable=invalid-name time.sleep(3) puts_err(colored.blue("Getting IP address...")) lookup = inst.enable_ip_lookup - ip_address = vmrun.getGuestIPAddress(lookup=lookup) + ip_address = vmrun.get_guest_ip_address(lookup=lookup) if not disable_shared_folders: puts_err(colored.blue("Sharing current folder...")) - vmrun.enableSharedFolders(quiet=False) - vmrun.addSharedFolder('mech', utils.main_dir(), quiet=True) + vmrun.enable_shared_folders(quiet=False) + vmrun.add_shared_folder('mech', utils.main_dir(), quiet=True) if ip_address: if started: puts_err(colored.green("VM ({})" @@ -685,7 +685,7 @@ def ps(self, arguments): # pylint: disable=invalid-name,no-self-use instance = arguments[''] inst = MechInstance(instance) vmrun = VMrun(inst.vmx, inst.user, inst.password) - print(vmrun.listProcessesInGuest()) + print(vmrun.list_processes_in_guest()) # alias "mech process_status" to "mech ps" process_status = ps @@ -714,8 +714,8 @@ def status(self, arguments): vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) lookup = inst.enable_ip_lookup - ip_address = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=lookup) - state = vmrun.checkToolsState(quiet=True) + 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: @@ -765,7 +765,7 @@ def destroy(self, arguments): vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) vmrun.stop(mode='hard', quiet=True) time.sleep(3) - vmrun.deleteVM() + vmrun.delete_vm() if os.path.exists(inst.path): shutil.rmtree(inst.path) @@ -801,7 +801,7 @@ def down(self, arguments): inst = MechInstance(instance) vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - if not force and vmrun.installedTools(): + if not force and vmrun.installed_tools(): stopped = vmrun.stop() else: stopped = vmrun.stop(mode='hard') @@ -876,14 +876,14 @@ def resume(self, arguments): time.sleep(1) puts_err(colored.blue("Getting IP address...")) lookup = inst.enable_ip_lookup - ip_address = vmrun.getGuestIPAddress(lookup=lookup) + ip_address = vmrun.get_guest_ip_address(lookup=lookup) if not disable_shared_folders: puts_err(colored.blue("Sharing current folder...")) - vmrun.enableSharedFolders(quiet=False) - vmrun.addSharedFolder('mech', utils.main_dir(), quiet=True) + vmrun.enable_shared_folders(quiet=False) + vmrun.add_shared_folder('mech', utils.main_dir(), quiet=True) else: puts_err(colored.blue("Disabling shared folders...")) - vmrun.disableSharedFolders(quiet=False) + vmrun.disable_shared_folders(quiet=False) if ip_address: puts_err(colored.green("VM resumed on {}".format(ip_address))) else: @@ -899,11 +899,11 @@ def resume(self, arguments): time.sleep(3) puts_err(colored.blue("Getting IP address...")) lookup = inst.enable_ip_lookup - ip_address = vmrun.getGuestIPAddress(lookup=lookup) + ip_address = vmrun.get_guest_ip_address(lookup=lookup) if not disable_shared_folders: puts_err(colored.blue("Sharing current folder...")) - vmrun.enableSharedFolders(quiet=False) - vmrun.addSharedFolder('mech', utils.main_dir(), quiet=True) + vmrun.enable_shared_folders(quiet=False) + vmrun.add_shared_folder('mech', utils.main_dir(), quiet=True) if ip_address: if started: puts_err(colored.green("VM ({}) started on " @@ -1087,7 +1087,7 @@ def ip(self, arguments): # pylint: disable=invalid-name,no-self-use if inst.created: vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) lookup = inst.enable_ip_lookup - ip_address = vmrun.getGuestIPAddress(lookup=lookup) + ip_address = vmrun.get_guest_ip_address(lookup=lookup) if ip_address: puts_err(colored.green(ip_address)) else: @@ -1153,7 +1153,7 @@ def reload(self, arguments): time.sleep(3) puts_err(colored.blue("Getting IP address...")) lookup = inst.enable_ip_lookup - ip_address = vmrun.getGuestIPAddress(lookup=lookup) + ip_address = vmrun.get_guest_ip_address(lookup=lookup) if ip_address: if started: puts_err(colored.green("VM ({}) started " @@ -1196,10 +1196,10 @@ def port(self, arguments): print('Instance ({}):'. format(instance)) nat_found = False vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - for line in vmrun.listHostNetworks().split('\n'): + for line in vmrun.list_host_networks().split('\n'): network = line.split() if len(network) > 2 and network[2] == 'nat': - print(vmrun.listPortForwardings(network[1])) + 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) @@ -1235,7 +1235,7 @@ def list(self, arguments): if inst.created: vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) lookup = inst.enable_ip_lookup - ip_address = vmrun.getGuestIPAddress(wait=False, quiet=True, lookup=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: diff --git a/mech/test_mech.py b/mech/test_mech.py index 22fc90e..ed07888 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -91,8 +91,8 @@ def test_mech_list_with_two(mock_locate, mock_load_mechfile, capfd): 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.listPortForwardings', return_value='Total port forwardings: 0') -@patch('mech.vmrun.VMrun.listHostNetworks', return_value=HOST_NETWORKS) +@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', return_value=MECHFILE_ONE_ENTRY) @patch('mech.utils.locate', return_value=None) def test_mech_port_with_nat(mock_locate, mock_load_mechfile, mock_list_host_networks, @@ -116,8 +116,8 @@ def test_mech_port_with_nat(mock_locate, mock_load_mechfile, mock_list_host_netw 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.listPortForwardings', return_value='Total port forwardings: 0') -@patch('mech.vmrun.VMrun.listHostNetworks', return_value=HOST_NETWORKS) +@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', return_value=MECHFILE_TWO_ENTRIES) @patch('mech.utils.locate', return_value=None) def test_mech_port_with_nat_two_hosts(mock_locate, mock_load_mechfile, mock_list_host_networks, @@ -140,7 +140,7 @@ def test_mech_port_with_nat_two_hosts(mock_locate, mock_load_mechfile, mock_list 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.listHostNetworks', return_value=HOST_NETWORKS_WITHOUT_NAT) +@patch('mech.vmrun.VMrun.list_host_networks', return_value=HOST_NETWORKS_WITHOUT_NAT) @patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) @patch('mech.utils.locate', return_value=None) def test_mech_port_without_nat(mock_locate, mock_load_mechfile, mock_list_host_networks, capfd): diff --git a/mech/utils.py b/mech/utils.py index 3fc0a53..29d1c28 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -606,7 +606,7 @@ def provision(instance, vmx, user, password, provision_config, show): vmrun = VMrun(vmx, user, password) # cannot run provisioning if vmware tools are not installed - if not vmrun.installedTools(): + if not vmrun.installed_tools(): puts_err(colored.red("Cannot provision if VMware Tools are not installed.")) return @@ -659,14 +659,14 @@ def provision_file(virtual_machine, source, destination): This simply copies a file from host to guest. """ puts_err(colored.blue("Copying ({}) to ({})".format(source, destination))) - return virtual_machine.copyFileFromHostToGuest(source, destination) + return virtual_machine.copy_file_from_host_to_guest(source, destination) def provision_shell(virtual_machine, inline, path, args=None): """Provision from shell.""" if args is None: args = [] - tmp_path = virtual_machine.createTempfileInGuest() + tmp_path = virtual_machine.create_tempfile_in_guest() LOGGER.debug('inline:%s path:%s args:%s tmp_path:%s', inline, path, args, tmp_path) if tmp_path is None: return @@ -674,7 +674,7 @@ def provision_shell(virtual_machine, inline, path, args=None): try: if path and os.path.isfile(path): puts_err(colored.blue("Configuring script {}...".format(path))) - if virtual_machine.copyFileFromHostToGuest(path, tmp_path) is None: + if virtual_machine.copy_file_from_host_to_guest(path, tmp_path) is None: return else: if path: @@ -701,20 +701,20 @@ def provision_shell(virtual_machine, inline, path, args=None): try: the_file.write(str.encode(inline)) the_file.close() - if virtual_machine.copyFileFromHostToGuest(the_file.name, tmp_path) is None: + if virtual_machine.copy_file_from_host_to_guest(the_file.name, tmp_path) is None: return finally: os.unlink(the_file.name) puts_err(colored.blue("Configuring environment...")) - if virtual_machine.runScriptInGuest('/bin/sh', "chmod +x '{}'".format(tmp_path)) is None: + if virtual_machine.run_script_in_guest('/bin/sh', "chmod +x '{}'".format(tmp_path)) is None: return puts_err(colored.blue("Executing program...")) - return virtual_machine.runProgramInGuest(tmp_path, args) + return virtual_machine.run_program_in_guest(tmp_path, args) finally: - virtual_machine.deleteFileInGuest(tmp_path, quiet=True) + virtual_machine.delete_file_in_guest(tmp_path, quiet=True) def config_ssh_string(config_ssh): diff --git a/mech/vmrun.py b/mech/vmrun.py index b160d4f..f8b4e44 100644 --- a/mech/vmrun.py +++ b/mech/vmrun.py @@ -20,6 +20,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # +"""mech code to interface with VMware vmrun command line utility.""" from __future__ import absolute_import @@ -31,7 +32,7 @@ from .compat import PY3, b2s -logger = logging.getLogger(__name__) +LOGGER = logging.getLogger(__name__) def get_fallback_executable(): @@ -102,14 +103,15 @@ def get_provider(vmrun_exe): except OSError: pass - stdoutdata, stderrdata = map(b2s, proc.communicate()) + map(b2s, proc.communicate()) if proc.returncode == 0: return provider -class VMrun(object): +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'. """ if sys.platform == 'darwin': default_executable = get_darwin_executable() @@ -119,7 +121,8 @@ class VMrun(object): default_executable = get_fallback_executable() default_provider = get_provider(default_executable) - 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): """Constructof for the instance. Set some sane defaults.""" self.vmx_file = vmx_file self.user = user @@ -145,7 +148,7 @@ def vmrun(self, cmd, *args, **kwargs): cmds.extend(filter(None, args)) cmds.extend(filter(None, arguments)) - logger.debug( + LOGGER.debug( " ".join( "'{}'".format( c.replace( @@ -164,16 +167,16 @@ def vmrun(self, cmd, *args, **kwargs): 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 @@ -236,7 +239,7 @@ 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', @@ -248,7 +251,7 @@ 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', @@ -257,7 +260,7 @@ def deleteSnapshot(self, snap_name, and_delete_children=False, quiet=False): 'andDeleteChildren' if and_delete_children else None, quiet=quiet) - def revertToSnapshot(self, snap_name, quiet=False): + 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) @@ -281,11 +284,11 @@ 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', @@ -294,7 +297,7 @@ def addNetworkAdapter(self, adapter_type, host_network=None, quiet=False): host_network, quiet=quiet) - def setNetworkAdapter(self, adapter_index, adapter_type, host_network=None, quiet=False): + 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', @@ -304,7 +307,7 @@ def setNetworkAdapter(self, adapter_index, adapter_type, host_network=None, quie host_network, quiet=quiet) - def deleteNetworkAdapter(self, adapter_index, quiet=False): + 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) @@ -329,15 +332,15 @@ 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( + def set_port_forwarding( # pylint: disable=too-many-arguments self, host_network, protocol, @@ -357,7 +360,8 @@ def setPortForwarding( description, quiet=quiet) - def deletePortForwarding(self, host_network, protocol, host_port, quiet=False): + 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) @@ -458,14 +462,17 @@ def deletePortForwarding(self, host_network, protocol, host_port, quiet=False): # [-wait] # - def runProgramInGuest( + def run_program_in_guest( # pylint: disable=too-many-arguments self, program_path, - program_arguments=[], + 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, @@ -476,15 +483,7 @@ def runProgramInGuest( 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 set_shared_folder_state(self, share_name, new_path, mode='readonly', quiet=False): '''Modify a Host-Guest shared folder''' return self.vmrun( 'setSharedFolderState', @@ -494,30 +493,31 @@ def setSharedFolderState(self, share_name, new_path, mode='readonly', quiet=Fals mode, quiet=quiet) - def addSharedFolder(self, share_name, host_path, quiet=False): + 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( + def run_script_in_guest( # pylint: disable=too-many-arguments self, interpreter_path, script, @@ -536,27 +536,27 @@ def runScriptInGuest( '-interactive' if interactive else None, quiet=quiet) - def deleteFileInGuest(self, file, quiet=False): + def delete_file_in_guest(self, file, quiet=False): '''Delete a file in Guest OS''' return self.vmrun('deleteFileInGuest', self.vmx_file, file, 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', @@ -565,7 +565,7 @@ def copyFileFromHostToGuest(self, host_path, guest_path, quiet=False): guest_path, quiet=quiet) - def copyFileFromGuestToHost(self, guest_path, host_path, quiet=False): + 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', @@ -574,57 +574,58 @@ def copyFileFromGuestToHost(self, guest_path, host_path, quiet=False): host_path, quiet=quiet) - def renameFileInGuest(self, original_name, new_name, quiet=False): + 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( + self.run_script_in_guest( '/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) + 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('/tmp/ip_address', temp_file.name, quiet=quiet) + ip_addresses = open(temp_file.name).read().split() 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) + 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 @@ -653,11 +654,11 @@ def upgradevm(self, quiet=False): '''Upgrade VM file format, virtual hw''' 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) @@ -671,12 +672,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) @@ -697,22 +698,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) @@ -734,38 +735,39 @@ 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 if VMware tools are either 'installed' or 'running'.''' + state = self.check_tools_state(quiet=quiet) return state in ('installed', 'running') From bf908a48d6a7d0ad7bedff5e775d576985176ab3 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 9 Feb 2020 11:18:09 -0800 Subject: [PATCH 047/116] add mech snapshot list/save unittest --- mech/mech.py | 7 +- mech/test_mech_box.py | 2 +- mech/test_mech_snapshot.py | 128 +++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 mech/test_mech_snapshot.py diff --git a/mech/mech.py b/mech/mech.py index b2bac15..1fcd590 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -334,9 +334,12 @@ def list(self, arguments): for instance in instances: inst = MechInstance(instance) - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) print('Snapshots for instance:{}'.format(instance)) - print(vmrun.list_snapshots()) + if inst.created: + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + print(vmrun.list_snapshots()) + else: + print('Instance ({}) is not created.'.format(instance)) # add alias for 'mech snapshot ls' ls = list diff --git a/mech/test_mech_box.py b/mech/test_mech_box.py index fc0e9fe..e91b754 100644 --- a/mech/test_mech_box.py +++ b/mech/test_mech_box.py @@ -26,7 +26,7 @@ def test_mech_box_list_no_mechdir(mock_os_getcwd, capfd): @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' driectory.""" + """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) diff --git a/mech/test_mech_snapshot.py b/mech/test_mech_snapshot.py new file mode 100644 index 0000000..5561288 --- /dev/null +++ b/mech/test_mech_snapshot.py @@ -0,0 +1,128 @@ +"""Unit tests for 'mech snapshot'.""" +import re + +from unittest.mock import patch + +import mech.command +import mech.mech +import mech.vmrun + + +MECHFILE_TWO_ENTRIES = { + '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' + } +} +@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@patch('os.getcwd') +def test_mech_snapshot_list_no_mechdir(mock_os_getcwd, mock_load_mechfile, capfd): + """Test 'mech snapshot list' with no '.mech' directory.""" + 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', return_value=MECHFILE_TWO_ENTRIES) +@patch('os.getcwd') +def test_mech_snapshot_list_no_snapshots(mock_os_getcwd, mock_load_mechfile, + mock_list_snapshots, capfd): + """Test 'mech snapshot list' without any snapshots.""" + 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 +foo +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', return_value=MECHFILE_TWO_ENTRIES) +@patch('os.getcwd') +def test_mech_snapshot_list_with_snapshot(mock_os_getcwd, mock_load_mechfile, + mock_list_snapshots, capfd): + """Test 'mech snapshot list' with a snapshots.""" + 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) From 821cb33d5489873df903a9eca32c157c452f7a3b Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 9 Feb 2020 12:13:14 -0800 Subject: [PATCH 048/116] remove puts_err calls; added mech snapshot delete unittest --- mech/mech.py | 146 ++++++++++++++++++------------------- mech/test_mech_snapshot.py | 50 ++++++++++++- mech/utils.py | 92 +++++++++++------------ 3 files changed, 168 insertions(+), 120 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 1fcd590..27fd363 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -36,7 +36,7 @@ import shutil import subprocess -from clint.textui import colored, puts_err +from clint.textui import colored from . import utils from .vmrun import VMrun @@ -89,7 +89,7 @@ def __init__(self, name, mechfile=None): if mechfile.get(name, None): self.name = name else: - puts_err(colored.red("Instance ({}) was not found in the Mechfile".format(name))) + print(colored.red("Instance ({}) was not found in the Mechfile".format(name))) sys.exit(1) self.box = mechfile[name].get('box', None) self.box_version = mechfile[name].get('box_version', None) @@ -133,7 +133,7 @@ def config_ssh(self): ip_address = vmrun.get_guest_ip_address(wait=False, lookup=lookup) if vmrun.installed_tools() else None if not ip_address: - puts_err(colored.red(textwrap.fill( + print(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 " @@ -307,9 +307,9 @@ def delete(self, arguments): # pylint: disable=no-self-use vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) if vmrun.delete_snapshot(name) is None: - puts_err(colored.red("Cannot delete name")) + print(colored.red("Cannot delete name")) else: - puts_err(colored.green("Snapshot {} deleted".format(name))) + print(colored.green("Snapshot {} deleted".format(name))) # add alias for 'mech snapshot remove' remove = delete @@ -365,10 +365,10 @@ def save(self, arguments): # pylint: disable=no-self-use inst = MechInstance(instance) vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) if vmrun.snapshot(name) is None: - puts_err(colored.red("Warning: Could not take snapshot.")) + print(colored.red("Warning: Could not take snapshot.")) sys.exit(1) else: - puts_err(colored.green("Snapshot ({}) on VM ({}) taken".format(name, instance))) + print(colored.green("Snapshot ({}) on VM ({}) taken".format(name, instance))) class Mech(MechCommand): @@ -467,25 +467,25 @@ def init(self, arguments): # pylint: disable=no-self-use 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( + print(colored.red(textwrap.fill( "`Mechfile` already exists in this directory. Remove it " "before running `mech init`." ))) sys.exit(1) - puts_err(colored.green("Initializing mech")) + print(colored.green("Initializing mech")) if utils.init_mechfile( location=location, box=box, name=name, box_version=box_version, requests_kwargs=requests_kwargs): - puts_err(colored.green(textwrap.fill( + 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!" ))) else: - puts_err(colored.red("Couldn't initialize mech")) + print(colored.red("Couldn't initialize mech")) def add(self, arguments): # pylint: disable=no-self-use """ @@ -512,14 +512,14 @@ def add(self, arguments): # pylint: disable=no-self-use location = arguments[''] if not name or name == "": - puts_err(colored.red("Need to provide a name for the instance to add to the Mechfile.")) + print(colored.red("Need to provide a name for the instance to add to the Mechfile.")) sys.exit(1) requests_kwargs = utils.get_requests_kwargs(arguments) LOGGER.debug('name:%s box:%s box_version:%s location:%s', name, box, box_version, location) - puts_err(colored.green("Adding ({}) to the Mechfile.".format(name))) + print(colored.green("Adding ({}) to the Mechfile.".format(name))) if utils.add_to_mechfile( location=location, @@ -527,9 +527,9 @@ def add(self, arguments): # pylint: disable=no-self-use name=name, box_version=box_version, requests_kwargs=requests_kwargs): - puts_err(colored.green("Added to the Mechfile.")) + print(colored.green("Added to the Mechfile.")) else: - puts_err(colored.red("Could not add {} to the Mechfile".format(name))) + print(colored.red("Could not add {} to the Mechfile".format(name))) def remove(self, arguments): """ @@ -543,7 +543,7 @@ def remove(self, arguments): name = arguments[''] if not name or name == "": - puts_err(colored.red("Need to provide a name to be removed from the Mechfile.")) + print(colored.red("Need to provide a name to be removed from the Mechfile.")) sys.exit(1) LOGGER.debug('name:%s', name) @@ -551,13 +551,13 @@ def remove(self, arguments): self.activate_mechfile() inst = self.mechfile.get(name, None) if inst: - puts_err(colored.green("Removing ({}) from the Mechfile.".format(name))) + print(colored.green("Removing ({}) from the Mechfile.".format(name))) if utils.remove_mechfile_entry(name=name): - puts_err(colored.green("Removed from the Mechfile.")) + print(colored.green("Removed from the Mechfile.")) else: - puts_err(colored.red("Could not remove {} from the Mechfile".format(name))) + print(colored.red("Could not remove {} from the Mechfile".format(name))) else: - puts_err(colored.red("There is no instance called ({}) in the Mechfile.".format(name))) + print(colored.red("There is no instance called ({}) in the Mechfile.".format(name))) sys.exit(1) # add alias for 'mech delete' @@ -630,33 +630,33 @@ def up(self, arguments): # pylint: disable=invalid-name inst.created = True vmrun = VMrun(vmx, user=inst.user, password=inst.password) - puts_err(colored.blue("Bringing machine ({}) up...".format(instance))) + print(colored.blue("Bringing machine ({}) up...".format(instance))) started = vmrun.start(gui=gui) if started is None: - puts_err(colored.red("VM not started")) + print(colored.red("VM not started")) else: time.sleep(3) - puts_err(colored.blue("Getting 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: - puts_err(colored.blue("Sharing current folder...")) + print(colored.blue("Sharing current folder...")) vmrun.enable_shared_folders(quiet=False) vmrun.add_shared_folder('mech', utils.main_dir(), quiet=True) if ip_address: if started: - puts_err(colored.green("VM ({})" - "started on {}".format(instance, ip_address))) + print(colored.green("VM ({})" + "started on {}".format(instance, ip_address))) else: - puts_err(colored.yellow("VM ({}) was already started " - "on {}".format(instance, ip_address))) + print(colored.yellow("VM ({}) was already started " + "on {}".format(instance, ip_address))) else: if started: - puts_err(colored.green("VM ({}) started on an unknown " - "IP address".format(instance))) + print(colored.green("VM ({}) started on an unknown " + "IP address".format(instance))) else: - puts_err(colored.yellow("VM ({}) was already started on an " - "unknown IP address".format(instance))) + print(colored.yellow("VM ({}) was already started on an " + "unknown IP address".format(instance))) if not disable_provisioning: utils.provision(instance, inst.vmx, inst.user, inst.password, inst.provision, show=False) @@ -764,7 +764,7 @@ def destroy(self, arguments): 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'): - puts_err(colored.green("Deleting ({})...".format(instance))) + print(colored.green("Deleting ({})...".format(instance))) vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) vmrun.stop(mode='hard', quiet=True) time.sleep(3) @@ -775,9 +775,9 @@ def destroy(self, arguments): else: LOGGER.debug("%s was not found.", inst.path) else: - puts_err(colored.red("Deletion aborted")) + print(colored.red("Deletion aborted")) else: - puts_err(colored.red("The box ({}) hasn't been initialized.".format(instance))) + print(colored.red("The box ({}) hasn't been initialized.".format(instance))) def down(self, arguments): """ @@ -809,9 +809,9 @@ def down(self, arguments): else: stopped = vmrun.stop(mode='hard') if stopped is None: - puts_err(colored.red("Not stopped", vmrun)) + print(colored.red("Not stopped", vmrun)) else: - puts_err(colored.green("Stopped", vmrun)) + print(colored.green("Stopped", vmrun)) # alias 'mech stop' and 'mech halt' to 'mech down' stop = down @@ -839,9 +839,9 @@ def pause(self, arguments): inst = MechInstance(instance) vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) if vmrun.pause() is None: - puts_err(colored.red("Not paused", vmrun)) + print(colored.red("Not paused", vmrun)) else: - puts_err(colored.yellow("Paused", vmrun)) + print(colored.yellow("Paused", vmrun)) def resume(self, arguments): """ @@ -877,52 +877,52 @@ def resume(self, arguments): if vmrun.unpause(quiet=True) is not None: time.sleep(1) - puts_err(colored.blue("Getting 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: - puts_err(colored.blue("Sharing current folder...")) + print(colored.blue("Sharing current folder...")) vmrun.enable_shared_folders(quiet=False) vmrun.add_shared_folder('mech', utils.main_dir(), quiet=True) else: - puts_err(colored.blue("Disabling shared folders...")) + print(colored.blue("Disabling shared folders...")) vmrun.disable_shared_folders(quiet=False) if ip_address: - puts_err(colored.green("VM resumed on {}".format(ip_address))) + print(colored.green("VM resumed on {}".format(ip_address))) else: - puts_err(colored.green("VM resumed on an unknown IP address")) + print(colored.green("VM resumed on an unknown IP address")) else: # Otherwise try starting vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) started = vmrun.start() if started is None: - puts_err(colored.red("VM not started")) + print(colored.red("VM not started")) else: time.sleep(3) - puts_err(colored.blue("Getting 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: - puts_err(colored.blue("Sharing current folder...")) + print(colored.blue("Sharing current folder...")) vmrun.enable_shared_folders(quiet=False) vmrun.add_shared_folder('mech', utils.main_dir(), quiet=True) if ip_address: if started: - puts_err(colored.green("VM ({}) started on " - "{}".format(instance, ip_address))) + print(colored.green("VM ({}) started on " + "{}".format(instance, ip_address))) else: - puts_err(colored.yellow("VM ({}) already was started " - "on {}".format(instance, ip_address))) + print(colored.yellow("VM ({}) already was started " + "on {}".format(instance, ip_address))) else: if started: - puts_err(colored.green("VM ({}) started on an unknown " - "IP address".format(instance))) + print(colored.green("VM ({}) started on an unknown " + "IP address".format(instance))) else: - puts_err(colored.yellow("VM ({}) already was started on an " - "unknown IP address".format(instance))) + print(colored.yellow("VM ({}) already was started on an " + "unknown IP address".format(instance))) else: - puts_err(colored.red("Need to start VM first")) + print(colored.red("Need to start VM first")) def suspend(self, arguments): """ @@ -946,9 +946,9 @@ def suspend(self, arguments): inst = MechInstance(instance) vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) if vmrun.suspend() is None: - puts_err(colored.red("Not suspended", vmrun)) + print(colored.red("Not suspended", vmrun)) else: - puts_err(colored.green("Suspended", vmrun)) + print(colored.green("Suspended", vmrun)) def ssh_config(self, arguments): """ @@ -1034,7 +1034,7 @@ def scp(self, arguments): # pylint: disable=no-self-use src_instance, src_is_host, src = src.partition(':') if dst_is_host and src_is_host: - puts_err(colored.red("Both src and host are host destinations")) + print(colored.red("Both src and host are host destinations")) sys.exit(1) if dst_is_host: instance = dst_instance @@ -1092,11 +1092,11 @@ def ip(self, arguments): # pylint: disable=invalid-name,no-self-use lookup = inst.enable_ip_lookup ip_address = vmrun.get_guest_ip_address(lookup=lookup) if ip_address: - puts_err(colored.green(ip_address)) + print(colored.green(ip_address)) else: - puts_err(colored.red("Unknown IP address")) + print(colored.red("Unknown IP address")) else: - puts_err(colored.yellow("VM not created")) + print(colored.yellow("VM not created")) # alias 'mech ip_address' to 'mech ip' ip_address = ip @@ -1148,29 +1148,29 @@ def reload(self, arguments): vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - puts_err(colored.blue("Reloading machine...")) + print(colored.blue("Reloading machine...")) started = vmrun.reset() if started is None: - puts_err(colored.red("VM not restarted")) + print(colored.red("VM not restarted")) else: time.sleep(3) - puts_err(colored.blue("Getting IP address...")) + 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: - puts_err(colored.green("VM ({}) started " - "on {}".format(instance, ip_address))) + print(colored.green("VM ({}) started " + "on {}".format(instance, ip_address))) else: - puts_err(colored.yellow("VM ({}) already was started on " - "{}".format(instance, ip_address))) + print(colored.yellow("VM ({}) already was started on " + "{}".format(instance, ip_address))) else: if started: - puts_err(colored.green("VM ({}) started on an unknown IP " - "address".format(instance))) + print(colored.green("VM ({}) started on an unknown IP " + "address".format(instance))) else: - puts_err(colored.yellow("VM ({}) already was started " - "on an unknown IP address".format(instance))) + print(colored.yellow("VM ({}) already was started " + "on an unknown IP address".format(instance))) def port(self, arguments): """ diff --git a/mech/test_mech_snapshot.py b/mech/test_mech_snapshot.py index 5561288..3b97fd3 100644 --- a/mech/test_mech_snapshot.py +++ b/mech/test_mech_snapshot.py @@ -91,7 +91,7 @@ def test_mech_snapshot_list_no_snapshots(mock_os_getcwd, mock_load_mechfile, SNAPSHOT_LIST_WITH_SNAPSHOT = """Snapshots for instance:first Total snapshots: 1 -foo +snap1 Snapshots for instance:second Instance (second) is not created.""" @patch('mech.vmrun.VMrun.list_snapshots', return_value=SNAPSHOT_LIST_WITH_SNAPSHOT) @@ -126,3 +126,51 @@ def test_mech_snapshot_list_with_snapshot(mock_os_getcwd, mock_load_mechfile, 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', return_value=MECHFILE_TWO_ENTRIES) +@patch('os.getcwd') +def test_mech_snapshot_delete_snapshot(mock_os_getcwd, mock_load_mechfile, + mock_list_snapshots, mock_delete_snapshot, capfd): + """Test 'mech snapshot delete'.""" + 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, err = 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, err = 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) diff --git a/mech/utils.py b/mech/utils.py index 29d1c28..718f343 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -40,7 +40,7 @@ from shutil import copyfile import requests -from clint.textui import colored, puts_err +from clint.textui import colored from clint.textui import progress from .compat import raw_input, b2s @@ -170,7 +170,7 @@ def update_vmx(path, numvcpus=None, memsize=None): 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 @@ -201,10 +201,10 @@ def load_mechfile(should_exist=True): LOGGER.debug('mechfile:%s', mechfile) return mechfile except ValueError: - puts_err(colored.red("Invalid Mechfile." + os.linesep)) + print(colored.red("Invalid Mechfile." + os.linesep)) else: if should_exist: - puts_err(colored.red(textwrap.fill( + print(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 " @@ -253,10 +253,10 @@ def build_mechfile_entry(location, box=None, name=None, box_version=None, reques try: account, box, ver = (location.split('/', 2) + ['', ''])[:3] if not account or not box: - puts_err(colored.red("Provided box name is not valid")) + print(colored.red("Provided box name is not valid")) if ver: box_version = ver - puts_err( + 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) @@ -264,10 +264,10 @@ def build_mechfile_entry(location, box=None, name=None, box_version=None, reques 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)) + print(colored.red("Bad response from HashiCorp's Vagrant Cloud API: %s" % exc)) sys.exit(1) except requests.ConnectionError: - puts_err(colored.red("Couldn't connect to HashiCorp's Vagrant Cloud API")) + print(colored.red("Couldn't connect to HashiCorp's Vagrant Cloud API")) sys.exit(1) 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) @@ -288,7 +288,7 @@ def catalog_to_mechfile(catalog, name=None, box=None, box_version=None): mechfile['box_version'] = current_version mechfile['url'] = provider['url'] return mechfile - puts_err( + print( colored.red( "Couldn't find a VMWare compatible VM for '{}'{}".format( name, " ({})".format(box_version) if box_version else ""))) @@ -340,7 +340,7 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= save=save, requests_kwargs=requests_kwargs) if not name_version_box: - puts_err(colored.red("Cannot find a valid box with a VMX file in it")) + print(colored.red("Cannot find a valid box with a VMX file in it")) sys.exit(1) box_parts = box.split('/') @@ -348,7 +348,7 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= box_parts[0], box_parts[1], box_version))) box_file = locate(box_dir, '*.box') - puts_err(colored.blue("Extracting box '{}'...".format(box_file))) + print(colored.blue("Extracting box '{}'...".format(box_file))) makedirs(instance_path) if sys.platform == 'win32': cmd = tar_cmd('-xf', box_file, force_local=True) @@ -361,7 +361,7 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= startupinfo.dwFlags |= subprocess.SW_HIDE | subprocess.STARTF_USESHOWWINDOW proc = subprocess.Popen(cmd, cwd=instance_path, startupinfo=startupinfo) if proc.wait(): - puts_err(colored.red("Cannot extract box")) + print(colored.red("Cannot extract box")) sys.exit(1) else: tar = tarfile.open(box_file, 'r') @@ -372,7 +372,7 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= vmx = locate(instance_path, '*.vmx') if not vmx: - puts_err(colored.red("Cannot locate a VMX file")) + print(colored.red("Cannot locate a VMX file")) sys.exit(1) update_vmx(vmx, numvcpus=numvcpus, memsize=memsize) @@ -428,7 +428,7 @@ def add_mechfile(mechfile_entry, name=None, box=None, box_version=None, return add_box_url(name=name, box=box, box_version=box_version, url=url, force=force, save=save, requests_kwargs=requests_kwargs) - puts_err( + print( colored.red( "Could not find a VMWare compatible VM for '{}'{}".format( name, " ({})".format(box_version) if box_version else ""))) @@ -446,12 +446,12 @@ def add_box_url(name, box, box_version, url, force=False, save=True, requests_kw exists = os.path.exists(box_dir) if not exists or force: if exists: - puts_err(colored.blue("Attempting to download box '{}'...".format(box))) + print(colored.blue("Attempting to download box '{}'...".format(box))) else: - puts_err(colored.blue("Box '{}' could not be found. " - "Attempting to download...".format(box))) + print(colored.blue("Box '{}' could not be found. " + "Attempting to download...".format(box))) try: - puts_err(colored.blue("URL: {}".format(url))) + print(colored.blue("URL: {}".format(url))) response = requests.get(url, stream=True, **requests_kwargs) response.raise_for_status() try: @@ -490,17 +490,17 @@ def add_box_url(name, box, box_version, url, force=False, save=True, requests_kw finally: os.unlink(the_file.name) except requests.HTTPError as exc: - puts_err(colored.red("Bad response: %s" % exc)) + print(colored.red("Bad response: %s" % exc)) sys.exit(1) except requests.ConnectionError: - puts_err(colored.red("Couldn't connect to '%s'" % url)) + print(colored.red("Couldn't connect to '%s'" % url)) sys.exit(1) return name, box_version, box 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.""" - puts_err(colored.blue("Checking box '{}' integrity filename:{}...".format(box, filename))) + 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) @@ -522,7 +522,7 @@ def add_box_file(box=None, box_version=None, filename=None, url=None, force=Fals valid_tar = True break if i.startswith('/') or i.startswith('..'): - puts_err(colored.red(textwrap.fill( + print(colored.red(textwrap.fill( "This box is comprised of filenames starting with '/' or '..' " "Exiting for the safety of your files." ))) @@ -595,19 +595,19 @@ def provision(instance, vmx, user, password, provision_config, show): """ if instance == '': - puts_err(colored.red("Need to provide an instance to provision().")) + print(colored.red("Need to provide an instance to provision().")) return if not vmx or not user or not password: - puts_err(colored.red("Need provide vmx/user/password to provision().")) + print(colored.red("Need provide vmx/user/password to provision().")) return - puts_err(colored.green('Provisioning instance:{}'.format(instance))) + print(colored.green('Provisioning instance:{}'.format(instance))) vmrun = VMrun(vmx, user, password) # cannot run provisioning if vmware tools are not installed if not vmrun.installed_tools(): - puts_err(colored.red("Cannot provision if VMware Tools are not installed.")) + print(colored.red("Cannot provision if VMware Tools are not installed.")) return provisioned = 0 @@ -618,12 +618,12 @@ def provision(instance, vmx, user, password, provision_config, show): source = pro.get('source') destination = pro.get('destination') if show: - puts_err(colored.green(" instance:{} provision_type:{} source:{} " - "destination:{}".format(instance, provision_type, - source, destination))) + print(colored.green(" instance:{} provision_type:{} source:{} " + "destination:{}".format(instance, provision_type, + source, destination))) else: if provision_file(vmrun, source, destination) is None: - puts_err(colored.red("Not Provisioned")) + print(colored.red("Not Provisioned")) return provisioned += 1 @@ -635,30 +635,30 @@ def provision(instance, vmx, user, password, provision_config, show): if not isinstance(args, list): args = [args] if show: - puts_err(colored.green(" instance:{} provision_type:{} inline:{} path:{} " - "args:{}".format(instance, provision_type, - inline, path, args))) + print(colored.green(" instance:{} provision_type:{} inline:{} path:{} " + "args:{}".format(instance, provision_type, + inline, path, args))) else: if provision_shell(vmrun, inline, path, args) is None: - puts_err(colored.red("Not Provisioned")) + print(colored.red("Not Provisioned")) return provisioned += 1 else: - puts_err(colored.red("Not Provisioned ({}".format(i))) + print(colored.red("Not Provisioned ({}".format(i))) return else: - puts_err(colored.green("VM ({}) Provision {} " - "entries".format(instance, provisioned))) + print(colored.green("VM ({}) Provision {} " + "entries".format(instance, provisioned))) else: - puts_err(colored.blue("Nothing to provision")) + print(colored.blue("Nothing to provision")) def provision_file(virtual_machine, source, destination): """Provision from file. This simply copies a file from host to guest. """ - puts_err(colored.blue("Copying ({}) to ({})".format(source, destination))) + print(colored.blue("Copying ({}) to ({})".format(source, destination))) return virtual_machine.copy_file_from_host_to_guest(source, destination) @@ -673,13 +673,13 @@ def provision_shell(virtual_machine, inline, path, args=None): try: if path and os.path.isfile(path): - puts_err(colored.blue("Configuring script {}...".format(path))) + print(colored.blue("Configuring script {}...".format(path))) if virtual_machine.copy_file_from_host_to_guest(path, tmp_path) is None: return else: if path: if any(path.startswith(s) for s in ('https://', 'http://', 'ftp://')): - puts_err(colored.blue("Downloading {}...".format(path))) + print(colored.blue("Downloading {}...".format(path))) try: response = requests.get(path) response.raise_for_status() @@ -689,14 +689,14 @@ def provision_shell(virtual_machine, inline, path, args=None): except requests.ConnectionError: return else: - puts_err(colored.red("Cannot open {}".format(path))) + print(colored.red("Cannot open {}".format(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 to run inline...")) + print(colored.blue("Configuring script to run inline...")) the_file = tempfile.NamedTemporaryFile(delete=False) try: the_file.write(str.encode(inline)) @@ -706,11 +706,11 @@ def provision_shell(virtual_machine, inline, path, args=None): finally: os.unlink(the_file.name) - puts_err(colored.blue("Configuring environment...")) + print(colored.blue("Configuring environment...")) if virtual_machine.run_script_in_guest('/bin/sh', "chmod +x '{}'".format(tmp_path)) is None: return - puts_err(colored.blue("Executing program...")) + print(colored.blue("Executing program...")) return virtual_machine.run_program_in_guest(tmp_path, args) finally: From 01e45555d422337010ed4104a757794ac276c01e Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 9 Feb 2020 15:48:12 -0800 Subject: [PATCH 049/116] add some mech up and mech ssh-config unit tests --- CONTRIBUTING.md | 3 + mech/mech.py | 21 +++--- mech/test_mech.py | 137 ++++++++++++++++++++++++++++++++++++- mech/test_mech_snapshot.py | 2 +- 4 files changed, 151 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba231b6..267b65c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,6 +43,9 @@ pytest # for code coverage pytest --cov mech +# to see what lines are not covered +pytest --cov-report term-missing --cov mech + # for testing/validation, we have also some integration tests cd tests/int ./simple.bats diff --git a/mech/mech.py b/mech/mech.py index 27fd363..b2119c0 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -89,8 +89,8 @@ def __init__(self, name, mechfile=None): if mechfile.get(name, None): self.name = name else: - print(colored.red("Instance ({}) was not found in the Mechfile".format(name))) - sys.exit(1) + 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) @@ -133,13 +133,11 @@ def config_ssh(self): ip_address = vmrun.get_guest_ip_address(wait=False, lookup=lookup) if vmrun.installed_tools() else None if not ip_address: - print(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) + 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."))) insecure_private_key = os.path.abspath(os.path.join( utils.mech_dir(), "insecure_private_key")) @@ -970,7 +968,10 @@ def ssh_config(self, arguments): for instance in instances: inst = MechInstance(instance) - print(utils.config_ssh_string(inst.config_ssh())) + 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 """ diff --git a/mech/test_mech.py b/mech/test_mech.py index ed07888..7159205 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -2,7 +2,8 @@ import subprocess import re -from unittest.mock import patch +from unittest.mock import patch, mock_open +from pytest import raises import mech.command import mech.mech @@ -86,6 +87,140 @@ def test_mech_list_with_two(mock_locate, mock_load_mechfile, capfd): assert re.search(r'second\s+notcreated', out, re.MULTILINE) +MECHFILE_BAD_ENTRY = { + '': { + 'name': + '', + 'box': + 'bento/ubuntu-18.04', + 'box_version': + '201912.04.0' + } +} +@patch('mech.utils.load_mechfile', return_value=MECHFILE_BAD_ENTRY) +@patch('mech.utils.locate', return_value=None) +def test_mech_up_without_name(mock_locate, mock_load_mechfile): + """Test 'mech up' (overriding name to be '') to test exception.""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--detail': False, + '--gui': False, + '--disable-shared-folders': False, + '--disable-provisioning': False, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--checksum': None, + '--checksum-type': None, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '': '', + } + with raises(AttributeError, match=r"Must provide a name for the instance."): + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + + +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.locate', return_value=None) +def test_mech_up_with_name_not_in_mechfile(mock_locate, mock_load_mechfile): + """Test 'mech up' with a name that is not in the Mechfile.""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--detail': False, + '--gui': False, + '--disable-shared-folders': False, + '--disable-provisioning': False, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--checksum': None, + '--checksum-type': None, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '': 'notfirst', + } + with raises(SystemExit, match=r" was not found in the Mechfile"): + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + + +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.locate', return_value=None) +def test_mech_ssh_config_not_created(mock_locate, mock_load_mechfile, capfd): + """Test 'mech ssh-config' when vm is not created.""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '': 'first', + } + 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.get_guest_ip_address', return_value=None) +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +@patch('os.getcwd') +def test_mech_ssh_config_not_started(mock_getcwd, mock_locate, mock_load_mechfile, + mock_get_guest_ip_address): + """Test 'mech ssh-config' when vm is created but not started.""" + 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) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_get_guest_ip_address.assert_called() + + +@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', return_value=MECHFILE_ONE_ENTRY) +@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): + """Test 'mech ssh-config'.""" + 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 diff --git a/mech/test_mech_snapshot.py b/mech/test_mech_snapshot.py index 3b97fd3..94a407a 100644 --- a/mech/test_mech_snapshot.py +++ b/mech/test_mech_snapshot.py @@ -154,7 +154,7 @@ def test_mech_snapshot_delete_snapshot(mock_os_getcwd, mock_load_mechfile, arguments = {'': 'first', '': 'snap2'} mock_delete_snapshot.return_value = None a_mech.delete(arguments) - out, err = capfd.readouterr() + out, _ = capfd.readouterr() mock_delete_snapshot.assert_called() mock_list_snapshots.assert_called() assert re.search(r'Cannot delete', out, re.MULTILINE) From b9b83e172286fad4f22bb0a668f33b75222b91dc Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 9 Feb 2020 16:13:33 -0800 Subject: [PATCH 050/116] add mech snapshot save unit tests --- mech/mech.py | 14 ++++---- mech/test_mech_snapshot.py | 69 +++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index b2119c0..1e7b60e 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -337,7 +337,7 @@ def list(self, arguments): vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) print(vmrun.list_snapshots()) else: - print('Instance ({}) is not created.'.format(instance)) + print(colored.red('Instance ({}) is not created.'.format(instance))) # add alias for 'mech snapshot ls' ls = list @@ -361,12 +361,14 @@ def save(self, arguments): # pylint: disable=no-self-use instance = arguments[''] inst = MechInstance(instance) - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - if vmrun.snapshot(name) is None: - print(colored.red("Warning: Could not take snapshot.")) - sys.exit(1) + if inst.created: + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + 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.green("Snapshot ({}) on VM ({}) taken".format(name, instance))) + print(colored.red('Instance ({}) is not created.'.format(instance))) class Mech(MechCommand): diff --git a/mech/test_mech_snapshot.py b/mech/test_mech_snapshot.py index 94a407a..b8f03ca 100644 --- a/mech/test_mech_snapshot.py +++ b/mech/test_mech_snapshot.py @@ -2,6 +2,7 @@ import re from unittest.mock import patch +from pytest import raises import mech.command import mech.mech @@ -163,7 +164,7 @@ def test_mech_snapshot_delete_snapshot(mock_os_getcwd, mock_load_mechfile, # Note: delete_snapshots return None if could not delete, or '' if it could mock_delete_snapshot.return_value = '' a_mech.delete(arguments) - out, err = capfd.readouterr() + out, _ = capfd.readouterr() mock_delete_snapshot.assert_called() assert re.search(r' deleted', out, re.MULTILINE) @@ -174,3 +175,69 @@ def test_mech_snapshot_delete_snapshot(mock_os_getcwd, mock_load_mechfile, mock_list_snapshots.assert_called() mock_delete_snapshot.assert_called() assert re.search(r'Total snapshots: 0', out, re.MULTILINE) + + +MECHFILE_ONE_ENTRY = { + 'first': { + 'name': + 'first', + 'box': + 'bento/ubuntu-18.04', + 'box_version': + '201912.04.0' + } +} +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.locate', return_value=None) +def test_mech_snapshot_list_not_created(mock_locate, mock_load_mechfile, capfd): + """Test 'mech snapshot list' when vm is not created.""" + global_arguments = {'--debug': False} + arguments = { + '': 'first', + } + a_mech = mech.mech.MechSnapshot(arguments=global_arguments) + arguments = {'': 'first'} + a_mech.list(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', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_snapshot_save_success(mock_locate, mock_load_mechfile, + mock_snapshot, capfd): + """Test 'mech snapshot save' successful.""" + 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', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_snapshot_save_failure(mock_locate, mock_load_mechfile, + mock_snapshot): + """Test 'mech snapshot save' failure.""" + 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) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_snapshot.assert_called() From 5e804b58e5993a353f71ca4497d04937e83f4161 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 9 Feb 2020 21:46:07 -0800 Subject: [PATCH 051/116] working on utils.build_mechfile_entry unittests --- mech/mech.py | 2 +- mech/test_mech.py | 41 ++++++++++++--- mech/test_mech_box.py | 32 ++++++++++++ mech/test_mech_snapshot.py | 20 ++++++-- mech/test_utils.py | 100 +++++++++++++++++++++++++++++++++++++ mech/utils.py | 39 ++++++++------- 6 files changed, 205 insertions(+), 29 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 1e7b60e..1c171f9 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -228,7 +228,7 @@ def add(self, arguments): # pylint: disable=no-self-use force = arguments['--force'] requests_kwargs = utils.get_requests_kwargs(arguments) - utils.add_box(name=None, box=location, box_version=box_version, + utils.add_box(name=None, box=None, location=location, box_version=box_version, force=force, requests_kwargs=requests_kwargs) def list(self, arguments): # pylint: disable=no-self-use diff --git a/mech/test_mech.py b/mech/test_mech.py index 7159205..20d6a6f 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -48,6 +48,20 @@ def test_mech_list_with_one(mock_locate, mock_load_mechfile, capfd): assert re.search(r'first\s+notcreated', out, re.MULTILINE) +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.locate', return_value=None) +def test_mech_list_with_one_and_debug(mock_locate, mock_load_mechfile, capfd): + """Test 'mech list' with 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) + + MECHFILE_TWO_ENTRIES = { 'first': { 'name': @@ -121,8 +135,6 @@ def test_mech_up_without_name(mock_locate, mock_load_mechfile): } with raises(AttributeError, match=r"Must provide a name for the instance."): a_mech.up(arguments) - mock_locate.assert_called() - mock_load_mechfile.assert_called() @patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) @@ -149,8 +161,6 @@ def test_mech_up_with_name_not_in_mechfile(mock_locate, mock_load_mechfile): } with raises(SystemExit, match=r" was not found in the Mechfile"): a_mech.up(arguments) - mock_locate.assert_called() - mock_load_mechfile.assert_called() @patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) @@ -184,9 +194,6 @@ def test_mech_ssh_config_not_started(mock_getcwd, mock_locate, mock_load_mechfil } with raises(SystemExit, match=r".*not yet ready for SSH.*"): a_mech.ssh_config(arguments) - mock_locate.assert_called() - mock_load_mechfile.assert_called() - mock_get_guest_ip_address.assert_called() @patch('os.chmod') @@ -246,6 +253,26 @@ def test_mech_port_with_nat(mock_locate, mock_load_mechfile, mock_list_host_netw 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', return_value=MECHFILE_ONE_ENTRY) +@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): + """Test 'mech port first' with nat networking.""" + 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 diff --git a/mech/test_mech_box.py b/mech/test_mech_box.py index e91b754..7bcd359 100644 --- a/mech/test_mech_box.py +++ b/mech/test_mech_box.py @@ -59,3 +59,35 @@ def test_mech_box_list_one_box(mock_os_getcwd, capfd): mock_walk.assert_called() out, _ = capfd.readouterr() assert re.search(r'ubuntu-18.04', out, re.MULTILINE) +# +# TODO: work on this after adding other unit tests +# @patch('os.getcwd') +# def test_mech_box_add_one(mock_os_getcwd, capfd): +# """Test 'mech box add'.""" +# 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']), +# ] +# arguments = { +# '--force': False, +# '--insecure': False, +# '--cacert': None, +# '--capath': None, +# '--cert': None, +# '--box-version': None, +# '--checksum': None, +# '--checksum-type': None, +# '': 'bento/ubuntu-19.10', +# } +# a_mech.add(arguments) +# mock_walk.assert_called() +# out, _ = capfd.readouterr() +# assert re.search(r'Checking box', out, re.MULTILINE) diff --git a/mech/test_mech_snapshot.py b/mech/test_mech_snapshot.py index b8f03ca..7335844 100644 --- a/mech/test_mech_snapshot.py +++ b/mech/test_mech_snapshot.py @@ -204,6 +204,23 @@ def test_mech_snapshot_list_not_created(mock_locate, mock_load_mechfile, capfd): assert re.search(r'not created', out, re.MULTILINE) +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.locate', return_value=None) +def test_mech_snapshot_save_not_created(mock_locate, mock_load_mechfile, capfd): + """Test 'mech snapshot save' when vm is not created.""" + 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', return_value=MECHFILE_ONE_ENTRY) @patch('mech.utils.locate', return_value='/tmp/first/some.vmx') @@ -238,6 +255,3 @@ def test_mech_snapshot_save_failure(mock_locate, mock_load_mechfile, arguments = {'': 'first', '': 'snap1'} with raises(SystemExit, match=r"Warning: Could not take snapshot."): a_mech.save(arguments) - mock_locate.assert_called() - mock_load_mechfile.assert_called() - mock_snapshot.assert_called() diff --git a/mech/test_utils.py b/mech/test_utils.py index 24e0f9e..d6680cc 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -3,6 +3,7 @@ from unittest.mock import patch, mock_open from collections import OrderedDict +from pytest import raises import mech.utils @@ -233,3 +234,102 @@ def test_parse_vmx(): with patch('builtins.open', a_mock): assert mech.utils.parse_vmx(partial_vmx) == expected_vmx a_mock.assert_called() + + +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': 'first', + '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': 'first', + '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': 'first', + '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', + '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(): + """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 + json_data = """{ + "description": "foo", + "short_description": "foo", + "name": "bento/ubuntu-18.04", + "versions": [ + { + "version": "202002.04.0", + "status": "active", + "description_html": "foo", + "description_markdown": "foo", + "providers": [ + { + "name": "vmware_desktop", + "url": "https://vagrantcloud.com/bento/boxes/ubuntu-18.04/\ +versions/202002.04.0/providers/vmware_desktop.box", + "checksum": null, + "checksum_type": null + } + ] + } + ] + }""" + expected = { + 'box': 'bento/ubuntu-18.04', + 'box_version': '202002.04.0', + 'name': 'first', + 'url': + 'https://vagrantcloud.com/bento/boxes/ubuntu-18.04/\ +versions/202002.04.0/providers/vmware_desktop.box' + } + a_mock = mock_open(read_data=json_data) + 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') diff --git a/mech/utils.py b/mech/utils.py index 718f343..fea0b2a 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -222,33 +222,37 @@ def build_mechfile_entry(location, box=None, name=None, box_version=None, reques if requests_kwargs is None: requests_kwargs = {} mechfile_entry = {} + if location is None: return mechfile_entry + if not name: + name = 'first' + mechfile_entry['name'] = name + mechfile_entry['box'] = box + mechfile_entry['box_version'] = box_version + if any(location.startswith(s) for s in ('https://', 'http://', 'ftp://')): mechfile_entry['url'] = location - if not name: - name = 'first' - mechfile_entry['box'] = box - if box_version: - mechfile_entry['box_version'] = box_version return mechfile_entry + elif location.startswith('file:') or os.path.isfile(re.sub(r'^file:(?://)?', '', location)): location = re.sub(r'^file:(?://)?', '', location) LOGGER.debug('location:%s', location) + mechfile_entry['file'] = location try: - # see if the location is a json file + # 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()) except ValueError: # includes simplejson.decoder.JSONDecodeError - mechfile_entry['file'] = location - if not name: - name = 'first' - mechfile_entry['box'] = box - if box_version: - mechfile_entry['box_version'] = box_version - LOGGER.debug('mechfile_entry:%s', mechfile_entry) - return mechfile_entry + # this means the location/file is probably a .box file + LOGGER.debug('mechfile_entry:%s', mechfile_entry) + return mechfile_entry + except IOError: + # cannot open file + sys.exit('Error: Cannot open file:({})'.format(location)) else: try: account, box, ver = (location.split('/', 2) + ['', ''])[:3] @@ -264,11 +268,10 @@ def build_mechfile_entry(location, box=None, name=None, box_version=None, reques response.raise_for_status() catalog = response.json() except (requests.HTTPError, ValueError) as exc: - print(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: - print(colored.red("Couldn't connect to HashiCorp's Vagrant Cloud API")) - sys.exit(1) + 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) From 0d001b40fa433a22e9dd5be61f1d40be490147ef Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 9 Feb 2020 23:36:36 -0800 Subject: [PATCH 052/116] improved build_mechfile unit tests --- mech/test_utils.py | 54 +++++++++++++++++++++++++++++++++++++++++++--- mech/utils.py | 14 ++++++------ 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/mech/test_utils.py b/mech/test_utils.py index d6680cc..df90d3f 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -1,5 +1,6 @@ """Test mech utils.""" import os +import json from unittest.mock import patch, mock_open from collections import OrderedDict @@ -246,7 +247,7 @@ def test_build_mechfile_entry_https_location(): assert mech.utils.build_mechfile_entry(location='https://foo') == { 'box': None, 'box_version': None, - 'name': 'first', + 'name': None, 'url': 'https://foo' } @@ -256,7 +257,7 @@ def test_build_mechfile_entry_http_location(): assert mech.utils.build_mechfile_entry(location='http://foo') == { 'box': None, 'box_version': None, - 'name': 'first', + 'name': None, 'url': 'http://foo' } @@ -267,7 +268,7 @@ def test_build_mechfile_entry_ftp_location(): assert mech.utils.build_mechfile_entry(location='ftp://foo') == { 'box': None, 'box_version': None, - 'name': 'first', + 'name': None, 'url': 'ftp://foo' } @@ -333,3 +334,50 @@ def test_build_mechfile_entry_file_location_but_file_not_found(): 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): + """Test if location talks to Hashicorp.""" + 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 + } + ] + } + ] + }""" + catalog_as_json = json.loads(catalog) + expected = { + 'box': 'bento/ubuntu-18.04', + 'box_version': 'aaa', + 'name': None, + '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') diff --git a/mech/utils.py b/mech/utils.py index fea0b2a..1687735 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -225,18 +225,20 @@ def build_mechfile_entry(location, box=None, name=None, box_version=None, reques if location is None: return mechfile_entry - if not name: - name = 'first' mechfile_entry['name'] = name mechfile_entry['box'] = box mechfile_entry['box_version'] = box_version if any(location.startswith(s) for s in ('https://', 'http://', 'ftp://')): + if not name: + 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 @@ -257,7 +259,7 @@ def build_mechfile_entry(location, box=None, name=None, box_version=None, reques try: account, box, ver = (location.split('/', 2) + ['', ''])[:3] if not account or not box: - print(colored.red("Provided box name is not valid")) + sys.exit(colored.red("Provided box name is not valid")) if ver: box_version = ver print( @@ -291,11 +293,7 @@ def catalog_to_mechfile(catalog, name=None, box=None, box_version=None): mechfile['box_version'] = current_version mechfile['url'] = provider['url'] return mechfile - print( - colored.red( - "Couldn't find a VMWare compatible VM for '{}'{}".format( - name, " ({})".format(box_version) if box_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): From 0bac6bc91fffb07f2ee029d71f82e3ab63421f07 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 10 Feb 2020 10:54:37 -0800 Subject: [PATCH 053/116] add mech box add/remove unittests --- mech/mech.py | 16 +++- mech/test_mech_box.py | 172 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 154 insertions(+), 34 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 1c171f9..f88bb6f 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -209,7 +209,11 @@ def add(self, arguments): # pylint: disable=no-self-use Usage: mech box add [options] Notes: - The location can be a URL, local .box file, or 'bento/ubuntu-18.04'. + 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: -f, --force Overwrite an existing box if it exists @@ -272,6 +276,9 @@ def remove(self, arguments): # pylint: disable=no-self-use 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 @@ -438,7 +445,12 @@ def init(self, arguments): # pylint: disable=no-self-use Usage: mech init [options] - Example box: bento/ubuntu-18.04 + 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: -f, --force Overwrite existing Mechfile diff --git a/mech/test_mech_box.py b/mech/test_mech_box.py index 7bcd359..f4faf2d 100644 --- a/mech/test_mech_box.py +++ b/mech/test_mech_box.py @@ -1,5 +1,6 @@ """Unit tests for 'mech box'.""" import re +import json from unittest.mock import patch @@ -59,35 +60,142 @@ def test_mech_box_list_one_box(mock_os_getcwd, capfd): mock_walk.assert_called() out, _ = capfd.readouterr() assert re.search(r'ubuntu-18.04', out, re.MULTILINE) -# -# TODO: work on this after adding other unit tests -# @patch('os.getcwd') -# def test_mech_box_add_one(mock_os_getcwd, capfd): -# """Test 'mech box add'.""" -# 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']), -# ] -# arguments = { -# '--force': False, -# '--insecure': False, -# '--cacert': None, -# '--capath': None, -# '--cert': None, -# '--box-version': None, -# '--checksum': None, -# '--checksum-type': None, -# '': 'bento/ubuntu-19.10', -# } -# a_mech.add(arguments) -# mock_walk.assert_called() -# 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_new(mock_os_getcwd, mock_os_path_exists, + mock_requests_get, capfd): + """Test 'mech box add' from Hashicorp'.""" + mock_os_getcwd.return_value = '/tmp' + mock_os_path_exists.return_value = False + global_arguments = {'--debug': False} + 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 + } + ] + } + ] + }""" + catalog_as_json = json.loads(catalog) + 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 = { + '--force': False, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--box-version': None, + '--checksum': None, + '--checksum-type': None, + '': '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): + """Test 'mech box add' from Hashicorp'.""" + mock_os_getcwd.return_value = '/tmp' + mock_os_path_exists.return_value = True + global_arguments = {'--debug': False} + 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 + } + ] + } + ] + }""" + catalog_as_json = json.loads(catalog) + 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 = { + '--force': False, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--box-version': None, + '--checksum': None, + '--checksum-type': None, + '': 'bento/ubuntu-18.04', + } + 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) From c8c6309c4d836b3a97b916df1611230c9c4ed6ce Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 10 Feb 2020 11:06:39 -0800 Subject: [PATCH 054/116] add unit test for mech init --- mech/mech.py | 8 ++--- mech/test_mech.py | 80 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index f88bb6f..6b66a97 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -479,11 +479,9 @@ def init(self, arguments): # pylint: disable=no-self-use LOGGER.debug('name:%s box:%s box_version:%s location:%s', name, box, box_version, location) if os.path.exists('Mechfile') and not force: - print(colored.red(textwrap.fill( - "`Mechfile` already exists in this directory. Remove it " - "before running `mech init`." - ))) - sys.exit(1) + sys.exit(colored.red(textwrap.fill( + "`Mechfile` already exists in this directory. Remove it " + "before running `mech init`."))) print(colored.green("Initializing mech")) if utils.init_mechfile( diff --git a/mech/test_mech.py b/mech/test_mech.py index 20d6a6f..7745e02 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -1,6 +1,7 @@ """Test the mech cli. """ import subprocess import re +import json from unittest.mock import patch, mock_open from pytest import raises @@ -317,3 +318,82 @@ def test_mech_port_without_nat(mock_locate, mock_load_mechfile, mock_list_host_n 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): + """Test 'mech init' from Hashicorp'.""" + mock_os_getcwd.return_value = '/tmp' + mock_os_path_exists.return_value = False + global_arguments = {'--debug': False} + 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 + } + ] + } + ] + }""" + catalog_as_json = json.loads(catalog) + 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 = { + '--force': False, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--box-version': None, + '--checksum': None, + '--checksum-type': None, + '--name': None, + '--box': None, + '': 'bento/ubuntu-18.04', + } + 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, capfd): + """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 = { + '--force': False, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--box-version': None, + '--checksum': None, + '--checksum-type': None, + '--name': None, + '--box': None, + '': 'bento/ubuntu-18.04', + } + with raises(SystemExit, match=r".*already exists in this directory.*"): + a_mech.init(arguments) From ffb630c7720b3dc6eb72c05809d056b57921eb9d Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 10 Feb 2020 11:29:44 -0800 Subject: [PATCH 055/116] add unittests for mech add/init --- mech/mech.py | 3 +- mech/test_mech.py | 98 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 6b66a97..a8356c6 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -522,8 +522,7 @@ def add(self, arguments): # pylint: disable=no-self-use location = arguments[''] if not name or name == "": - print(colored.red("Need to provide a name for the instance to add to the Mechfile.")) - sys.exit(1) + sys.exit(colored.red("Need to provide a name for the instance to add to the Mechfile.")) requests_kwargs = utils.get_requests_kwargs(arguments) diff --git a/mech/test_mech.py b/mech/test_mech.py index 7745e02..f6af100 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -376,7 +376,7 @@ def test_mech_init(mock_os_getcwd, mock_os_path_exists, @patch('os.path.exists') @patch('os.getcwd') -def test_mech_init_mechfile_exists(mock_os_getcwd, mock_os_path_exists, capfd): +def test_mech_init_mechfile_exists(mock_os_getcwd, mock_os_path_exists): """Test 'mech init' when Mechfile exists'.""" mock_os_getcwd.return_value = '/tmp' mock_os_path_exists.return_value = True @@ -397,3 +397,99 @@ def test_mech_init_mechfile_exists(mock_os_getcwd, mock_os_path_exists, capfd): } 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): + """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 = { + '--force': False, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--box-version': None, + '--checksum': None, + '--checksum-type': None, + '--name': None, + '--box': None, + '': '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): + """Test 'mech add' when Mechfile exists'.""" + mock_os_getcwd.return_value = '/tmp' + 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 + } + ] + } + ] + }""" + catalog_as_json = json.loads(catalog) + 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 = { + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--box-version': None, + '--checksum': None, + '--checksum-type': None, + '': 'second', + '--box': None, + '': 'bento/ubuntu-18.04', + } + 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(): + """Test 'mech add' when Mechfile exists but no name provided'.""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--box-version': None, + '--checksum': None, + '--checksum-type': None, + '': None, + '--box': None, + '': 'bento/ubuntu-18.04', + } + with raises(SystemExit, match=r".*Need to provide a name.*"): + a_mech.add(arguments) From ab940df4dc642876491e6a008dfaf94728710d63 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 10 Feb 2020 12:01:20 -0800 Subject: [PATCH 056/116] add unittests for mech remove --- mech/mech.py | 6 ++---- mech/test_mech.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index a8356c6..e49bcdc 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -552,8 +552,7 @@ def remove(self, arguments): name = arguments[''] if not name or name == "": - print(colored.red("Need to provide a name to be removed from the Mechfile.")) - sys.exit(1) + sys.exit(colored.red("Need to provide a name to be removed from the Mechfile.")) LOGGER.debug('name:%s', name) @@ -566,8 +565,7 @@ def remove(self, arguments): else: print(colored.red("Could not remove {} from the Mechfile".format(name))) else: - print(colored.red("There is no instance called ({}) in the Mechfile.".format(name))) - sys.exit(1) + sys.exit(colored.red("There is no instance called ({}) in the Mechfile.".format(name))) # add alias for 'mech delete' delete = remove diff --git a/mech/test_mech.py b/mech/test_mech.py index f6af100..204ecae 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -493,3 +493,45 @@ def test_mech_add_mechfile_exists_no_name(): } with raises(SystemExit, match=r".*Need to provide a name.*"): a_mech.add(arguments) + + +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('os.getcwd') +def test_mech_remove(mock_os_getcwd, mock_load_mechfile, capfd): + """Test 'mech remove'.""" + 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', return_value=MECHFILE_ONE_ENTRY) +@patch('os.getcwd') +def test_mech_remove_a_nonexisting_entry(mock_os_getcwd, mock_load_mechfile, capfd): + """Test 'mech remove'.""" + 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) From 32d56b495ac0a8a81b8886240e6c1d229764fffe Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 10 Feb 2020 12:29:17 -0800 Subject: [PATCH 057/116] add unittests for mech up --- mech/mech.py | 1 - mech/test_mech.py | 114 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/mech/mech.py b/mech/mech.py index e49bcdc..ad23eee 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -642,7 +642,6 @@ def up(self, arguments): # pylint: disable=invalid-name if started is None: print(colored.red("VM not started")) else: - time.sleep(3) print(colored.blue("Getting IP address...")) lookup = inst.enable_ip_lookup ip_address = vmrun.get_guest_ip_address(lookup=lookup) diff --git a/mech/test_mech.py b/mech/test_mech.py index 204ecae..1c0207b 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -164,6 +164,120 @@ def test_mech_up_with_name_not_in_mechfile(mock_locate, mock_load_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=True) +@patch('mech.utils.init_box', return_value='/tmp/first/one.vmx') +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.locate', return_value='/tmp/first/one.vmx') +def test_mech_up(mock_locate, mock_load_mechfile, mock_init_box, + mock_vmrun_start, mock_vmrun_get_ip, capfd): + """Test 'mech up'.""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--gui': False, + '--disable-shared-folders': True, + '--disable-provisioning': True, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--checksum': None, + '--checksum-type': None, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '': None, + } + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_init_box.assert_called() + mock_vmrun_start.assert_called() + mock_vmrun_get_ip.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'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.init_box', return_value='/tmp/first/one.vmx') +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.locate', return_value='/tmp/first/one.vmx') +def test_mech_up_with_provisioning(mock_locate, mock_load_mechfile, mock_init_box, + mock_vmrun_start, mock_vmrun_get_ip, + mock_provision, capfd): + """Test 'mech up'.""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--gui': False, + '--disable-shared-folders': True, + '--disable-provisioning': False, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--checksum': None, + '--checksum-type': None, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '': None, + } + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_init_box.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.add_shared_folder') +@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.init_box', return_value='/tmp/first/one.vmx') +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.locate', return_value='/tmp/first/one.vmx') +def test_mech_up_wth_shared_folders(mock_locate, mock_load_mechfile, mock_init_box, + mock_vmrun_start, mock_vmrun_get_ip, + mock_vmrun_enable_shared_folders, + mock_vmrun_add_shared_folder, capfd): + """Test 'mech up'.""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--gui': False, + '--disable-shared-folders': False, + '--disable-provisioning': True, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--checksum': None, + '--checksum-type': None, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '': None, + } + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_init_box.assert_called() + mock_vmrun_start.assert_called() + mock_vmrun_get_ip.assert_called() + mock_vmrun_enable_shared_folders.assert_called() + mock_vmrun_add_shared_folder.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'started', out, re.MULTILINE) + + @patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) @patch('mech.utils.locate', return_value=None) def test_mech_ssh_config_not_created(mock_locate, mock_load_mechfile, capfd): From 23b56fe2c11eb6a939f96c9389ec180920bf72ae Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 10 Feb 2020 12:38:55 -0800 Subject: [PATCH 058/116] add another case for mech up --- mech/test_mech.py | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/mech/test_mech.py b/mech/test_mech.py index 1c0207b..56b1d3e 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -169,8 +169,8 @@ def test_mech_up_with_name_not_in_mechfile(mock_locate, mock_load_mechfile): @patch('mech.utils.init_box', return_value='/tmp/first/one.vmx') @patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) @patch('mech.utils.locate', return_value='/tmp/first/one.vmx') -def test_mech_up(mock_locate, mock_load_mechfile, mock_init_box, - mock_vmrun_start, mock_vmrun_get_ip, capfd): +def test_mech_up_already_started(mock_locate, mock_load_mechfile, mock_init_box, + mock_vmrun_start, mock_vmrun_get_ip, capfd): """Test 'mech up'.""" global_arguments = {'--debug': False} a_mech = mech.mech.Mech(arguments=global_arguments) @@ -196,7 +196,41 @@ def test_mech_up(mock_locate, mock_load_mechfile, mock_init_box, mock_vmrun_start.assert_called() mock_vmrun_get_ip.assert_called() out, _ = capfd.readouterr() - assert re.search(r'started', out, re.MULTILINE) + assert re.search(r'\)started on', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") +@patch('mech.vmrun.VMrun.start', return_value=None) +@patch('mech.utils.init_box', return_value='/tmp/first/one.vmx') +@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.locate', return_value='/tmp/first/one.vmx') +def test_mech_up_problem(mock_locate, mock_load_mechfile, mock_init_box, + mock_vmrun_start, mock_vmrun_get_ip, capfd): + """Test 'mech up' when issue with starting VM""" + global_arguments = {'--debug': False} + a_mech = mech.mech.Mech(arguments=global_arguments) + arguments = { + '--gui': False, + '--disable-shared-folders': True, + '--disable-provisioning': True, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--checksum': None, + '--checksum-type': None, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '': None, + } + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_init_box.assert_called() + mock_vmrun_start.assert_called() + out, _ = capfd.readouterr() + assert re.search(r'not started', out, re.MULTILINE) @patch('mech.utils.provision') From 3525b07dbca1bf6212b42be59d84a5b11cc25a28 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 10 Feb 2020 13:20:32 -0800 Subject: [PATCH 059/116] add unittest for mech ps --- mech/mech.py | 15 ++++++++---- mech/test_mech.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index ad23eee..ea03ec3 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -81,7 +81,7 @@ class MechInstance(): def __init__(self, name, mechfile=None): """Constructor for the mech instance.""" - if name == "": + if not name or name == "": raise AttributeError("Must provide a name for the instance.") if not mechfile: mechfile = utils.load_mechfile() @@ -691,10 +691,15 @@ def ps(self, arguments): # pylint: disable=invalid-name,no-self-use Options: -h, --help Print this help """ - instance = arguments[''] - inst = MechInstance(instance) - vmrun = VMrun(inst.vmx, inst.user, inst.password) - print(vmrun.list_processes_in_guest()) + instance_name = arguments[''] + + inst = MechInstance(instance_name) + + if inst.created: + 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 diff --git a/mech/test_mech.py b/mech/test_mech.py index 56b1d3e..514d663 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -683,3 +683,64 @@ def test_mech_remove_no_name(): } 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', return_value=MECHFILE_TWO_ENTRIES) +@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): + """Test 'mech ps'.""" + 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', return_value=MECHFILE_TWO_ENTRIES) +@patch('mech.utils.locate', return_value='') +@patch('os.getcwd') +def test_mech_ps_not_started_vm(mock_getcwd, mock_locate, + mock_load_mechfile, capfd): + """Test 'mech ps'.""" + 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) From 1edbc9a0d5fd28000c694150755356222f103655 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 10 Feb 2020 15:54:18 -0800 Subject: [PATCH 060/116] add more unittests pause/resume/stop; remove sleeps --- mech/mech.py | 85 +++++++++++++------------ mech/test_mech.py | 130 +++++++++++++++++++++++++++++++++++++++ tests/int/provision.bats | 1 + 3 files changed, 176 insertions(+), 40 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index ea03ec3..4e44a9e 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -28,7 +28,6 @@ import os import re import sys -import time import fnmatch import logging import tempfile @@ -725,28 +724,31 @@ def status(self, arguments): for instance in instances: inst = MechInstance(instance) - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + if inst.created: + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - 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)) + 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): """ @@ -778,13 +780,12 @@ def destroy(self, arguments): print(colored.green("Deleting ({})...".format(instance))) vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) vmrun.stop(mode='hard', quiet=True) - time.sleep(3) vmrun.delete_vm() - if os.path.exists(inst.path): shutil.rmtree(inst.path) else: LOGGER.debug("%s was not found.", inst.path) + print("Deleted") else: print(colored.red("Deletion aborted")) else: @@ -814,15 +815,18 @@ def down(self, arguments): for instance in instances: inst = MechInstance(instance) - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - 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)) + if inst.created: + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + 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.green("Stopped", vmrun)) + print(colored.red("VM ({}) not created.".format(instance))) # alias 'mech stop' and 'mech halt' to 'mech down' stop = down @@ -848,11 +852,15 @@ def pause(self, arguments): for instance in instances: inst = MechInstance(instance) - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - if vmrun.pause() is None: - print(colored.red("Not paused", vmrun)) + + if inst.created: + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + if vmrun.pause() is None: + print(colored.red("Not paused", vmrun)) + else: + print(colored.yellow("Paused", vmrun)) else: - print(colored.yellow("Paused", vmrun)) + print(colored.red("VM ({}) not created.".format(instance))) def resume(self, arguments): """ @@ -882,12 +890,11 @@ def resume(self, arguments): LOGGER.debug('instance:%s inst.vmx:%s', instance, inst.vmx) # if we have started this instance before, try to unpause - if inst.vmx: + if inst.created: vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) if vmrun.unpause(quiet=True) is not None: - time.sleep(1) print(colored.blue("Getting IP address...")) lookup = inst.enable_ip_lookup ip_address = vmrun.get_guest_ip_address(lookup=lookup) @@ -910,7 +917,6 @@ def resume(self, arguments): if started is None: print(colored.red("VM not started")) else: - time.sleep(3) print(colored.blue("Getting IP address...")) lookup = inst.enable_ip_lookup ip_address = vmrun.get_guest_ip_address(lookup=lookup) @@ -1167,7 +1173,6 @@ def reload(self, arguments): if started is None: print(colored.red("VM not restarted")) else: - time.sleep(3) print(colored.blue("Getting IP address...")) lookup = inst.enable_ip_lookup ip_address = vmrun.get_guest_ip_address(lookup=lookup) diff --git a/mech/test_mech.py b/mech/test_mech.py index 514d663..e244983 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -102,6 +102,136 @@ def test_mech_list_with_two(mock_locate, mock_load_mechfile, capfd): assert re.search(r'second\s+notcreated', out, re.MULTILINE) +@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@patch('mech.utils.locate', return_value=None) +def test_mech_status_with_two_not_created(mock_locate, mock_load_mechfile, capfd): + """Test 'mech status' with two entries, neither created.""" + 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', return_value=MECHFILE_TWO_ENTRIES) +@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): + """Test 'mech status' powered on.""" + 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('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', return_value=MECHFILE_TWO_ENTRIES) +@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, capfd): + """Test 'mech destroy' powered on.""" + 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_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.vmrun.VMrun.installed_tools', return_value='running') +@patch('mech.vmrun.VMrun.stop', return_value=True) +@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_stop(mock_locate, mock_load_mechfile, + mock_vmrun_stop, mock_installed_tools, + capfd): + """Test 'mech stop' powered on.""" + 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.pause', return_value=True) +@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_pause(mock_locate, mock_load_mechfile, + mock_vmrun_stop, capfd): + """Test 'mech stop' powered on.""" + 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_stop.assert_called() + assert re.search(r'Paused', out, re.MULTILINE) + + +@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', return_value=MECHFILE_TWO_ENTRIES) +@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, + capfd): + """Test 'mech resume'.""" + 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_get_ip.assert_called() + assert re.search(r'resumed', out, re.MULTILINE) + + MECHFILE_BAD_ENTRY = { '': { 'name': diff --git a/tests/int/provision.bats b/tests/int/provision.bats index 1008b87..0de9997 100755 --- a/tests/int/provision.bats +++ b/tests/int/provision.bats @@ -10,6 +10,7 @@ # setup find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + mech destroy -f || true run mech up --disable-provisioning regex1="VM (first) started" From a2f25c3c95f1212971f48899baae948a8865ed3b Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 10 Feb 2020 16:32:19 -0800 Subject: [PATCH 061/116] add more unittests mech ssh/suspend/ip --- mech/mech.py | 65 ++++++++++++++++++++++++++--------------------- mech/test_mech.py | 63 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 4e44a9e..01705e8 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -961,11 +961,15 @@ def suspend(self, arguments): for instance in instances: inst = MechInstance(instance) - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - if vmrun.suspend() is None: - print(colored.red("Not suspended", vmrun)) + + if inst.created: + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + if vmrun.suspend() is None: + print(colored.red("Not suspended", vmrun)) + else: + print(colored.green("Suspended", vmrun)) else: - print(colored.green("Suspended", vmrun)) + print("VM has not been created.") def ssh_config(self, arguments): """ @@ -1011,31 +1015,34 @@ def ssh(self, arguments): # pylint: disable=no-self-use inst = MechInstance(instance) - config_ssh = inst.config_ssh() - temp_file = tempfile.NamedTemporaryFile(delete=False) - try: - temp_file.write(utils.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)) - return subprocess.call(cmds) - finally: - os.unlink(temp_file.name) + if inst.created: + config_ssh = inst.config_ssh() + temp_file = tempfile.NamedTemporaryFile(delete=False) + try: + temp_file.write(utils.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)) + return subprocess.call(cmds) + finally: + os.unlink(temp_file.name) + else: + print("VM not created.") def scp(self, arguments): # pylint: disable=no-self-use """ diff --git a/mech/test_mech.py b/mech/test_mech.py index e244983..26a016d 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -188,6 +188,69 @@ def test_mech_stop(mock_locate, mock_load_mechfile, assert re.search(r'Stopped', out, re.MULTILINE) +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.145") +@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_ip(mock_locate, mock_load_mechfile, + mock_get_ip, capfd): + """Test 'mech ip' powered on.""" + 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.suspend', return_value=True) +@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_suspend(mock_locate, mock_load_mechfile, + mock_vmrun_suspend, capfd): + """Test 'mech suspend' powered on.""" + 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.installed_tools', return_value='running') +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.130") +@patch('subprocess.call', return_value='00:03:30 up 2 min, load average: 0.00, 0.00, 0.00') +@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +def test_mech_ssh(mock_locate, mock_load_mechfile, + mock_subprocess_call, mock_get_ip, mock_installed_tools): + """Test 'mech ssh'""" + 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() + mock_subprocess_call.assert_called() + mock_installed_tools.assert_called() + mock_get_ip.assert_called() + + @patch('mech.vmrun.VMrun.pause', return_value=True) @patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) @patch('mech.utils.locate', return_value='/tmp/first/some.vmx') From f55de9fd4e6c1169daebe92a24a2f9fb8fdf86be Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 10 Feb 2020 16:52:58 -0800 Subject: [PATCH 062/116] add mech provision unittests --- mech/mech.py | 6 ++- mech/test_mech.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/mech/mech.py b/mech/mech.py index 01705e8..187ddd1 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -1150,7 +1150,11 @@ def provision(self, arguments): for instance in instances: inst = MechInstance(instance) - utils.provision(instance, inst.vmx, inst.user, inst.password, inst.provision, show) + + if inst.created: + utils.provision(instance, inst.vmx, inst.user, inst.password, inst.provision, show) + else: + print("VM not created.") def reload(self, arguments): """ diff --git a/mech/test_mech.py b/mech/test_mech.py index 26a016d..4f88099 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -207,6 +207,105 @@ def test_mech_ip(mock_locate, mock_load_mechfile, assert re.search(r'192.168', 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.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', return_value=MECHFILE_TWO_ENTRIES) @patch('mech.utils.locate', return_value='/tmp/first/some.vmx') From 7960703380643758d3dc59770931e0d286d88059 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 11 Feb 2020 07:55:00 -0800 Subject: [PATCH 063/116] migrate some test code into fixtures --- mech/conftest.py | 42 ++++++++ mech/test_mech.py | 192 ++++++++++++++++++++----------------- mech/test_mech_snapshot.py | 80 ++++++---------- 3 files changed, 174 insertions(+), 140 deletions(-) create mode 100644 mech/conftest.py diff --git a/mech/conftest.py b/mech/conftest.py new file mode 100644 index 0000000..8175b57 --- /dev/null +++ b/mech/conftest.py @@ -0,0 +1,42 @@ +"""Common pytest code.""" + +import pytest + + +@pytest.fixture +def mechfile_one_entry(): + return { + 'first': { + 'name': 'first', + 'box': 'bento/ubuntu-18.04', + 'box_version': '201912.04.0' + } + } + + +@pytest.fixture +def mechfile_two_entries(): + return { + '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' + } + } diff --git a/mech/test_mech.py b/mech/test_mech.py index 4f88099..1b7ae51 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -25,20 +25,12 @@ def test_help(): assert return_value == 0 -MECHFILE_ONE_ENTRY = { - 'first': { - 'name': - 'first', - 'box': - 'bento/ubuntu-18.04', - 'box_version': - '201912.04.0' - } -} -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.load_mechfile') @patch('mech.utils.locate', return_value=None) -def test_mech_list_with_one(mock_locate, mock_load_mechfile, capfd): +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} @@ -49,10 +41,12 @@ def test_mech_list_with_one(mock_locate, mock_load_mechfile, capfd): assert re.search(r'first\s+notcreated', out, re.MULTILINE) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@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): +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} @@ -63,34 +57,12 @@ def test_mech_list_with_one_and_debug(mock_locate, mock_load_mechfile, capfd): assert re.search(r'created:False', out, re.MULTILINE) -MECHFILE_TWO_ENTRIES = { - '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' - } -} -@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@patch('mech.utils.load_mechfile') @patch('mech.utils.locate', return_value=None) -def test_mech_list_with_two(mock_locate, mock_load_mechfile, capfd): +def test_mech_list_with_two(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} @@ -102,10 +74,12 @@ def test_mech_list_with_two(mock_locate, mock_load_mechfile, capfd): assert re.search(r'second\s+notcreated', out, re.MULTILINE) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@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): +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} @@ -119,11 +93,13 @@ def test_mech_status_with_two_not_created(mock_locate, mock_load_mechfile, capfd @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', return_value=MECHFILE_TWO_ENTRIES) +@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): + 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'} @@ -140,12 +116,14 @@ def test_mech_status_powered_on(mock_locate, mock_load_mechfile, @patch('shutil.rmtree') @patch('mech.vmrun.VMrun.delete_vm') @patch('mech.vmrun.VMrun.stop', return_value=True) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@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, capfd): + mock_rmtree, mock_path_exists, 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) @@ -167,12 +145,13 @@ def test_mech_destroy(mock_locate, mock_load_mechfile, @patch('mech.vmrun.VMrun.installed_tools', return_value='running') @patch('mech.vmrun.VMrun.stop', return_value=True) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@patch('mech.utils.load_mechfile') @patch('mech.utils.locate', return_value='/tmp/first/some.vmx') def test_mech_stop(mock_locate, mock_load_mechfile, mock_vmrun_stop, mock_installed_tools, - capfd): + capfd, mechfile_two_entries): """Test 'mech stop' powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries global_arguments = {'--debug': False} a_mech = mech.mech.Mech(arguments=global_arguments) arguments = { @@ -189,11 +168,12 @@ def test_mech_stop(mock_locate, mock_load_mechfile, @patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.145") -@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@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): + 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 = { @@ -307,11 +287,12 @@ def test_mech_provision_shell(mock_locate, mock_load_mechfile, @patch('mech.vmrun.VMrun.suspend', return_value=True) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@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): + 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 = { @@ -328,11 +309,13 @@ def test_mech_suspend(mock_locate, mock_load_mechfile, @patch('mech.vmrun.VMrun.installed_tools', return_value='running') @patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.130") @patch('subprocess.call', return_value='00:03:30 up 2 min, load average: 0.00, 0.00, 0.00') -@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@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_call, mock_get_ip, mock_installed_tools): + mock_subprocess_call, mock_get_ip, mock_installed_tools, + mechfile_two_entries): """Test 'mech ssh'""" + mock_load_mechfile.return_value = mechfile_two_entries global_arguments = {'--debug': False} a_mech = mech.mech.Mech(arguments=global_arguments) arguments = { @@ -351,11 +334,12 @@ def test_mech_ssh(mock_locate, mock_load_mechfile, @patch('mech.vmrun.VMrun.pause', return_value=True) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@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_stop, capfd): + mock_vmrun_stop, capfd, mechfile_two_entries): """Test 'mech stop' powered on.""" + mock_load_mechfile.return_value = mechfile_two_entries global_arguments = {'--debug': False} a_mech = mech.mech.Mech(arguments=global_arguments) arguments = { @@ -372,12 +356,13 @@ def test_mech_pause(mock_locate, mock_load_mechfile, @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', return_value=MECHFILE_TWO_ENTRIES) +@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, - capfd): + 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 = { @@ -430,10 +415,12 @@ def test_mech_up_without_name(mock_locate, mock_load_mechfile): a_mech.up(arguments) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.load_mechfile') @patch('mech.utils.locate', return_value=None) -def test_mech_up_with_name_not_in_mechfile(mock_locate, mock_load_mechfile): +def test_mech_up_with_name_not_in_mechfile(mock_locate, 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 = { @@ -459,11 +446,13 @@ def test_mech_up_with_name_not_in_mechfile(mock_locate, mock_load_mechfile): @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.init_box', return_value='/tmp/first/one.vmx') -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@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_init_box, - mock_vmrun_start, mock_vmrun_get_ip, capfd): + 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 = { @@ -494,11 +483,13 @@ def test_mech_up_already_started(mock_locate, mock_load_mechfile, mock_init_box, @patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") @patch('mech.vmrun.VMrun.start', return_value=None) @patch('mech.utils.init_box', return_value='/tmp/first/one.vmx') -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@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_init_box, - mock_vmrun_start, mock_vmrun_get_ip, capfd): + mock_vmrun_start, mock_vmrun_get_ip, 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 = { @@ -529,12 +520,13 @@ def test_mech_up_problem(mock_locate, mock_load_mechfile, mock_init_box, @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.init_box', return_value='/tmp/first/one.vmx') -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@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_init_box, mock_vmrun_start, mock_vmrun_get_ip, - mock_provision, capfd): + 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 = { @@ -568,13 +560,15 @@ def test_mech_up_with_provisioning(mock_locate, mock_load_mechfile, mock_init_bo @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.init_box', return_value='/tmp/first/one.vmx') -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@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_init_box, mock_vmrun_start, mock_vmrun_get_ip, mock_vmrun_enable_shared_folders, - mock_vmrun_add_shared_folder, capfd): + mock_vmrun_add_shared_folder, 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 = { @@ -604,10 +598,12 @@ def test_mech_up_wth_shared_folders(mock_locate, mock_load_mechfile, mock_init_b assert re.search(r'started', out, re.MULTILINE) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@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): +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 = { @@ -621,12 +617,13 @@ def test_mech_ssh_config_not_created(mock_locate, mock_load_mechfile, capfd): @patch('mech.vmrun.VMrun.get_guest_ip_address', return_value=None) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.load_mechfile') @patch('mech.utils.locate', return_value='/tmp/first/some.vmx') @patch('os.getcwd') def test_mech_ssh_config_not_started(mock_getcwd, mock_locate, mock_load_mechfile, - mock_get_guest_ip_address): + mock_get_guest_ip_address, mechfile_one_entry): """Test 'mech ssh-config' when vm is created but not started.""" + 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) @@ -640,13 +637,14 @@ def test_mech_ssh_config_not_started(mock_getcwd, mock_locate, mock_load_mechfil @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', return_value=MECHFILE_ONE_ENTRY) +@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): + 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} @@ -676,11 +674,13 @@ def test_mech_ssh_config(mock_getcwd, mock_locate, # pylint: disable=too-many-a 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', return_value=MECHFILE_ONE_ENTRY) +@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): + 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 = {} @@ -696,11 +696,12 @@ def test_mech_port_with_nat(mock_locate, mock_load_mechfile, mock_list_host_netw @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', return_value=MECHFILE_ONE_ENTRY) +@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): + 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 = {} @@ -721,11 +722,12 @@ def test_mech_port_with_nat_and_instance(mock_locate, mock_load_mechfile, mock_l 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', return_value=MECHFILE_TWO_ENTRIES) +@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): + 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 = {} @@ -744,10 +746,12 @@ def test_mech_port_with_nat_two_hosts(mock_locate, mock_load_mechfile, mock_list 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', return_value=MECHFILE_ONE_ENTRY) +@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): +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 = {} @@ -935,10 +939,12 @@ def test_mech_add_mechfile_exists_no_name(): a_mech.add(arguments) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.load_mechfile') @patch('os.getcwd') -def test_mech_remove(mock_os_getcwd, mock_load_mechfile, capfd): +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) @@ -952,10 +958,12 @@ def test_mech_remove(mock_os_getcwd, mock_load_mechfile, capfd): assert re.search(r'Removed', out, re.MULTILINE) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.load_mechfile') @patch('os.getcwd') -def test_mech_remove_a_nonexisting_entry(mock_os_getcwd, mock_load_mechfile, capfd): +def test_mech_remove_a_nonexisting_entry(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) @@ -998,11 +1006,13 @@ def test_mech_global_status(mock_list, capfd): 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', return_value=MECHFILE_TWO_ENTRIES) +@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): +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) @@ -1018,12 +1028,14 @@ def test_mech_ps(mock_getcwd, mock_locate, mock_load_mechfile, mock_list_process assert re.search(r'kworker', out, re.MULTILINE) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@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): + 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) diff --git a/mech/test_mech_snapshot.py b/mech/test_mech_snapshot.py index 7335844..595ca92 100644 --- a/mech/test_mech_snapshot.py +++ b/mech/test_mech_snapshot.py @@ -9,34 +9,12 @@ import mech.vmrun -MECHFILE_TWO_ENTRIES = { - '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' - } -} -@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@patch('mech.utils.load_mechfile') @patch('os.getcwd') -def test_mech_snapshot_list_no_mechdir(mock_os_getcwd, mock_load_mechfile, capfd): +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) @@ -57,11 +35,13 @@ def test_mech_snapshot_list_no_mechdir(mock_os_getcwd, mock_load_mechfile, capfd 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', return_value=MECHFILE_TWO_ENTRIES) +@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): + 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) @@ -96,11 +76,13 @@ def test_mech_snapshot_list_no_snapshots(mock_os_getcwd, mock_load_mechfile, 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', return_value=MECHFILE_TWO_ENTRIES) +@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): + 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) @@ -131,11 +113,13 @@ def test_mech_snapshot_list_with_snapshot(mock_os_getcwd, mock_load_mechfile, @patch('mech.vmrun.VMrun.delete_snapshot') @patch('mech.vmrun.VMrun.list_snapshots', return_value=SNAPSHOT_LIST_WITH_SNAPSHOT) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_TWO_ENTRIES) +@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): + 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) @@ -177,20 +161,12 @@ def test_mech_snapshot_delete_snapshot(mock_os_getcwd, mock_load_mechfile, assert re.search(r'Total snapshots: 0', out, re.MULTILINE) -MECHFILE_ONE_ENTRY = { - 'first': { - 'name': - 'first', - 'box': - 'bento/ubuntu-18.04', - 'box_version': - '201912.04.0' - } -} -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@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): +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', @@ -204,10 +180,12 @@ def test_mech_snapshot_list_not_created(mock_locate, mock_load_mechfile, capfd): assert re.search(r'not created', out, re.MULTILINE) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@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): +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', @@ -222,11 +200,12 @@ def test_mech_snapshot_save_not_created(mock_locate, mock_load_mechfile, capfd): @patch('mech.vmrun.VMrun.snapshot', return_value='Snapshot (snap1) on VM (first) taken') -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@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): + 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', @@ -242,11 +221,12 @@ def test_mech_snapshot_save_success(mock_locate, mock_load_mechfile, @patch('mech.vmrun.VMrun.snapshot', return_value=None) -@patch('mech.utils.load_mechfile', return_value=MECHFILE_ONE_ENTRY) +@patch('mech.utils.load_mechfile') @patch('mech.utils.locate', return_value='/tmp/first/some.vmx') def test_mech_snapshot_save_failure(mock_locate, mock_load_mechfile, - mock_snapshot): + mock_snapshot, mechfile_one_entry): """Test 'mech snapshot save' failure.""" + mock_load_mechfile.return_value = mechfile_one_entry global_arguments = {'--debug': False} arguments = { '': 'first', From f3791778f7852438d236d14589f32f139da5e190 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 11 Feb 2020 08:50:16 -0800 Subject: [PATCH 064/116] pylint tests --- mech/conftest.py | 80 ++++++++++++++++++++---- mech/test_mech.py | 121 +++++++------------------------------ mech/test_mech_box.py | 81 +++---------------------- mech/test_mech_snapshot.py | 9 +-- pylintrc | 4 ++ 5 files changed, 107 insertions(+), 188 deletions(-) create mode 100644 pylintrc diff --git a/mech/conftest.py b/mech/conftest.py index 8175b57..b2196d5 100644 --- a/mech/conftest.py +++ b/mech/conftest.py @@ -1,6 +1,6 @@ """Common pytest code.""" - import pytest +import json @pytest.fixture @@ -18,25 +18,79 @@ def mechfile_one_entry(): def mechfile_two_entries(): return { 'first': { - 'name': - 'first', - 'box': - 'bento/ubuntu-18.04', - 'box_version': - '201912.04.0', + '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', + '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' } } + + +@pytest.fixture +def catalog_as_json(): + 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 + } + ] + } + ] + }""" + catalog_as_json = json.loads(catalog) + return catalog_as_json + + +@pytest.fixture +def mech_add_arguments(): + return { + '--force': False, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--box-version': None, + '--checksum': None, + '--checksum-type': None, + '--name': None, + '--box': None, + '': None, + } + + +@pytest.fixture +def mech_box_arguments(): + return { + '--force': False, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--box-version': None, + '--checksum': None, + '--checksum-type': None, + '': None, + } diff --git a/mech/test_mech.py b/mech/test_mech.py index 1b7ae51..193d639 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -1,7 +1,6 @@ """Test the mech cli. """ import subprocess import re -import json from unittest.mock import patch, mock_open from pytest import raises @@ -389,10 +388,10 @@ def test_mech_resume(mock_locate, mock_load_mechfile, '201912.04.0' } } -@patch('mech.utils.load_mechfile', return_value=MECHFILE_BAD_ENTRY) -@patch('mech.utils.locate', return_value=None) -def test_mech_up_without_name(mock_locate, mock_load_mechfile): +@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 = { @@ -416,8 +415,7 @@ def test_mech_up_without_name(mock_locate, mock_load_mechfile): @patch('mech.utils.load_mechfile') -@patch('mech.utils.locate', return_value=None) -def test_mech_up_with_name_not_in_mechfile(mock_locate, mock_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 @@ -480,13 +478,12 @@ def test_mech_up_already_started(mock_locate, mock_load_mechfile, mock_init_box, assert re.search(r'\)started on', out, re.MULTILINE) -@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") @patch('mech.vmrun.VMrun.start', return_value=None) @patch('mech.utils.init_box', return_value='/tmp/first/one.vmx') @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_init_box, - mock_vmrun_start, mock_vmrun_get_ip, capfd, + mock_vmrun_start, capfd, mechfile_one_entry): """Test 'mech up' when issue with starting VM""" mock_load_mechfile.return_value = mechfile_one_entry @@ -616,13 +613,13 @@ def test_mech_ssh_config_not_created(mock_locate, mock_load_mechfile, capfd, assert re.search(r'not created', 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') +@patch('mech.utils.locate') @patch('os.getcwd') def test_mech_ssh_config_not_started(mock_getcwd, mock_locate, mock_load_mechfile, - mock_get_guest_ip_address, mechfile_one_entry): + 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} @@ -768,34 +765,11 @@ def test_mech_port_without_nat(mock_locate, mock_load_mechfile, mock_list_host_n @patch('os.path.exists') @patch('os.getcwd') def test_mech_init(mock_os_getcwd, mock_os_path_exists, - mock_requests_get, capfd): + mock_requests_get, capfd, catalog_as_json): """Test 'mech init' from Hashicorp'.""" mock_os_getcwd.return_value = '/tmp' mock_os_path_exists.return_value = False global_arguments = {'--debug': False} - 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 - } - ] - } - ] - }""" - catalog_as_json = json.loads(catalog) mock_requests_get.return_value.status_code = 200 mock_requests_get.return_value.json.return_value = catalog_as_json @@ -845,25 +819,14 @@ def test_mech_init_mechfile_exists(mock_os_getcwd, mock_os_path_exists): @patch('os.path.exists') @patch('os.getcwd') -def test_mech_init_with_invalid_location(mock_os_getcwd, mock_os_path_exists): +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 = { - '--force': False, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--box-version': None, - '--checksum': None, - '--checksum-type': None, - '--name': None, - '--box': None, - '': 'bento', - } + arguments = mech_add_arguments + arguments[''] = 'bento' with raises(SystemExit, match=r"Provided box name is not valid"): a_mech.init(arguments) @@ -871,70 +834,30 @@ def test_mech_init_with_invalid_location(mock_os_getcwd, mock_os_path_exists): @patch('requests.get') @patch('os.getcwd') def test_mech_add_mechfile_exists(mock_os_getcwd, - mock_requests_get, capfd): + mock_requests_get, capfd, + catalog_as_json, mech_add_arguments): """Test 'mech add' when Mechfile exists'.""" mock_os_getcwd.return_value = '/tmp' - 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 - } - ] - } - ] - }""" - catalog_as_json = json.loads(catalog) 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 = { - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--box-version': None, - '--checksum': None, - '--checksum-type': None, - '': 'second', - '--box': None, - '': 'bento/ubuntu-18.04', - } + 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(): +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 = { - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--box-version': None, - '--checksum': None, - '--checksum-type': None, - '': None, - '--box': None, - '': 'bento/ubuntu-18.04', - } + 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) @@ -961,7 +884,7 @@ def test_mech_remove(mock_os_getcwd, mock_load_mechfile, capfd, @patch('mech.utils.load_mechfile') @patch('os.getcwd') def test_mech_remove_a_nonexisting_entry(mock_os_getcwd, mock_load_mechfile, - capfd, mechfile_one_entry): + mechfile_one_entry): """Test 'mech remove'.""" mock_load_mechfile.return_value = mechfile_one_entry mock_os_getcwd.return_value = '/tmp' diff --git a/mech/test_mech_box.py b/mech/test_mech_box.py index f4faf2d..b4457f7 100644 --- a/mech/test_mech_box.py +++ b/mech/test_mech_box.py @@ -1,6 +1,5 @@ """Unit tests for 'mech box'.""" import re -import json from unittest.mock import patch @@ -66,49 +65,18 @@ def test_mech_box_list_one_box(mock_os_getcwd, capfd): @patch('os.path.exists') @patch('os.getcwd') def test_mech_box_add_new(mock_os_getcwd, mock_os_path_exists, - mock_requests_get, capfd): + 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 = False + mock_os_getcwd.return_value = '/tmp' global_arguments = {'--debug': False} - 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 - } - ] - } - ] - }""" - catalog_as_json = json.loads(catalog) 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 = { - '--force': False, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--box-version': None, - '--checksum': None, - '--checksum-type': None, - '': 'bento/ubuntu-19.10', - } + 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) @@ -118,49 +86,18 @@ def test_mech_box_add_new(mock_os_getcwd, mock_os_path_exists, @patch('os.path.exists') @patch('os.getcwd') def test_mech_box_add_existing(mock_os_getcwd, mock_os_path_exists, - mock_requests_get, capfd): + 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} - 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 - } - ] - } - ] - }""" - catalog_as_json = json.loads(catalog) 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 = { - '--force': False, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--box-version': None, - '--checksum': None, - '--checksum-type': None, - '': 'bento/ubuntu-18.04', - } + 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) diff --git a/mech/test_mech_snapshot.py b/mech/test_mech_snapshot.py index 595ca92..2b32e49 100644 --- a/mech/test_mech_snapshot.py +++ b/mech/test_mech_snapshot.py @@ -175,8 +175,8 @@ def test_mech_snapshot_list_not_created(mock_locate, mock_load_mechfile, capfd, arguments = {'': 'first'} a_mech.list(arguments) out, _ = capfd.readouterr() - mock_locate.assert_called() mock_load_mechfile.assert_called() + mock_locate.assert_called() assert re.search(r'not created', out, re.MULTILINE) @@ -220,12 +220,13 @@ def test_mech_snapshot_save_success(mock_locate, mock_load_mechfile, assert re.search(r' taken', out, re.MULTILINE) -@patch('mech.vmrun.VMrun.snapshot', return_value=None) +#@patch('mech.vmrun.VMrun.snapshot', return_value=None) @patch('mech.utils.load_mechfile') -@patch('mech.utils.locate', return_value='/tmp/first/some.vmx') +@patch('mech.utils.locate') def test_mech_snapshot_save_failure(mock_locate, mock_load_mechfile, - mock_snapshot, mechfile_one_entry): + 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 = { 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 From 74db045473e2daf12d85c20e5d48e3516036635c Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 11 Feb 2020 11:47:19 -0800 Subject: [PATCH 065/116] pylint fixes --- mech/__main__.py | 6 +-- mech/conftest.py | 78 ++++++++++++++++++++++++++------------ mech/mech.py | 16 ++++---- mech/test_mech.py | 36 ++++-------------- mech/test_mech_snapshot.py | 1 - mech/test_utils.py | 57 +++------------------------- 6 files changed, 77 insertions(+), 117 deletions(-) diff --git a/mech/__main__.py b/mech/__main__.py index c8ad1ce..430f7aa 100644 --- a/mech/__main__.py +++ b/mech/__main__.py @@ -21,21 +21,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(): + """Main function.""" try: - import os import sys from . import VERSION from .mech import Mech - from .utils import makedirs - MECH_DIR = os.path.expanduser(os.getcwd() + '/.mech') - makedirs(MECH_DIR) arguments = Mech.docopt(Mech.__doc__, argv=sys.argv[1:], version=VERSION) return Mech(arguments)() except KeyboardInterrupt: diff --git a/mech/conftest.py b/mech/conftest.py index b2196d5..9cbb5f1 100644 --- a/mech/conftest.py +++ b/mech/conftest.py @@ -1,10 +1,11 @@ """Common pytest code.""" -import pytest import json +import pytest @pytest.fixture def mechfile_one_entry(): + """Return one mechfile entry.""" return { 'first': { 'name': 'first', @@ -16,6 +17,7 @@ def mechfile_one_entry(): @pytest.fixture def mechfile_two_entries(): + """Return two mechfile entries.""" return { 'first': { 'name': 'first', @@ -36,36 +38,43 @@ def mechfile_two_entries(): } +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(): - 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 - } - ] - } - ] - }""" - catalog_as_json = json.loads(catalog) - return 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, '--insecure': False, @@ -83,6 +92,23 @@ def mech_add_arguments(): @pytest.fixture def mech_box_arguments(): + """Return the default 'mech box' arguments.""" + return { + '--force': False, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--box-version': None, + '--checksum': None, + '--checksum-type': None, + '': None, + } + + +@pytest.fixture +def mech_init_arguments(): + """Return the default 'mech init' arguments.""" return { '--force': False, '--insecure': False, @@ -92,5 +118,7 @@ def mech_box_arguments(): '--box-version': None, '--checksum': None, '--checksum-type': None, + '--name': None, + '--box': None, '': None, } diff --git a/mech/mech.py b/mech/mech.py index 187ddd1..ddce8f1 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -133,10 +133,10 @@ def config_ssh(self): 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."))) + "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."))) insecure_private_key = os.path.abspath(os.path.join( utils.mech_dir(), "insecure_private_key")) @@ -234,7 +234,7 @@ def add(self, arguments): # pylint: disable=no-self-use utils.add_box(name=None, box=None, location=location, box_version=box_version, force=force, requests_kwargs=requests_kwargs) - def list(self, arguments): # pylint: disable=no-self-use + def list(self, arguments): # pylint: disable=no-self-use,unused-argument """ List all available boxes in the catalog. @@ -479,8 +479,8 @@ def init(self, arguments): # pylint: disable=no-self-use if os.path.exists('Mechfile') and not force: sys.exit(colored.red(textwrap.fill( - "`Mechfile` already exists in this directory. Remove it " - "before running `mech init`."))) + "`Mechfile` already exists in this directory. Remove it " + "before running `mech init`."))) print(colored.green("Initializing mech")) if utils.init_mechfile( @@ -669,7 +669,7 @@ def up(self, arguments): # pylint: disable=invalid-name # allows "mech start" to alias to "mech up" start = up - def global_status(self, arguments): # pylint: disable=no-self-use + def global_status(self, arguments): # pylint: disable=no-self-use,unused-argument """ Outputs info about all VMs running on this computer. diff --git a/mech/test_mech.py b/mech/test_mech.py index 193d639..56f8dfa 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -765,7 +765,8 @@ def test_mech_port_without_nat(mock_locate, mock_load_mechfile, mock_list_host_n @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): + 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 @@ -774,19 +775,8 @@ def test_mech_init(mock_os_getcwd, mock_os_path_exists, mock_requests_get.return_value.json.return_value = catalog_as_json a_mech = mech.mech.Mech(arguments=global_arguments) - arguments = { - '--force': False, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--box-version': None, - '--checksum': None, - '--checksum-type': None, - '--name': None, - '--box': None, - '': 'bento/ubuntu-18.04', - } + arguments = mech_init_arguments + arguments[''] = 'bento/ubuntu-18.04' a_mech.init(arguments) out, _ = capfd.readouterr() assert re.search(r'Loading metadata', out, re.MULTILINE) @@ -794,25 +784,15 @@ def test_mech_init(mock_os_getcwd, mock_os_path_exists, @patch('os.path.exists') @patch('os.getcwd') -def test_mech_init_mechfile_exists(mock_os_getcwd, mock_os_path_exists): +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 = { - '--force': False, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--box-version': None, - '--checksum': None, - '--checksum-type': None, - '--name': None, - '--box': None, - '': 'bento/ubuntu-18.04', - } + arguments = mech_init_arguments + arguments[''] = 'bento/ubuntu-18.04' with raises(SystemExit, match=r".*already exists in this directory.*"): a_mech.init(arguments) diff --git a/mech/test_mech_snapshot.py b/mech/test_mech_snapshot.py index 2b32e49..4a5b253 100644 --- a/mech/test_mech_snapshot.py +++ b/mech/test_mech_snapshot.py @@ -220,7 +220,6 @@ def test_mech_snapshot_save_success(mock_locate, mock_load_mechfile, 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, diff --git a/mech/test_utils.py b/mech/test_utils.py index df90d3f..785face 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -1,6 +1,5 @@ """Test mech utils.""" import os -import json from unittest.mock import patch, mock_open from collections import OrderedDict @@ -285,43 +284,21 @@ def test_build_mechfile_entry_ftp_location_with_other_values(): box='bbb', box_version='ccc') == expected -def test_build_mechfile_entry_file_location_json(): +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 - json_data = """{ - "description": "foo", - "short_description": "foo", - "name": "bento/ubuntu-18.04", - "versions": [ - { - "version": "202002.04.0", - "status": "active", - "description_html": "foo", - "description_markdown": "foo", - "providers": [ - { - "name": "vmware_desktop", - "url": "https://vagrantcloud.com/bento/boxes/ubuntu-18.04/\ -versions/202002.04.0/providers/vmware_desktop.box", - "checksum": null, - "checksum_type": null - } - ] - } - ] - }""" expected = { 'box': 'bento/ubuntu-18.04', - 'box_version': '202002.04.0', + 'box_version': 'aaa', 'name': 'first', 'url': 'https://vagrantcloud.com/bento/boxes/ubuntu-18.04/\ -versions/202002.04.0/providers/vmware_desktop.box' +versions/aaa/providers/vmware_desktop.box' } - a_mock = mock_open(read_data=json_data) + 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 @@ -337,31 +314,9 @@ def test_build_mechfile_entry_file_location_but_file_not_found(): @patch('requests.get') -def test_build_mechfile_entry_file_location_external_good(mock_requests_get): +def test_build_mechfile_entry_file_location_external_good(mock_requests_get, + catalog_as_json): """Test if location talks to Hashicorp.""" - 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 - } - ] - } - ] - }""" - catalog_as_json = json.loads(catalog) expected = { 'box': 'bento/ubuntu-18.04', 'box_version': 'aaa', From 5a8e1b834848c17e03885a38dee92fa4196bc139 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 11 Feb 2020 12:54:56 -0800 Subject: [PATCH 066/116] pylint fixes and additional unittests --- .gitignore | 1 + mech/__main__.py | 10 +++--- mech/command.py | 4 +-- mech/mech.py | 88 ++++++++++++++++++++++------------------------- mech/test_mech.py | 56 ++++++++++++++++++++++++++---- 5 files changed, 99 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index 23528ba..39b1294 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +cov_html/ .coverage *.pyc .idea/ diff --git a/mech/__main__.py b/mech/__main__.py index 430f7aa..b205a16 100644 --- a/mech/__main__.py +++ b/mech/__main__.py @@ -25,15 +25,15 @@ from __future__ import absolute_import +import sys + +from . import VERSION +from .mech import Mech + def main(): """Main function.""" try: - import sys - - from . import VERSION - from .mech import Mech - 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 c3ae923..65ca177 100644 --- a/mech/command.py +++ b/mech/command.py @@ -43,9 +43,9 @@ def cmd_usage(doc): docopt_extras_ref = docopt.extras -def docopt_extras(help, version, options, doc): +def docopt_extras(the_help, version, options, doc): """Show the "Extra" help info.""" - return docopt_extras_ref(help, version, options, cmd_usage(doc)) + return docopt_extras_ref(the_help, version, options, cmd_usage(doc)) def DocoptExit____init__(self, message=''): diff --git a/mech/mech.py b/mech/mech.py index ddce8f1..1ab9a03 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -483,18 +483,15 @@ def init(self, arguments): # pylint: disable=no-self-use "before running `mech init`."))) print(colored.green("Initializing mech")) - if utils.init_mechfile( - location=location, - box=box, - name=name, - box_version=box_version, - requests_kwargs=requests_kwargs): - 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!" - ))) - else: - print(colored.red("Couldn't initialize mech")) + utils.init_mechfile( + location=location, + box=box, + name=name, + box_version=box_version, + requests_kwargs=requests_kwargs) + 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 """ @@ -529,15 +526,13 @@ def add(self, arguments): # pylint: disable=no-self-use print(colored.green("Adding ({}) to the Mechfile.".format(name))) - if utils.add_to_mechfile( - location=location, - box=box, - name=name, - box_version=box_version, - requests_kwargs=requests_kwargs): - print(colored.green("Added to the Mechfile.")) - else: - print(colored.red("Could not add {} to the Mechfile".format(name))) + utils.add_to_mechfile( + location=location, + box=box, + name=name, + box_version=box_version, + requests_kwargs=requests_kwargs) + print(colored.green("Added to the Mechfile.")) def remove(self, arguments): """ @@ -559,10 +554,8 @@ def remove(self, arguments): inst = self.mechfile.get(name, None) if inst: print(colored.green("Removing ({}) from the Mechfile.".format(name))) - if utils.remove_mechfile_entry(name=name): - print(colored.green("Removed from the Mechfile.")) - else: - print(colored.red("Could not remove {} from the Mechfile".format(name))) + utils.remove_mechfile_entry(name=name) + print(colored.green("Removed from the Mechfile.")) else: sys.exit(colored.red("There is no instance called ({}) in the Mechfile.".format(name))) @@ -1177,30 +1170,33 @@ def reload(self, arguments): for instance in instances: inst = MechInstance(instance) - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + if inst.created: + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) - print(colored.blue("Reloading machine...")) - started = vmrun.reset() - if started is None: - print(colored.red("VM not restarted")) - else: - 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))) + print(colored.blue("Reloading machine...")) + started = vmrun.reset() + if started is None: + print(colored.red("VM not restarted")) else: - if started: - print(colored.green("VM ({}) started on an unknown IP " - "address".format(instance))) + 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: - print(colored.yellow("VM ({}) already was started " - "on an unknown IP address".format(instance))) + 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("VM not created.") def port(self, arguments): """ diff --git a/mech/test_mech.py b/mech/test_mech.py index 56f8dfa..6ca8b65 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -58,8 +58,8 @@ def test_mech_list_with_one_and_debug(mock_locate, mock_load_mechfile, capfd, @patch('mech.utils.load_mechfile') @patch('mech.utils.locate', return_value=None) -def test_mech_list_with_two(mock_locate, mock_load_mechfile, capfd, - mechfile_two_entries): +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} @@ -73,6 +73,25 @@ def test_mech_list_with_two(mock_locate, mock_load_mechfile, capfd, 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.utils.load_mechfile') @patch('mech.utils.locate', return_value=None) def test_mech_status_with_two_not_created(mock_locate, mock_load_mechfile, capfd, @@ -146,10 +165,10 @@ def test_mech_destroy(mock_locate, mock_load_mechfile, @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_stop(mock_locate, mock_load_mechfile, +def test_mech_down(mock_locate, mock_load_mechfile, mock_vmrun_stop, mock_installed_tools, capfd, mechfile_two_entries): - """Test 'mech stop' powered on.""" + """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) @@ -336,8 +355,8 @@ def test_mech_ssh(mock_locate, mock_load_mechfile, @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_stop, capfd, mechfile_two_entries): - """Test 'mech stop' powered on.""" + 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) @@ -349,10 +368,33 @@ def test_mech_pause(mock_locate, mock_load_mechfile, out, _ = capfd.readouterr() mock_locate.assert_called() mock_load_mechfile.assert_called() - mock_vmrun_stop.assert_called() + mock_vmrun_pause.assert_called() assert re.search(r'Paused', 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.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') From ba492727331362acc2e83417b65f6a076b63de56 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 11 Feb 2020 13:33:58 -0800 Subject: [PATCH 067/116] add unittest for update_vmx --- mech/test_utils.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/mech/test_utils.py b/mech/test_utils.py index 785face..3b2bef9 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -1,5 +1,6 @@ """Test mech utils.""" import os +import re from unittest.mock import patch, mock_open from collections import OrderedDict @@ -236,6 +237,57 @@ def test_parse_vmx(): a_mock.assert_called() +@patch('mech.utils.parse_vmx', return_value={}) +def test_update_vmx_empty(mock_parse_vmx, 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 +""" + 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 = _get_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', return_value={'ethernet0.present': 'true'}) +def test_update_vmx_with_a_network_entry(mock_parse_vmx, capfd): + """Test update_vmx.""" + 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', return_value={'ethernet0.present': 'true'}) +def test_update_vmx_with_cpu_and_memory(mock_parse_vmx, capfd): + """Test update_vmx.""" + 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 = _get_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) == {} From dece82eda54ce61a8eea23e8f3421244c1a1260d Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 11 Feb 2020 13:51:17 -0800 Subject: [PATCH 068/116] run pytest with multiple cpus --- CONTRIBUTING.md | 2 +- pytest.ini | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 267b65c..f20168d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ source venv/bin/activate python setup.py install # if doing development -pip install flake8 pytest pytest_mock mock pytest-cov pylint +pip install flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist # also optional pip install autopep8 diff --git a/pytest.ini b/pytest.ini index 50bd45c..7e7ef35 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,6 @@ [pytest] +addopts = -n4 + filterwarnings = ignore::DeprecationWarning From 2a074d4d8346d57ab60dd7cae338632c9e3cabc6 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 11 Feb 2020 13:54:45 -0800 Subject: [PATCH 069/116] update info about seeing slowest pytest tests --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f20168d..7ccbc60 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,9 @@ pytest --cov mech # to see what lines are not covered pytest --cov-report term-missing --cov mech +# to see the slowest tests +pytest --durations=0 + # for testing/validation, we have also some integration tests cd tests/int ./simple.bats From 50d896bd92a814766c494cf5443cf6d29ba47227 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 11 Feb 2020 15:08:01 -0800 Subject: [PATCH 070/116] add more unittests to test_utils --- mech/test_utils.py | 206 ++++++++++++++++++++++++++++++++++++++++++++- mech/utils.py | 16 ++-- 2 files changed, 211 insertions(+), 11 deletions(-) diff --git a/mech/test_utils.py b/mech/test_utils.py index 3b2bef9..986ab21 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -237,7 +237,7 @@ def test_parse_vmx(): a_mock.assert_called() -@patch('mech.utils.parse_vmx', return_value={}) +@patch('mech.utils.parse_vmx') def test_update_vmx_empty(mock_parse_vmx, capfd): """Test update_vmx.""" expected_vmx = """ethernet0.addresstype = generated @@ -250,6 +250,7 @@ def test_update_vmx_empty(mock_parse_vmx, capfd): 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') @@ -260,9 +261,10 @@ def test_update_vmx_empty(mock_parse_vmx, capfd): assert re.search(r'Added network interface to vmx file', out, re.MULTILINE) -@patch('mech.utils.parse_vmx', return_value={'ethernet0.present': 'true'}) +@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') @@ -271,9 +273,10 @@ def test_update_vmx_with_a_network_entry(mock_parse_vmx, capfd): assert out == '' -@patch('mech.utils.parse_vmx', return_value={'ethernet0.present': 'true'}) +@patch('mech.utils.parse_vmx') def test_update_vmx_with_cpu_and_memory(mock_parse_vmx, capfd): """Test update_vmx.""" + mock_parse_vmx.return_value = {'ethernet0.present': 'true'} expected_vmx = '''ethernet0.present = true numvcpus = "3" memsize = "1025" @@ -388,3 +391,200 @@ 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, vmx=None, user=None, password=None, + provision_config=None, show=None) + + +def test_provision_no_vmx(): + """Test provisioning.""" + with raises(SystemExit, match=r"Need to provide vmx.*"): + mech.utils.provision(instance='first', vmx=None, user=None, password=None, + provision_config=None, show=None) + + +@patch('mech.vmrun.VMrun.installed_tools') +def test_provision_no_vmare_tools(mock_installed_tools): + """Test provisioning.""" + mock_installed_tools.return_value = None + with raises(SystemExit, match=r"Cannot provision if VMware Tools are not installed"): + mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', + user='vagrant', password='vagrant', provision_config=None, + 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 + mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', user='vagrant', + password='vagrant', provision_config=[], 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", + }, + ] + mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', user='vagrant', + password='vagrant', provision_config=config, 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", + }, + ] + mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', user='vagrant', + password='vagrant', provision_config=config, 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", + }, + ] + mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', + user='vagrant', password='vagrant', + provision_config=config, 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" + }, + ] + mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', + user='vagrant', password='vagrant', + provision_config=config, 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", + ], + }, + ] + mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', + user='vagrant', password='vagrant', + provision_config=config, 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", + ], + }, + ] + mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', + user='vagrant', password='vagrant', + provision_config=config, 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", + }, + ] + mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', + user='vagrant', password='vagrant', + provision_config=config, show=None) + out, _ = capfd.readouterr() + mock_installed_tools.assert_called() + assert re.search(r'Not Provisioned', out, re.MULTILINE) diff --git a/mech/utils.py b/mech/utils.py index 1687735..1613e35 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -595,21 +595,18 @@ def provision(instance, vmx, user, password, provision_config, show): """ - if instance == '': - print(colored.red("Need to provide an instance to provision().")) - return + if not instance or instance == '': + sys.exit(colored.red("Need to provide an instance to provision().")) if not vmx or not user or not password: - print(colored.red("Need provide vmx/user/password to provision().")) - return + sys.exit(colored.red("Need to provide vmx/user/password to provision().")) print(colored.green('Provisioning instance:{}'.format(instance))) vmrun = VMrun(vmx, user, password) # cannot run provisioning if vmware tools are not installed if not vmrun.installed_tools(): - print(colored.red("Cannot provision if VMware Tools are not installed.")) - return + sys.exit(colored.red("Cannot provision if VMware Tools are not installed.")) provisioned = 0 if provision_config: @@ -619,7 +616,7 @@ def provision(instance, vmx, user, password, provision_config, show): source = pro.get('source') destination = pro.get('destination') if show: - print(colored.green(" instance:{} provision_type:{} source:{} " + print(colored.green("instance:{} provision_type:{} source:{} " "destination:{}".format(instance, provision_type, source, destination))) else: @@ -670,12 +667,14 @@ def provision_shell(virtual_machine, inline, path, args=None): tmp_path = virtual_machine.create_tempfile_in_guest() LOGGER.debug('inline:%s path:%s args:%s tmp_path:%s', inline, 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): print(colored.blue("Configuring script {}...".format(path))) if virtual_machine.copy_file_from_host_to_guest(path, tmp_path) is None: + print(colored.red("Warning: Could not copy file to guest.")) return else: if path: @@ -709,6 +708,7 @@ def provision_shell(virtual_machine, inline, path, args=None): print(colored.blue("Configuring environment...")) if virtual_machine.run_script_in_guest('/bin/sh', "chmod +x '{}'".format(tmp_path)) is None: + print(colored.red("Warning: Could not configure script in the environment.")) return print(colored.blue("Executing program...")) From d1ba7b94d9f5b574494bbd05b7e17e47a216a5d3 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 11 Feb 2020 16:15:09 -0800 Subject: [PATCH 071/116] unittest additions --- mech/mech.py | 6 +- mech/test_mech.py | 193 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 193 insertions(+), 6 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 1ab9a03..6de9697 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -776,13 +776,11 @@ def destroy(self, arguments): vmrun.delete_vm() if os.path.exists(inst.path): shutil.rmtree(inst.path) - else: - LOGGER.debug("%s was not found.", inst.path) print("Deleted") else: - print(colored.red("Deletion aborted")) + print(colored.red("Delete aborted.")) else: - print(colored.red("The box ({}) hasn't been initialized.".format(instance))) + print(colored.red("VM ({}) not created.".format(instance))) def down(self, arguments): """ diff --git a/mech/test_mech.py b/mech/test_mech.py index 6ca8b65..377c8bd 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -92,6 +92,25 @@ def test_mech_list_powered_on(mock_locate, mock_load_mechfile, 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, @@ -161,6 +180,25 @@ def test_mech_destroy(mock_locate, mock_load_mechfile, 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') @@ -185,6 +223,25 @@ def test_mech_down(mock_locate, mock_load_mechfile, assert re.search(r'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') @@ -205,6 +262,44 @@ def test_mech_ip(mock_locate, mock_load_mechfile, 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", @@ -282,6 +377,23 @@ def test_mech_provision_file(mock_locate, mock_load_mechfile, 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) @@ -324,6 +436,24 @@ def test_mech_suspend(mock_locate, mock_load_mechfile, assert re.search(r'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('mech.vmrun.VMrun.installed_tools', return_value='running') @patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.130") @patch('subprocess.call', return_value='00:03:30 up 2 min, load average: 0.00, 0.00, 0.00') @@ -340,7 +470,7 @@ def test_mech_ssh(mock_locate, mock_load_mechfile, '': 'first', '--plain': None, '--command': 'uptime', - '': None, + '': 'blah', } a_mech.ssh(arguments) # Note: Could not figure out how to capture output from subprocess.call. @@ -351,6 +481,28 @@ def test_mech_ssh(mock_locate, mock_load_mechfile, mock_get_ip.assert_called() +@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') @@ -372,6 +524,25 @@ def test_mech_pause(mock_locate, mock_load_mechfile, 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' powered on.""" + 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.get_guest_ip_address', return_value='192.168.1.101') @patch('mech.vmrun.VMrun.reset', return_value=True) @patch('mech.utils.load_mechfile') @@ -395,6 +566,24 @@ def test_mech_reload(mock_locate, mock_load_mechfile, 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.get_guest_ip_address', return_value='192.168.1.101') @patch('mech.vmrun.VMrun.unpause', return_value=True) @patch('mech.utils.load_mechfile') @@ -646,7 +835,7 @@ def test_mech_ssh_config_not_created(mock_locate, mock_load_mechfile, capfd, global_arguments = {'--debug': False} a_mech = mech.mech.Mech(arguments=global_arguments) arguments = { - '': 'first', + '': None, } a_mech.ssh_config(arguments) out, _ = capfd.readouterr() From abb52820239f39709c6eef9a46957aa0173a3ce8 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 11 Feb 2020 17:00:44 -0800 Subject: [PATCH 072/116] add ability to run int tests from pytest --- CONTRIBUTING.md | 19 +++++--- mech/test_mech_int.py | 61 +++++++++++++++++++++++++ pytest.ini | 5 +- tests/int/add_and_remove_instances.bats | 4 +- 4 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 mech/test_mech_int.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ccbc60..b11ca7c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,9 @@ 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 @@ -37,7 +40,7 @@ flake8 --install-hook git # see https://github.com/bats-core/bats-core brew install bats-core -# for testing: +# for running unit tests: pytest # for code coverage @@ -50,10 +53,12 @@ pytest --cov-report term-missing --cov mech pytest --durations=0 # for testing/validation, we have also some integration tests -cd tests/int -./simple.bats -./two_ubuntu.bats - -# consider installing/using direnv (there is a .envrc in this repo) -# may need to run "direnv allow" +# 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" + +# Or, just one run int test like this (with verbose and show local variables): +pytest -m"int" -k"provision" -v -l ``` diff --git a/mech/test_mech_int.py b/mech/test_mech_int.py new file mode 100644 index 0000000..17f5da7 --- /dev/null +++ b/mech/test_mech_int.py @@ -0,0 +1,61 @@ +"""Test the mech cli. """ +import subprocess + +import pytest + + +@pytest.mark.int +def test_int_add_and_remove_instances(): + """Test add and remove instances integration tests.""" + return_value, out = subprocess.getstatusoutput( + 'cd tests/int && ./add_and_remove_instances.bats') + assert return_value == 0 + + +@pytest.mark.int +def test_int_quick(): + """Test quick integration tests.""" + return_value, out = subprocess.getstatusoutput('cd tests/int && ./quick.bats') + assert return_value == 0 + + +@pytest.mark.int +def test_int_no_mechfile(): + """Test no_mechfile integration tests.""" + return_value, out = subprocess.getstatusoutput('cd tests/int && ./no_mechfile.bats') + assert return_value == 0 + + +@pytest.mark.int +def test_int_simple(): + """Test simple integration tests.""" + return_value, out = subprocess.getstatusoutput('cd tests/int && ./simple.bats') + assert return_value == 0 + + +@pytest.mark.int +def test_int_two_ubuntu(): + """Test two_ubuntu tests.""" + return_value, out = subprocess.getstatusoutput('cd tests/int && ./two_ubuntu.bats') + assert return_value == 0 + + +@pytest.mark.int +def test_int_init_from_file(): + """Test init_from_file tests.""" + return_value, out = subprocess.getstatusoutput('cd tests/int && ./init_from_file.bats') + assert return_value == 0 + + +@pytest.mark.int +def test_int_provision(): + """Test provision tests.""" + return_value, out = subprocess.getstatusoutput('cd tests/int && ./provision.bats') + assert return_value == 0 + + +@pytest.mark.int +def test_int_shared_folders(): + """Test shared_folders tests.""" + return_value, out = subprocess.getstatusoutput('cd tests/int && ./shared_folders.bats') + assert return_value == 0 diff --git a/pytest.ini b/pytest.ini index 7e7ef35..faa439d 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,9 @@ [pytest] -addopts = -n4 +addopts = -n4 -m "not int" filterwarnings = ignore::DeprecationWarning + +markers = + int: marks tests as slow (deselect with '-m "not slow"') diff --git a/tests/int/add_and_remove_instances.bats b/tests/int/add_and_remove_instances.bats index d5263b0..4108524 100755 --- a/tests/int/add_and_remove_instances.bats +++ b/tests/int/add_and_remove_instances.bats @@ -1,9 +1,9 @@ #!/usr/bin/env bats # -# shared_folders.bats - run shared_folders tests +# add_and_remove_instances.bats - add and remove instance tests # # Note: must be run from this directory -# like this: ./shared_folders.bats +# like this: ./add_and_remove_instances.bats @test "add and remove instances tests" { if ! [ -d add_and_remove_instances ]; then From 7a614549456c2f63cb8e16509cd70e8ac3f2862b Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 11 Feb 2020 17:09:36 -0800 Subject: [PATCH 073/116] show how to run all tests from pytest --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b11ca7c..f98a3ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,6 +59,9 @@ pytest --durations=0 # Or, you can run them from the main project directory like this: pytest -m"int" +# To run all tests: +pytest -m"int or not int" + # Or, just one run int test like this (with verbose and show local variables): pytest -m"int" -k"provision" -v -l ``` From bf07d3fe091d0950ba86eceeb657e1ffd38abce7 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 11 Feb 2020 17:28:46 -0800 Subject: [PATCH 074/116] more comments about running tests --- CONTRIBUTING.md | 9 ++++++++- pytest.ini | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f98a3ae..35dbc95 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,10 +49,17 @@ pytest --cov mech # to see what lines are not covered pytest --cov-report term-missing --cov mech -# to see the slowest tests +# 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" + +# to see the slowest unit tests pytest --durations=0 # 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. diff --git a/pytest.ini b/pytest.ini index faa439d..f9f59c2 100644 --- a/pytest.ini +++ b/pytest.ini @@ -6,4 +6,4 @@ filterwarnings = ignore::DeprecationWarning markers = - int: marks tests as slow (deselect with '-m "not slow"') + int: marks tests as slow (deselect with '-m "not int"') From ba9e2628bfdcc898096c8a7e7efbdbc42ca1cf1a Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 12 Feb 2020 08:51:27 -0800 Subject: [PATCH 075/116] after cleaning some .mech dirs, found unit test was using them and had to mock the write of the insecure_private_key file --- mech/conftest.py | 20 ++++++++++++++++++++ mech/test_mech.py | 23 +++++++++++++++-------- mech/test_utils.py | 29 ++++++++--------------------- 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/mech/conftest.py b/mech/conftest.py index 9cbb5f1..44350f8 100644 --- a/mech/conftest.py +++ b/mech/conftest.py @@ -122,3 +122,23 @@ def mech_init_arguments(): '--box': 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 + + +@pytest.fixture +def helpers(): + return Helpers diff --git a/mech/test_mech.py b/mech/test_mech.py index 377c8bd..b45e106 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -1,4 +1,5 @@ """Test the mech cli. """ +import os import subprocess import re @@ -454,6 +455,7 @@ def test_mech_suspend_not_created(mock_locate, mock_load_mechfile, 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.1.130") @patch('subprocess.call', return_value='00:03:30 up 2 min, load average: 0.00, 0.00, 0.00') @@ -461,7 +463,7 @@ def test_mech_suspend_not_created(mock_locate, mock_load_mechfile, @patch('mech.utils.locate', return_value='/tmp/first/some.vmx') def test_mech_ssh(mock_locate, mock_load_mechfile, mock_subprocess_call, mock_get_ip, mock_installed_tools, - mechfile_two_entries): + mock_chmod, mechfile_two_entries): """Test 'mech ssh'""" mock_load_mechfile.return_value = mechfile_two_entries global_arguments = {'--debug': False} @@ -472,13 +474,18 @@ def test_mech_ssh(mock_locate, mock_load_mechfile, '--command': 'uptime', '': 'blah', } - 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() - mock_subprocess_call.assert_called() - mock_installed_tools.assert_called() - mock_get_ip.assert_called() + 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.ssh(arguments) + # Note: Could not figure out how to capture output from subprocess.call. + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_subprocess_call.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') diff --git a/mech/test_utils.py b/mech/test_utils.py index 986ab21..282a9d1 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -37,20 +37,7 @@ def test_save_mechfile_empty_config(): a_mock.return_value.write.assert_called_once_with('{}') -def _get_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 - - -def test_save_mechfile_one(): +def test_save_mechfile_one(helpers): """Test save_mechfile with one entry.""" first_dict = { 'first': { @@ -78,10 +65,10 @@ def test_save_mechfile_one(): 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 == _get_data_written(a_mock) + assert first_json == helpers.get_mock_data_written(a_mock) -def test_save_mechfile_two(): +def test_save_mechfile_two(helpers): """Test save_mechfile with two entries.""" two_dict = { 'first': { @@ -126,7 +113,7 @@ def test_save_mechfile_two(): 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 == _get_data_written(a_mock) + assert two_json == helpers.get_mock_data_written(a_mock) def test_tar_cmd(): @@ -238,7 +225,7 @@ def test_parse_vmx(): @patch('mech.utils.parse_vmx') -def test_update_vmx_empty(mock_parse_vmx, capfd): +def test_update_vmx_empty(mock_parse_vmx, helpers, capfd): """Test update_vmx.""" expected_vmx = """ethernet0.addresstype = generated ethernet0.bsdname = en0 @@ -255,7 +242,7 @@ def test_update_vmx_empty(mock_parse_vmx, capfd): with patch('builtins.open', a_mock, create=True): mech.utils.update_vmx('/tmp/first/one.vmx') a_mock.assert_called() - got = _get_data_written(a_mock) + 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) @@ -274,7 +261,7 @@ def test_update_vmx_with_a_network_entry(mock_parse_vmx, capfd): @patch('mech.utils.parse_vmx') -def test_update_vmx_with_cpu_and_memory(mock_parse_vmx, capfd): +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 @@ -285,7 +272,7 @@ def test_update_vmx_with_cpu_and_memory(mock_parse_vmx, capfd): 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 = _get_data_written(a_mock) + got = helpers.get_mock_data_written(a_mock) assert expected_vmx == got out, _ = capfd.readouterr() assert out == '' From 1fd9b689dcab1c34aa8d75765c23e604df0078a8 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 12 Feb 2020 09:56:03 -0800 Subject: [PATCH 076/116] add more unittests --- mech/mech.py | 62 ++++----- mech/test_mech.py | 337 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 367 insertions(+), 32 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 6de9697..b3f68a5 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -930,7 +930,7 @@ def resume(self, arguments): print(colored.yellow("VM ({}) already was started on an " "unknown IP address".format(instance))) else: - print(colored.red("Need to start VM first")) + print(colored.red("VM not created")) def suspend(self, arguments): """ @@ -1044,7 +1044,7 @@ def scp(self, arguments): # pylint: disable=no-self-use Options: -h, --help Print this help """ - extra = arguments[''] + extra = arguments[''] src = arguments[''] dst = arguments[''] @@ -1052,8 +1052,7 @@ def scp(self, arguments): # pylint: disable=no-self-use src_instance, src_is_host, src = src.partition(':') if dst_is_host and src_is_host: - print(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 = dst_instance else: @@ -1065,32 +1064,35 @@ def scp(self, arguments): # pylint: disable=no-self-use inst = MechInstance(instance) - config_ssh = inst.config_ssh() - temp_file = tempfile.NamedTemporaryFile(delete=False) - - try: - temp_file.write(utils.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 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(temp_file.name) + if inst.created: + config_ssh = inst.config_ssh() + temp_file = tempfile.NamedTemporaryFile(delete=False) + + try: + temp_file.write(utils.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 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(temp_file.name) + else: + print(colored.red('VM not created.')) def ip(self, arguments): # pylint: disable=invalid-name,no-self-use """ diff --git a/mech/test_mech.py b/mech/test_mech.py index b45e106..17ab137 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -150,6 +150,48 @@ def test_mech_status_powered_on(mock_locate, mock_load_mechfile, 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('os.path.exists', return_value=True) @patch('shutil.rmtree') @patch('mech.vmrun.VMrun.delete_vm') @@ -224,6 +266,30 @@ def test_mech_down(mock_locate, mock_load_mechfile, 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, @@ -616,6 +682,117 @@ def test_mech_resume(mock_locate, mock_load_mechfile, 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 current', 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 current', out, re.MULTILINE) + assert re.search(r'started', out, re.MULTILINE) + + MECHFILE_BAD_ENTRY = { '': { 'name': @@ -680,7 +857,7 @@ def test_mech_up_with_name_not_in_mechfile(mock_load_mechfile, @patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") -@patch('mech.vmrun.VMrun.start', return_value=True) +@patch('mech.vmrun.VMrun.start', return_value='') @patch('mech.utils.init_box', return_value='/tmp/first/one.vmx') @patch('mech.utils.load_mechfile') @patch('mech.utils.locate', return_value='/tmp/first/one.vmx') @@ -713,7 +890,82 @@ def test_mech_up_already_started(mock_locate, mock_load_mechfile, mock_init_box, mock_vmrun_start.assert_called() mock_vmrun_get_ip.assert_called() out, _ = capfd.readouterr() - assert re.search(r'\)started on', out, re.MULTILINE) + assert re.search(r'was already started on', out, re.MULTILINE) + + +@patch('mech.vmrun.VMrun.get_guest_ip_address', return_value='') +@patch('mech.vmrun.VMrun.start', return_value='') +@patch('mech.utils.init_box', return_value='/tmp/first/one.vmx') +@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_init_box, + 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, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--checksum': None, + '--checksum-type': None, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '': None, + } + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_init_box.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.init_box', return_value='/tmp/first/one.vmx') +@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_init_box, + 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, + '--insecure': False, + '--cacert': None, + '--capath': None, + '--cert': None, + '--checksum': None, + '--checksum-type': None, + '--no-cache': None, + '--memsize': None, + '--numvcpus': None, + '': None, + } + a_mech.up(arguments) + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_init_box.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) @@ -1189,3 +1441,84 @@ def test_mech_ps_not_started_vm(mock_getcwd, mock_locate, mock_locate.assert_called() mock_load_mechfile.assert_called() assert re.search(r'not created', out, re.MULTILINE) + + +@patch('subprocess.call', 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_call, + 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) + # Note: Could not figure out how to capture output from subprocess.call. + mock_locate.assert_called() + mock_load_mechfile.assert_called() + mock_subprocess_call.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.call', 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_call, + 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_call.assert_called() + mock_installed_tools.assert_called() + mock_get_ip.assert_called() + mock_chmod.assert_called() + a_mock.assert_called_once_with(filename, 'w') + + +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) From 945e76c818f72d82f76441c2119074248e2f6d51 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 12 Feb 2020 11:33:23 -0800 Subject: [PATCH 077/116] add test_tar unittest --- mech/test_utils.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/mech/test_utils.py b/mech/test_utils.py index 282a9d1..26946c2 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -2,7 +2,7 @@ import os import re -from unittest.mock import patch, mock_open +from unittest.mock import patch, mock_open, MagicMock from collections import OrderedDict from pytest import raises @@ -117,10 +117,23 @@ def test_save_mechfile_two(helpers): def test_tar_cmd(): - """Note: not really a unit test per se, as it calls out.""" + """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({}) From 6c2d077fffc8da253801ab6fb15b09fee62f8de2 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 12 Feb 2020 14:08:00 -0800 Subject: [PATCH 078/116] add no-nat (eg bridged) networking --- mech/mech.py | 27 ++++++++++++++++++--------- mech/test_mech.py | 8 ++++++++ mech/utils.py | 9 +++++---- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index b3f68a5..d110003 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -210,8 +210,8 @@ def add(self, arguments): # pylint: disable=no-self-use 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 + 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: @@ -447,8 +447,8 @@ def init(self, arguments): # pylint: disable=no-self-use 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 + 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: @@ -568,7 +568,12 @@ def up(self, arguments): # pylint: disable=invalid-name Usage: mech up [options] [] - Note: If no instance is specified, all instances will be started. + 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. Options: --gui Start GUI @@ -583,6 +588,7 @@ def up(self, arguments): # pylint: disable=invalid-name --no-cache Do not save the downloaded box --memsize 1024 Specify the size of memory for VM --numvcpus 1 Specify the number of vcpus for VM + --no-nat Do not use NAT network (i.e., bridged) -h, --help Print this help """ gui = arguments['--gui'] @@ -591,14 +597,16 @@ def up(self, arguments): # pylint: disable=invalid-name save = not arguments['--no-cache'] requests_kwargs = utils.get_requests_kwargs(arguments) - numvcpus = arguments['--numvcpus'] memsize = arguments['--memsize'] + numvcpus = arguments['--numvcpus'] + no_nat = arguments['--no-nat'] instance_name = arguments[''] LOGGER.debug('gui:%s disable_shared_folders:%s disable_provisioning:%s ' - 'save:%s numvcpus:%s memsize:%s', gui, disable_shared_folders, - disable_provisioning, save, numvcpus, memsize) + 'save:%s numvcpus:%s memsize:%s no_nat:%s', gui, + disable_shared_folders, disable_provisioning, save, + numvcpus, memsize, no_nat) if instance_name: # single instance @@ -623,7 +631,8 @@ def up(self, arguments): # pylint: disable=invalid-name requests_kwargs=requests_kwargs, save=save, numvcpus=numvcpus, - memsize=memsize) + memsize=memsize, + no_nat=no_nat) if vmx: inst.vmx = vmx inst.created = True diff --git a/mech/test_mech.py b/mech/test_mech.py index 17ab137..c2e56fa 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -823,6 +823,7 @@ def test_mech_up_without_name(mock_load_mechfile): '--no-cache': None, '--memsize': None, '--numvcpus': None, + '--no-nat': None, '': '', } with raises(AttributeError, match=r"Must provide a name for the instance."): @@ -850,6 +851,7 @@ def test_mech_up_with_name_not_in_mechfile(mock_load_mechfile, '--no-cache': None, '--memsize': None, '--numvcpus': None, + '--no-nat': None, '': 'notfirst', } with raises(SystemExit, match=r" was not found in the Mechfile"): @@ -881,6 +883,7 @@ def test_mech_up_already_started(mock_locate, mock_load_mechfile, mock_init_box, '--no-cache': None, '--memsize': None, '--numvcpus': None, + '--no-nat': None, '': None, } a_mech.up(arguments) @@ -919,6 +922,7 @@ def test_mech_up_already_started_but_could_not_get_ip(mock_locate, mock_load_mec '--no-cache': None, '--memsize': None, '--numvcpus': None, + '--no-nat': None, '': None, } a_mech.up(arguments) @@ -956,6 +960,7 @@ def test_mech_up_already_started_but_on_unknnown_ip(mock_locate, mock_load_mechf '--no-cache': None, '--memsize': None, '--numvcpus': None, + '--no-nat': None, '': None, } a_mech.up(arguments) @@ -992,6 +997,7 @@ def test_mech_up_problem(mock_locate, mock_load_mechfile, mock_init_box, '--no-cache': None, '--memsize': None, '--numvcpus': None, + '--no-nat': None, '': None, } a_mech.up(arguments) @@ -1029,6 +1035,7 @@ def test_mech_up_with_provisioning(mock_locate, mock_load_mechfile, mock_init_bo '--no-cache': None, '--memsize': None, '--numvcpus': None, + '--no-nat': None, '': None, } a_mech.up(arguments) @@ -1071,6 +1078,7 @@ def test_mech_up_wth_shared_folders(mock_locate, mock_load_mechfile, mock_init_b '--no-cache': None, '--memsize': None, '--numvcpus': None, + '--no-nat': None, '': None, } a_mech.up(arguments) diff --git a/mech/utils.py b/mech/utils.py index 1613e35..3fd99c3 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -145,7 +145,7 @@ def parse_vmx(path): return vmx -def update_vmx(path, numvcpus=None, memsize=None): +def update_vmx(path, numvcpus=None, memsize=None, no_nat=False): """Update the virtual machine configuration (.vmx) file with desired settings. """ @@ -163,7 +163,8 @@ def update_vmx(path, numvcpus=None, memsize=None): 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" @@ -323,7 +324,7 @@ def tar_cmd(*args, **kwargs): def init_box(name, box=None, box_version=None, location=None, force=False, save=True, instance_path=None, requests_kwargs=None, numvcpus=None, - memsize=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. @@ -376,7 +377,7 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= print(colored.red("Cannot locate a VMX file")) sys.exit(1) - update_vmx(vmx, numvcpus=numvcpus, memsize=memsize) + update_vmx(vmx, numvcpus=numvcpus, memsize=memsize, no_nat=no_nat) return vmx From 3c408663b92262c219b9cedd29c9883ab4b4ea58 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Wed, 12 Feb 2020 14:33:34 -0800 Subject: [PATCH 079/116] args should use dash not underbar --- mech/mech.py | 6 +++--- mech/test_mech.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index d110003..706a444 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -1000,7 +1000,7 @@ 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 @@ -1008,7 +1008,7 @@ def ssh(self, arguments): # pylint: disable=no-self-use -h, --help Print this help """ plain = arguments['--plain'] - extra = arguments[''] + extra = arguments[''] command = arguments['--command'] instance = arguments[''] @@ -1048,7 +1048,7 @@ 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 diff --git a/mech/test_mech.py b/mech/test_mech.py index c2e56fa..3113016 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -538,7 +538,7 @@ def test_mech_ssh(mock_locate, mock_load_mechfile, '': 'first', '--plain': None, '--command': 'uptime', - '': 'blah', + '': 'blah', } filename = os.path.join(mech.utils.mech_dir(), 'insecure_private_key') a_mock = mock_open() @@ -566,7 +566,7 @@ def test_mech_ssh_not_created(mock_locate, mock_load_mechfile, '': 'first', '--plain': None, '--command': 'uptime', - '': None, + '': None, } a_mech.ssh(arguments) # Note: Could not figure out how to capture output from subprocess.call. From 8a24afb3ca1d30c0e3d52bd0db0675f17d5e7c38 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Thu, 13 Feb 2020 09:50:43 -0800 Subject: [PATCH 080/116] add zsh completions --- README.md | 26 +++++ _mech | 309 +++++++++++++++++++++++++++++++++++++++++++++++++++ mech/mech.py | 66 +++++------ 3 files changed, 368 insertions(+), 33 deletions(-) create mode 100644 _mech diff --git a/README.md b/README.md index ac2e293..386ddcc 100644 --- a/README.md +++ b/README.md @@ -122,3 +122,29 @@ 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. diff --git a/_mech b/_mech new file mode 100644 index 0000000..bbce629 --- /dev/null +++ b/_mech @@ -0,0 +1,309 @@ +#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 \ + "--force[Force a hard stop]" \ + "-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 \ + "--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]" \ + "--cacert=[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)]" \ + "-force[Force a hard stop]" \ + "--force[Force a hard stop]" \ + "-h[Show help information]" \ + "--help[Show help information]" \ + "--insecure[Do not validate SSL certificates]" \ + "--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 \ + "-g[PORT Output the host port that maps to the given guest port]" \ + "-h[Show help information]" \ + "--help[Show help information]" \ + "--machine-readable[Display machine-readable output]" \ + '::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 \ + "--cacert=[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)]" \ + "--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]" \ + "--insecure[Do not validate SSL certificates]" \ + "--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/mech/mech.py b/mech/mech.py index 706a444..4dd2bd0 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -215,14 +215,14 @@ def add(self, arguments): # pylint: disable=no-self-use HashiCorp account/box (ex: 'bento/ubuntu-18.04'). Options: - -f, --force Overwrite an existing box if it exists - --insecure Do not validate SSL certificates + --box-version VERSION Constrain version of the added box --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) + --insecure Do not validate SSL certificates + -f, --force Overwrite an existing box if it exists -h, --help Print this help """ @@ -387,26 +387,26 @@ class Mech(MechCommand): --debug Show debug messages. Common commands: - (list|ls) lists all available boxes - init initializes a new Mech environment by creating a Mechfile + box manages boxes: add, list remove, etc. destroy stops and deletes all traces of the instances - (up|start) starts instances (aka virtual machines) (down|stop|halt) stops the instances - suspend suspends the instances - pause pauses the instances - ssh connects to an instance via SSH - ssh-config outputs OpenSSH valid configuration to connect to the instances - scp copies files to/from the machine via SCP - ip outputs ip of an instance - box manages boxes: add, list remove, etc. global-status outputs status of all virutal machines on this host - status outputs status of the instances - ps list running processes for an instance + 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. - port displays information about guest port mappings + 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) For help on any individual command run `mech -h` @@ -452,17 +452,17 @@ def init(self, arguments): # pylint: disable=no-self-use HashiCorp account/box (ex: 'bento/ubuntu-18.04'). Options: - -f, --force Overwrite existing Mechfile - --insecure Do not validate SSL certificates + --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) + --box-version VERSION Constrain version of the added box --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) - --name INSTANCE Name of the instance (myinst1) - --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) + -f, --force Overwrite existing Mechfile -h, --help Print this help + --insecure Do not validate SSL certificates + --name INSTANCE Name of the instance (myinst1) """ name = arguments['--name'] box_version = arguments['--box-version'] @@ -502,14 +502,14 @@ def add(self, arguments): # pylint: disable=no-self-use Example box: bento/ubuntu-18.04 Options: - --insecure Do not validate SSL certificates + --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) + --box-version VERSION Constrain version of the added box --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) - --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) + --insecure Do not validate SSL certificates -h, --help Print this help """ name = arguments[''] @@ -576,19 +576,19 @@ def up(self, arguments): # pylint: disable=invalid-name interface supplied in the box file. Options: - --gui Start GUI - --disable-shared-folders Do not share folders with VM - --disable-provisioning Do not provision - --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) - --no-cache Do not save the downloaded box + --disable-provisioning Do not provision + --disable-shared-folders Do not share folders with VM + --gui Start GUI + --insecure Do not validate SSL certificates --memsize 1024 Specify the size of memory for VM - --numvcpus 1 Specify the number of vcpus 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 """ gui = arguments['--gui'] @@ -798,7 +798,7 @@ def down(self, arguments): Usage: mech down [options] [] Options: - --force Force a hard stop + -f, --force Force a hard stop -h, --help Print this help """ force = arguments['--force'] @@ -1137,8 +1137,8 @@ def provision(self, arguments): Usage: mech provision [options] [] Options: - -s, --show-only Show the provisioning info (do not run) -h, --help Print this help + -s, --show-only Show the provisioning info (do not run) """ show = arguments['--show-only'] instance_name = arguments[''] @@ -1215,8 +1215,8 @@ 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 + --machine-readable Display machine-readable output """ instance_name = arguments[''] From 2f722b15796560bd5612a5d6dfcbe0a2c0e9bf48 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Thu, 13 Feb 2020 11:25:19 -0800 Subject: [PATCH 081/116] add bash completions --- README.md | 21 ++++++++ _mech | 8 ++-- mech_completion.sh | 117 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 mech_completion.sh diff --git a/README.md b/README.md index 386ddcc..cb77f73 100644 --- a/README.md +++ b/README.md @@ -148,3 +148,24 @@ 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 index bbce629..a88823d 100644 --- a/_mech +++ b/_mech @@ -42,7 +42,6 @@ __mech_box_list () { function _mech_box_arguments { _arguments \ - "--force[Force a hard stop]" \ "-h[Show help information]" \ "1: :(add delete list ls remove)" } @@ -74,6 +73,7 @@ function _mech_down { typeset -A opt_args _arguments -C \ + "-f[Force a hard stop]" \ "--force[Force a hard stop]" \ "-h[Show help information]" \ "--help[Show help information]" \ @@ -102,8 +102,8 @@ function _mech_init { "--cert=[FILE A client SSL cert, if needed]" \ "--checksum=[CHECKSUM Checksum for the box]" \ "--checksum-type=[TYPE Checksum type (md5, sha1, sha256)]" \ - "-force[Force a hard stop]" \ - "--force[Force a hard stop]" \ + "-f[Overwrite exsting Mechfile]" \ + "--force[Overwrite existing Mechfile]" \ "-h[Show help information]" \ "--help[Show help information]" \ "--insecure[Do not validate SSL certificates]" \ @@ -146,7 +146,7 @@ function _mech_port { typeset -A opt_args _arguments -C \ - "-g[PORT Output the host port that maps to the given guest port]" \ + "--guest[PORT Output the host port that maps to the given guest port]" \ "-h[Show help information]" \ "--help[Show help information]" \ "--machine-readable[Display machine-readable output]" \ diff --git a/mech_completion.sh b/mech_completion.sh new file mode 100644 index 0000000..8a24a54 --- /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 --cacert --capath --cert --checksum --checksum-type --force -h --help --insecure --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 --machine-readable" + 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="--cacert --capath --cert --checksum --checksum-type --disable-provisioning --disable-shared-folders --gui -h --help --insecure --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 From 9de79b95e3b2561ca3fe0f5c9527f0fdf9fd8fb7 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Thu, 13 Feb 2020 22:17:45 -0800 Subject: [PATCH 082/116] add ability to have multiple shared folders --- mech/conftest.py | 6 ++++++ mech/mech.py | 23 ++++++++++++----------- mech/test_mech.py | 9 +++------ mech/test_utils.py | 6 ++++++ mech/utils.py | 25 ++++++++++++++++++++++++- tests/int/init_from_file.bats | 2 +- tests/int/shared_folders.bats | 14 +++++++++++--- tests/int/shared_folders/Mechfile | 14 ++++++++++++-- tests/int/simple.bats | 6 +++--- 9 files changed, 78 insertions(+), 27 deletions(-) diff --git a/mech/conftest.py b/mech/conftest.py index 44350f8..222a4f5 100644 --- a/mech/conftest.py +++ b/mech/conftest.py @@ -23,6 +23,12 @@ def mechfile_two_entries(): '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' diff --git a/mech/mech.py b/mech/mech.py index 4dd2bd0..cd1034b 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -97,6 +97,7 @@ def __init__(self, name, mechfile=None): self.provision = mechfile[name].get('provision', None) self.enable_ip_lookup = False self.config = {} + self.shared_folders = mechfile[name].get('shared_folders', []) self.user = DEFAULT_USER self.password = DEFAULT_PASSWORD self.path = os.path.join(utils.mech_dir(), name) @@ -117,13 +118,13 @@ def __repr__(self): '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}config:{config}{sep}shared_folders:{shared_folders}'. 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, - sep=sep)) + shared_folders=self.shared_folders, sep=sep)) def config_ssh(self): """Configure ssh to work. Create a insecure private key file for ssh/scp.""" @@ -450,6 +451,8 @@ def init(self, arguments): # pylint: disable=no-self-use 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. Options: --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) @@ -574,6 +577,10 @@ def up(self, arguments): # pylint: disable=invalid-name 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. Options: --cacert FILE CA certificate for SSL download @@ -647,9 +654,7 @@ def up(self, arguments): # pylint: disable=invalid-name lookup = inst.enable_ip_lookup ip_address = vmrun.get_guest_ip_address(lookup=lookup) if not disable_shared_folders: - print(colored.blue("Sharing current folder...")) - vmrun.enable_shared_folders(quiet=False) - vmrun.add_shared_folder('mech', utils.main_dir(), quiet=True) + utils.share_folders(vmrun, inst) if ip_address: if started: print(colored.green("VM ({})" @@ -899,9 +904,7 @@ def resume(self, arguments): lookup = inst.enable_ip_lookup ip_address = vmrun.get_guest_ip_address(lookup=lookup) if not disable_shared_folders: - print(colored.blue("Sharing current folder...")) - vmrun.enable_shared_folders(quiet=False) - vmrun.add_shared_folder('mech', utils.main_dir(), quiet=True) + utils.share_folders(vmrun, inst) else: print(colored.blue("Disabling shared folders...")) vmrun.disable_shared_folders(quiet=False) @@ -921,9 +924,7 @@ def resume(self, arguments): lookup = inst.enable_ip_lookup ip_address = vmrun.get_guest_ip_address(lookup=lookup) if not disable_shared_folders: - print(colored.blue("Sharing current folder...")) - vmrun.enable_shared_folders(quiet=False) - vmrun.add_shared_folder('mech', utils.main_dir(), quiet=True) + utils.share_folders(vmrun, inst) if ip_address: if started: print(colored.green("VM ({}) started on " diff --git a/mech/test_mech.py b/mech/test_mech.py index 3113016..395af59 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -755,7 +755,7 @@ def test_mech_resume_shared_folders(mock_locate, mock_load_mechfile, mock_enable_shared_folders.assert_called() mock_add_shared_folder.assert_called() mock_vmrun_get_ip.assert_called() - assert re.search(r'Sharing current', out, re.MULTILINE) + assert re.search(r'Sharing folders', out, re.MULTILINE) assert re.search(r'resumed', out, re.MULTILINE) @@ -789,7 +789,7 @@ def test_mech_resume_unpause_fails_starts_successfully_with_shared_folders( mock_enable_shared_folders.assert_called() mock_add_shared_folder.assert_called() mock_vmrun_get_ip.assert_called() - assert re.search(r'Sharing current', out, re.MULTILINE) + assert re.search(r'Sharing folders', out, re.MULTILINE) assert re.search(r'started', out, re.MULTILINE) @@ -1049,7 +1049,6 @@ def test_mech_up_with_provisioning(mock_locate, mock_load_mechfile, mock_init_bo assert re.search(r'started', out, re.MULTILINE) -@patch('mech.vmrun.VMrun.add_shared_folder') @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) @@ -1059,8 +1058,7 @@ def test_mech_up_with_provisioning(mock_locate, mock_load_mechfile, mock_init_bo def test_mech_up_wth_shared_folders(mock_locate, mock_load_mechfile, mock_init_box, mock_vmrun_start, mock_vmrun_get_ip, mock_vmrun_enable_shared_folders, - mock_vmrun_add_shared_folder, capfd, - mechfile_one_entry): + capfd, mechfile_one_entry): """Test 'mech up'.""" mock_load_mechfile.return_value = mechfile_one_entry global_arguments = {'--debug': False} @@ -1088,7 +1086,6 @@ def test_mech_up_wth_shared_folders(mock_locate, mock_load_mechfile, mock_init_b mock_vmrun_start.assert_called() mock_vmrun_get_ip.assert_called() mock_vmrun_enable_shared_folders.assert_called() - mock_vmrun_add_shared_folder.assert_called() out, _ = capfd.readouterr() assert re.search(r'started', out, re.MULTILINE) diff --git a/mech/test_utils.py b/mech/test_utils.py index 26946c2..ef520f9 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -302,6 +302,7 @@ def test_build_mechfile_entry_https_location(): 'box': None, 'box_version': None, 'name': None, + 'shared_folders': [{'host_path': '../..', 'share_name': 'mech'}], 'url': 'https://foo' } @@ -312,6 +313,7 @@ def test_build_mechfile_entry_http_location(): 'box': None, 'box_version': None, 'name': None, + 'shared_folders': [{'host_path': '../..', 'share_name': 'mech'}], 'url': 'http://foo' } @@ -323,6 +325,7 @@ def test_build_mechfile_entry_ftp_location(): 'box': None, 'box_version': None, 'name': None, + 'shared_folders': [{'host_path': '../..', 'share_name': 'mech'}], 'url': 'ftp://foo' } @@ -333,6 +336,7 @@ def test_build_mechfile_entry_ftp_location_with_other_values(): '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', @@ -349,6 +353,7 @@ def test_build_mechfile_entry_file_location_json(catalog): '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' @@ -376,6 +381,7 @@ def test_build_mechfile_entry_file_location_external_good(mock_requests_get, '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' diff --git a/mech/utils.py b/mech/utils.py index 3fd99c3..ce9809f 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -217,7 +217,15 @@ def load_mechfile(should_exist=True): return {} -def build_mechfile_entry(location, box=None, name=None, box_version=None, requests_kwargs=None): +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, requests_kwargs=None): """Build the Mechfile from the inputs.""" LOGGER.debug("location:%s name:%s box:%s box_version:%s", location, name, box, box_version) if requests_kwargs is None: @@ -231,6 +239,10 @@ def build_mechfile_entry(location, box=None, name=None, box_version=None, reques 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 = 'first' @@ -293,6 +305,7 @@ def catalog_to_mechfile(catalog, name=None, box=None, box_version=None): mechfile['box'] = catalog['name'] mechfile['box_version'] = current_version mechfile['url'] = provider['url'] + mechfile['shared_folders'] = default_shared_folders() return mechfile sys.exit(colored.red("Couldn't find a VMWare compatible VM using catalog:{}".format(catalog))) @@ -726,3 +739,13 @@ def config_ssh_string(config_ssh): if key != 'Host': ssh_config += " {} {}".format(key, value) + os.linesep return ssh_config + + +def share_folders(vmrun, inst): + 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) diff --git a/tests/int/init_from_file.bats b/tests/int/init_from_file.bats index af12f67..184bb21 100755 --- a/tests/int/init_from_file.bats +++ b/tests/int/init_from_file.bats @@ -39,7 +39,7 @@ regex3="Added network" regex4="Bringing machine" regex5="Getting IP" - regex6="Sharing current folder" + regex6="Sharing folders" regex7="started" [ "$status" -eq 0 ] [[ "$output" =~ $regex1 ]] diff --git a/tests/int/shared_folders.bats b/tests/int/shared_folders.bats index f7356a8..2632a04 100755 --- a/tests/int/shared_folders.bats +++ b/tests/int/shared_folders.bats @@ -6,7 +6,7 @@ # like this: ./shared_folders.bats @test "shared folders testing" { - cd provision + cd shared_folders # setup find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true @@ -21,6 +21,14 @@ [ "$status" -eq 0 ] [[ "$output" =~ $regex1 ]] + # make sure we can see files on 2nd "mech2" mount + date > /tmp/now + run mech ssh -c "ls -al /mnt/hgfs/mech2/now" first + regex1=" /mnt/hgfs/mech2/now" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + rm /tmp/now + run mech stop regex1="Stopped" [ "$status" -eq 0 ] @@ -33,7 +41,7 @@ run mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first regex1="No such file or directory" - [ "$status" -eq 1 ] + [ "$status" -eq 2 ] [[ "$output" =~ $regex1 ]] run mech pause @@ -48,7 +56,7 @@ run mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first regex1="No such file or directory" - [ "$status" -eq 1 ] + [ "$status" -eq 2 ] [[ "$output" =~ $regex1 ]] run mech pause diff --git a/tests/int/shared_folders/Mechfile b/tests/int/shared_folders/Mechfile index 6777f02..4d689b7 100644 --- a/tests/int/shared_folders/Mechfile +++ b/tests/int/shared_folders/Mechfile @@ -3,6 +3,16 @@ "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" + "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" + } + ] } -} \ No newline at end of file +} diff --git a/tests/int/simple.bats b/tests/int/simple.bats index 7700434..6ac6924 100755 --- a/tests/int/simple.bats +++ b/tests/int/simple.bats @@ -47,7 +47,7 @@ regex7="Added network" regex8="Bringing machine" regex9="Getting IP" - regex10="Sharing current folder" + regex10="Sharing folders" regex11="started" [ "$status" -eq 0 ] [[ "$output" =~ $regex1 ]] @@ -66,7 +66,7 @@ run mech up regex1="Bringing machine" regex2="Getting IP" - regex3="Sharing current folder" + regex3="Sharing folders" regex4="was already started" [ "$status" -eq 0 ] [[ "$output" =~ $regex1 ]] @@ -78,7 +78,7 @@ run mech start regex1="Bringing machine" regex2="Getting IP" - regex3="Sharing current folder" + regex3="Sharing folders" regex4="was already started" [ "$status" -eq 0 ] [[ "$output" =~ $regex1 ]] From 0241c3854497aabe8fb2f4eb695399d0a9de3a0c Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 10:20:45 -0800 Subject: [PATCH 083/116] adding copyright --- mech/__init__.py | 1 + mech/__main__.py | 1 + mech/command.py | 1 + mech/compat.py | 1 + mech/conftest.py | 2 ++ mech/mech.py | 1 + mech/test_mech.py | 2 ++ mech/test_mech_box.py | 2 ++ mech/test_mech_int.py | 3 +++ mech/test_mech_snapshot.py | 2 ++ mech/test_utils.py | 2 ++ mech/utils.py | 1 + mech/vmrun.py | 1 + 13 files changed, 20 insertions(+) diff --git a/mech/__init__.py b/mech/__init__.py index d9d5dd5..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 diff --git a/mech/__main__.py b/mech/__main__.py index b205a16..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 diff --git a/mech/command.py b/mech/command.py index 65ca177..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 diff --git a/mech/compat.py b/mech/compat.py index 3007817..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 diff --git a/mech/conftest.py b/mech/conftest.py index 222a4f5..8b84859 100644 --- a/mech/conftest.py +++ b/mech/conftest.py @@ -1,3 +1,5 @@ +# Copyright (c) 2020 Mike Kinney + """Common pytest code.""" import json import pytest diff --git a/mech/mech.py b/mech/mech.py index cd1034b..22851a3 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 diff --git a/mech/test_mech.py b/mech/test_mech.py index 395af59..eb6ed58 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -1,3 +1,5 @@ +# Copyright (c) 2020 Mike Kinney + """Test the mech cli. """ import os import subprocess diff --git a/mech/test_mech_box.py b/mech/test_mech_box.py index b4457f7..e7cde03 100644 --- a/mech/test_mech_box.py +++ b/mech/test_mech_box.py @@ -1,3 +1,5 @@ +# Copyright (c) 2020 Mike Kinney + """Unit tests for 'mech box'.""" import re diff --git a/mech/test_mech_int.py b/mech/test_mech_int.py index 17f5da7..59f7d32 100644 --- a/mech/test_mech_int.py +++ b/mech/test_mech_int.py @@ -1,6 +1,9 @@ +# Copyright (c) 2020 Mike Kinney + """Test the mech cli. """ import subprocess + import pytest diff --git a/mech/test_mech_snapshot.py b/mech/test_mech_snapshot.py index 4a5b253..597f00d 100644 --- a/mech/test_mech_snapshot.py +++ b/mech/test_mech_snapshot.py @@ -1,3 +1,5 @@ +# Copyright (c) 2020 Mike Kinney + """Unit tests for 'mech snapshot'.""" import re diff --git a/mech/test_utils.py b/mech/test_utils.py index ef520f9..4c317ec 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -1,3 +1,5 @@ +# Copyright (c) 2020 Mike Kinney + """Test mech utils.""" import os import re diff --git a/mech/utils.py b/mech/utils.py index ce9809f..e35fb1a 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 diff --git a/mech/vmrun.py b/mech/vmrun.py index f8b4e44..2549141 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 From 9935ed68d8908c5ddf4b41b71574d717018a50b9 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 10:23:37 -0800 Subject: [PATCH 084/116] add initial workflow --- .github/workflows/python-package.yml | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/python-package.yml diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..1cf4f3a --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,32 @@ +name: Python package + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 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 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 From 5c2cfecd7ac310e31d69cd30503ab2ac419afba5 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 10:26:05 -0800 Subject: [PATCH 085/116] add docopt to deps --- .github/workflows/python-package.yml | 2 +- CONTRIBUTING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 1cf4f3a..8ed9ff7 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -19,7 +19,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist + pip install docopt flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist - name: Lint with flake8 run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35dbc95..23a82a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ source venv/bin/activate python setup.py install # if doing development -pip install flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist +pip install docopt flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist # also optional pip install autopep8 From 7b49d4b7fc3f5a55b239eab3673130254592f012 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 10:28:35 -0800 Subject: [PATCH 086/116] add more deps --- .github/workflows/python-package.yml | 2 +- CONTRIBUTING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 8ed9ff7..a4ad1a9 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -19,7 +19,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install docopt flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist + pip install docopt clint requests flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist - name: Lint with flake8 run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23a82a0..2bf7d4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ source venv/bin/activate python setup.py install # if doing development -pip install docopt flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist +pip install docopt clint requests flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist # also optional pip install autopep8 From 9f4f22cbff17f3cf1b232e39326837c8cc3a03e1 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 12:28:59 -0800 Subject: [PATCH 087/116] move code into constructor; mock a few more things --- CONTRIBUTING.md | 3 +++ mech/test_mech.py | 11 ++++++++--- mech/test_mech_snapshot.py | 3 ++- mech/vmrun.py | 23 ++++++++++++++--------- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2bf7d4d..8be7c53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,9 @@ 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 diff --git a/mech/test_mech.py b/mech/test_mech.py index eb6ed58..1475d6f 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -6,13 +6,14 @@ import re from unittest.mock import patch, mock_open -from pytest import raises +from pytest import raises, mark import mech.command import mech.mech import mech.vmrun +@mark.int def test_version(): """Test '--version'.""" return_value, out = subprocess.getstatusoutput('mech --version') @@ -20,6 +21,7 @@ def test_version(): assert return_value == 0 +@mark.int def test_help(): """Test '--help'.""" return_value, out = subprocess.getstatusoutput('mech --help') @@ -659,13 +661,14 @@ def test_mech_reload_not_created(mock_locate, mock_load_mechfile, 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, - capfd, mechfile_two_entries): + mock_vmrun_disable_shared_folders, capfd, mechfile_two_entries): """Test 'mech resume'.""" mock_load_mechfile.return_value = mechfile_two_entries global_arguments = {'--debug': False} @@ -680,6 +683,7 @@ def test_mech_resume(mock_locate, mock_load_mechfile, 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) @@ -1110,11 +1114,12 @@ def test_mech_ssh_config_not_created(mock_locate, mock_load_mechfile, capfd, 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, - mechfile_one_entry): + 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 diff --git a/mech/test_mech_snapshot.py b/mech/test_mech_snapshot.py index 597f00d..918d157 100644 --- a/mech/test_mech_snapshot.py +++ b/mech/test_mech_snapshot.py @@ -222,10 +222,11 @@ def test_mech_snapshot_save_success(mock_locate, mock_load_mechfile, 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, - mechfile_one_entry): + 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 diff --git a/mech/vmrun.py b/mech/vmrun.py index 2549141..52a46f8 100644 --- a/mech/vmrun.py +++ b/mech/vmrun.py @@ -46,6 +46,7 @@ def get_fallback_executable(): vmrun = os.path.join(path, 'vmrun.exe') if os.path.exists(vmrun): return vmrun + return None def get_darwin_executable(): @@ -114,13 +115,6 @@ class VMrun(): # pylint: disable=too-many-public-methods The 'vmrun' command is used to interact with VMware. To add/update vmware functionality, run the 'vmrun' command with '--help'. """ - 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) def __init__(self, vmx_file=None, # pylint: disable=too-many-arguments user=None, password=None, executable=None, provider=None): @@ -128,8 +122,19 @@ def __init__(self, vmx_file=None, # pylint: disable=too-many-arguments 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 + + if self.executable is None: + if sys.platform == 'darwin': + default_executable = get_darwin_executable() + elif sys.platform == 'win32': + default_executable = get_win32_executable() + else: + default_executable = get_fallback_executable() + if self.provider is None: + if default_executable is not None: + self.provider = get_provider(default_executable) def vmrun(self, cmd, *args, **kwargs): """Execute a 'vmrun' command.""" From 34012db14e4ae2c4c7eea41af908414247636d83 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 12:32:13 -0800 Subject: [PATCH 088/116] do not test on python 2.7, it is dead, jim --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index a4ad1a9..b1d7e15 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + python-version: [3.5, 3.6, 3.7, 3.8] steps: - uses: actions/checkout@v2 From ba15adce11e5fd44129a3e909856fd461d40eba7 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 12:36:55 -0800 Subject: [PATCH 089/116] mock get_provider --- mech/test_mech.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mech/test_mech.py b/mech/test_mech.py index 1475d6f..dabf0a1 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -196,6 +196,7 @@ def test_mech_status_could_not_get_ip(mock_locate, mock_load_mechfile, assert re.search(r'VM is on.*no IP to connect', out, re.MULTILINE) +@patch('mech.vmrun.get_provider', return_value=None) @patch('os.path.exists', return_value=True) @patch('shutil.rmtree') @patch('mech.vmrun.VMrun.delete_vm') @@ -204,8 +205,8 @@ def test_mech_status_could_not_get_ip(mock_locate, mock_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, capfd, - mechfile_two_entries): + 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 @@ -220,6 +221,7 @@ def test_mech_destroy(mock_locate, mock_load_mechfile, 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() From 81d07905aba0974bfe1da49a7265535440625a66 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 12:42:07 -0800 Subject: [PATCH 090/116] drop test under python 3.5 --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index b1d7e15..857e179 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8] steps: - uses: actions/checkout@v2 From 5d1a3d7a34f480e3e9eb22bc477c79c21c40a5cf Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 18:28:07 -0800 Subject: [PATCH 091/116] give up on python 3.6 --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 857e179..51df5e4 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.7, 3.8] steps: - uses: actions/checkout@v2 From 02f2250c46ecc64e13b24b1e2013796ce75e59c6 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 19:16:15 -0800 Subject: [PATCH 092/116] fix issues that int tests found; init_from_file is intermittently failing --- CONTRIBUTING.md | 4 ++-- mech/mech.py | 6 +++++- mech/vmrun.py | 13 ++++++++----- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8be7c53..93ce1b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,8 +69,8 @@ pytest --durations=0 # Or, you can run them from the main project directory like this: pytest -m"int" -# To run all tests: -pytest -m"int or not int" +# To run all tests (with verbose output and show local variables): +pytest -m"int or not int" -vv -l # Or, just one run int test like this (with verbose and show local variables): pytest -m"int" -k"provision" -v -l diff --git a/mech/mech.py b/mech/mech.py index 22851a3..dba7a9e 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -1289,11 +1289,15 @@ def list(self, arguments): 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), - inst.box_version.rjust(12) + box_version.rjust(12) )) # allow 'mech ls' as alias to 'mech list' diff --git a/mech/vmrun.py b/mech/vmrun.py index 52a46f8..115acab 100644 --- a/mech/vmrun.py +++ b/mech/vmrun.py @@ -39,6 +39,7 @@ 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): @@ -127,14 +128,16 @@ def __init__(self, vmx_file=None, # pylint: disable=too-many-arguments if self.executable is None: if sys.platform == 'darwin': - default_executable = get_darwin_executable() + self.executable = get_darwin_executable() elif sys.platform == 'win32': - default_executable = get_win32_executable() + self.executable = get_win32_executable() else: - default_executable = get_fallback_executable() + self.executable = get_fallback_executable() if self.provider is None: - if default_executable is not None: - self.provider = get_provider(default_executable) + if self.executable is not None: + self.provider = get_provider(self.executable) + LOGGER.debug('self.executable:%s self.provider:%s', + self.executable, self.provider) def vmrun(self, cmd, *args, **kwargs): """Execute a 'vmrun' command.""" From b8cd3a8a277d3d4e94fb9708f3af955eb4a02416 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 19:52:34 -0800 Subject: [PATCH 093/116] refactor so each class in own file; all utils in utils.py --- mech/mech.py | 343 +------------------------ mech/mech_box.py | 131 ++++++++++ mech/mech_command.py | 50 ++++ mech/mech_instance.py | 170 ++++++++++++ mech/mech_snapshot.py | 132 ++++++++++ mech/{test_mech_int.py => test_int.py} | 19 +- mech/test_mech.py | 23 +- mech/utils.py | 77 +++++- mech/vmrun.py | 89 +------ 9 files changed, 593 insertions(+), 441 deletions(-) create mode 100644 mech/mech_box.py create mode 100644 mech/mech_command.py create mode 100644 mech/mech_instance.py create mode 100644 mech/mech_snapshot.py rename mech/{test_mech_int.py => test_int.py} (79%) diff --git a/mech/mech.py b/mech/mech.py index dba7a9e..9516f89 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -22,14 +22,12 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -"""Mech classes.""" +"""Mech class""" from __future__ import print_function, absolute_import import os -import re import sys -import fnmatch import logging import tempfile import textwrap @@ -40,344 +38,13 @@ from . import utils from .vmrun import VMrun -from .command import Command +from .mech_instance import MechInstance +from .mech_command import MechCommand +from .mech_box import MechBox +from .mech_snapshot import MechSnapshot 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.shared_folders = mechfile[name].get('shared_folders', []) - self.user = DEFAULT_USER - self.password = DEFAULT_PASSWORD - 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 - - 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}'. - 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, sep=sep)) - - def config_ssh(self): - """Configure ssh to work. Create a insecure private key file for ssh/scp.""" - vmrun = VMrun(self.vmx, user=self.user, password=self.password) - 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."))) - - insecure_private_key = os.path.abspath(os.path.join( - utils.mech_dir(), "insecure_private_key")) - if not os.path.exists(insecure_private_key): - with open(insecure_private_key, 'w') as the_file: - the_file.write(INSECURE_PRIVATE_KEY) - os.chmod(insecure_private_key, 0o400) - self.config = { - "Host": self.name, - "User": self.user, - "Port": "22", - "UserKnownHostsFile": "/dev/null", - "StrictHostKeyChecking": "no", - "PasswordAuthentication": "no", - "IdentityFile": insecure_private_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 - - -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) - - -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 - --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) - --insecure Do not validate SSL certificates - -f, --force Overwrite an existing box if it exists - -h, --help Print this help - """ - - location = arguments[''] - box_version = arguments['--box-version'] - - force = arguments['--force'] - requests_kwargs = utils.get_requests_kwargs(arguments) - utils.add_box(name=None, box=None, location=location, box_version=box_version, - force=force, requests_kwargs=requests_kwargs) - - 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 - - -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, user=inst.user, password=inst.password) - 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, user=inst.user, password=inst.password) - 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, user=inst.user, password=inst.password) - 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))) - class Mech(MechCommand): """ diff --git a/mech/mech_box.py b/mech/mech_box.py new file mode 100644 index 0000000..c774b6b --- /dev/null +++ b/mech/mech_box.py @@ -0,0 +1,131 @@ +# -*- 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 + --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) + --insecure Do not validate SSL certificates + -f, --force Overwrite an existing box if it exists + -h, --help Print this help + """ + + location = arguments[''] + box_version = arguments['--box-version'] + + force = arguments['--force'] + requests_kwargs = utils.get_requests_kwargs(arguments) + utils.add_box(name=None, box=None, location=location, box_version=box_version, + force=force, requests_kwargs=requests_kwargs) + + 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..34a4031 --- /dev/null +++ b/mech/mech_instance.py @@ -0,0 +1,170 @@ +# -*- 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.shared_folders = mechfile[name].get('shared_folders', []) + self.user = DEFAULT_USER + self.password = DEFAULT_PASSWORD + 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 + + 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}'. + 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, sep=sep)) + + def config_ssh(self): + """Configure ssh to work. Create a insecure private key file for ssh/scp.""" + vmrun = VMrun(self.vmx, user=self.user, password=self.password) + 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."))) + + insecure_private_key = os.path.abspath(os.path.join( + utils.mech_dir(), "insecure_private_key")) + if not os.path.exists(insecure_private_key): + with open(insecure_private_key, 'w') as the_file: + the_file.write(INSECURE_PRIVATE_KEY) + os.chmod(insecure_private_key, 0o400) + self.config = { + "Host": self.name, + "User": self.user, + "Port": "22", + "UserKnownHostsFile": "/dev/null", + "StrictHostKeyChecking": "no", + "PasswordAuthentication": "no", + "IdentityFile": insecure_private_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..bb681cf --- /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, user=inst.user, password=inst.password) + 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, user=inst.user, password=inst.password) + 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, user=inst.user, password=inst.password) + 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_mech_int.py b/mech/test_int.py similarity index 79% rename from mech/test_mech_int.py rename to mech/test_int.py index 59f7d32..1462b38 100644 --- a/mech/test_mech_int.py +++ b/mech/test_int.py @@ -1,12 +1,29 @@ # Copyright (c) 2020 Mike Kinney -"""Test the mech cli. """ +"""Mech integration tests""" +import re import subprocess import pytest +@pytest.mark.int +def test_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_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_add_and_remove_instances(): """Test add and remove instances integration tests.""" diff --git a/mech/test_mech.py b/mech/test_mech.py index dabf0a1..004d7fd 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -1,34 +1,17 @@ # Copyright (c) 2020 Mike Kinney -"""Test the mech cli. """ +"""mech tests""" import os -import subprocess import re from unittest.mock import patch, mock_open -from pytest import raises, mark +from pytest import raises import mech.command import mech.mech import mech.vmrun -@mark.int -def test_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 - - -@mark.int -def test_help(): - """Test '--help'.""" - return_value, out = subprocess.getstatusoutput('mech --help') - assert re.match(r'Usage: mech ', out) - assert return_value == 0 - - @patch('mech.utils.load_mechfile') @patch('mech.utils.locate', return_value=None) def test_mech_list_with_one(mock_locate, mock_load_mechfile, capfd, @@ -196,7 +179,7 @@ def test_mech_status_could_not_get_ip(mock_locate, mock_load_mechfile, assert re.search(r'VM is on.*no IP to connect', out, re.MULTILINE) -@patch('mech.vmrun.get_provider', return_value=None) +@patch('mech.utils.get_provider', return_value=None) @patch('os.path.exists', return_value=True) @patch('shutil.rmtree') @patch('mech.vmrun.VMrun.delete_vm') diff --git a/mech/utils.py b/mech/utils.py index e35fb1a..43b23f2 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -44,8 +44,8 @@ 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__) @@ -750,3 +750,78 @@ def share_folders(vmrun, inst): 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_exe): + """ + Identifies the right hosttype for vmrun command (ws | fusion | player) + """ + + 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 + + map(b2s, proc.communicate()) + if proc.returncode == 0: + return provider diff --git a/mech/vmrun.py b/mech/vmrun.py index 115acab..db2d971 100644 --- a/mech/vmrun.py +++ b/mech/vmrun.py @@ -21,7 +21,7 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -"""mech code to interface with VMware vmrun command line utility.""" +"""VMrun class""" from __future__ import absolute_import @@ -31,84 +31,11 @@ import subprocess import tempfile -from .compat import PY3, b2s +from .compat import b2s +from . import utils -LOGGER = logging.getLogger(__name__) - - -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_exe): - """ - identifies the right hosttype for vmrun command (ws | fusion | player) - """ - 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 - - map(b2s, proc.communicate()) - if proc.returncode == 0: - return provider +LOGGER = logging.getLogger(__name__) class VMrun(): # pylint: disable=too-many-public-methods @@ -128,14 +55,14 @@ def __init__(self, vmx_file=None, # pylint: disable=too-many-arguments if self.executable is None: if sys.platform == 'darwin': - self.executable = get_darwin_executable() + self.executable = utils.get_darwin_executable() elif sys.platform == 'win32': - self.executable = get_win32_executable() + self.executable = utils.get_win32_executable() else: - self.executable = get_fallback_executable() + self.executable = utils.get_fallback_executable() if self.provider is None: if self.executable is not None: - self.provider = get_provider(self.executable) + self.provider = utils.get_provider(self.executable) LOGGER.debug('self.executable:%s self.provider:%s', self.executable, self.provider) From 7799da53c0f7713d0277ffa6e04f0446a42a7af2 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 20:35:00 -0800 Subject: [PATCH 094/116] refactor utils.provision --- mech/mech.py | 5 ++-- mech/test_utils.py | 72 ++++++++++++++++++++++++++++------------------ mech/utils.py | 58 +++++++++++++++++-------------------- 3 files changed, 73 insertions(+), 62 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 9516f89..c1e5f37 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -338,8 +338,7 @@ def up(self, arguments): # pylint: disable=invalid-name print(colored.yellow("VM ({}) was already started on an " "unknown IP address".format(instance))) if not disable_provisioning: - utils.provision(instance, inst.vmx, inst.user, inst.password, - inst.provision, show=False) + utils.provision(inst, show=False) # allows "mech start" to alias to "mech up" start = up @@ -823,7 +822,7 @@ def provision(self, arguments): inst = MechInstance(instance) if inst.created: - utils.provision(instance, inst.vmx, inst.user, inst.password, inst.provision, show) + utils.provision(inst, show) else: print("VM not created.") diff --git a/mech/test_utils.py b/mech/test_utils.py index 4c317ec..408bf91 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -404,25 +404,24 @@ def test_build_mechfile_entry_file_location_external_bad_location(): def test_provision_no_instance(): """Test provisioning.""" with raises(SystemExit, match=r"Need to provide an instance to provision"): - mech.utils.provision(instance=None, vmx=None, user=None, password=None, - provision_config=None, show=None) + 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='first', vmx=None, user=None, password=None, - provision_config=None, show=None) + 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='first', vmx='/tmp/first/some.vmx', - user='vagrant', password='vagrant', provision_config=None, - show=None) + mech.utils.provision(instance=mock_inst, show=None) @patch('mech.utils.provision_file') @@ -431,8 +430,9 @@ def test_provision_file_no_provisioning(mock_installed_tools, mock_provision_fil """Test provisioning.""" mock_installed_tools.return_value = "running" mock_provision_file.return_value = None - mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', user='vagrant', - password='vagrant', provision_config=[], show=None) + mock_inst = MagicMock() + mock_inst.provision = [] + mech.utils.provision(instance=mock_inst, show=None) out, _ = capfd.readouterr() assert re.search(r'Nothing to provision', out, re.MULTILINE) @@ -450,8 +450,11 @@ def test_provision_file(mock_installed_tools, mock_copy_file, capfd): "destination": "/tmp/file1.txt", }, ] - mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', user='vagrant', - password='vagrant', provision_config=config, show=None) + 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() assert re.search(r'Copying ', out, re.MULTILINE) @@ -470,8 +473,11 @@ def test_provision_file_could_not_copy_file_to_guest(mock_installed_tools, "destination": "/tmp/file1.txt", }, ] - mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', user='vagrant', - password='vagrant', provision_config=config, show=None) + 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() assert re.search(r'Not Provisioned', out, re.MULTILINE) @@ -487,9 +493,11 @@ def test_provision_file_show(mock_installed_tools, capfd): "destination": "/tmp/file1.txt", }, ] - mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', - user='vagrant', password='vagrant', - provision_config=config, show=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() assert re.search(r'instance:', out, re.MULTILINE) @@ -520,9 +528,11 @@ def test_provision_shell(mock_installed_tools, mock_copy_file, "inline": "echo hello from inline" }, ] - mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', - user='vagrant', password='vagrant', - provision_config=config, show=None) + 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_copy_file.assert_called() @@ -550,9 +560,11 @@ def test_provision_shell_show_only(mock_installed_tools, capfd): ], }, ] - mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', - user='vagrant', password='vagrant', - provision_config=config, show=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) @@ -573,9 +585,11 @@ def test_provision_shell_with_issue(mock_installed_tools, mock_provision_shell, ], }, ] - mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', - user='vagrant', password='vagrant', - provision_config=config, show=None) + 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() @@ -590,9 +604,11 @@ def test_provision_with_unknown_type(mock_installed_tools, capfd): "type": "foo", }, ] - mech.utils.provision(instance='first', vmx='/tmp/first/some.vmx', - user='vagrant', password='vagrant', - provision_config=config, show=None) + 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) diff --git a/mech/utils.py b/mech/utils.py index 43b23f2..443962b 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -193,7 +193,7 @@ def update_vmx(path, numvcpus=None, memsize=None, no_nat=False): def load_mechfile(should_exist=True): - """Load the Mechfile from disk and return the object.""" + """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): @@ -206,14 +206,12 @@ def load_mechfile(should_exist=True): print(colored.red("Invalid Mechfile." + os.linesep)) else: if should_exist: - print(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." - ))) - sys.exit(1) + 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 {} @@ -356,8 +354,7 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= save=save, requests_kwargs=requests_kwargs) if not name_version_box: - print(colored.red("Cannot find a valid box with a VMX file in it")) - sys.exit(1) + 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', @@ -377,8 +374,7 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= startupinfo.dwFlags |= subprocess.SW_HIDE | subprocess.STARTF_USESHOWWINDOW proc = subprocess.Popen(cmd, cwd=instance_path, startupinfo=startupinfo) if proc.wait(): - print(colored.red("Cannot extract box")) - sys.exit(1) + sys.exit(colored.red("Cannot extract box")) else: tar = tarfile.open(box_file, 'r') tar.extractall(instance_path) @@ -388,8 +384,7 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= vmx = locate(instance_path, '*.vmx') if not vmx: - print(colored.red("Cannot locate a VMX file")) - sys.exit(1) + sys.exit(colored.red("Cannot locate a VMX file")) update_vmx(vmx, numvcpus=numvcpus, memsize=memsize, no_nat=no_nat) return vmx @@ -506,11 +501,9 @@ def add_box_url(name, box, box_version, url, force=False, save=True, requests_kw finally: os.unlink(the_file.name) except requests.HTTPError as exc: - print(colored.red("Bad response: %s" % exc)) - sys.exit(1) + sys.exit(colored.red("Bad response: %s" % exc)) except requests.ConnectionError: - print(colored.red("Couldn't connect to '%s'" % url)) - sys.exit(1) + sys.exit(colored.red("Couldn't connect to '%s'" % url)) return name, box_version, box @@ -538,11 +531,9 @@ def add_box_file(box=None, box_version=None, filename=None, url=None, force=Fals valid_tar = True break if i.startswith('/') or i.startswith('..'): - print(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: @@ -601,31 +592,36 @@ def get_requests_kwargs(arguments): return requests_kwargs -def provision(instance, vmx, user, password, provision_config, show): +def provision(instance, show=False): """Provision an instance. - provision types: + 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 or instance == '': + if not instance: sys.exit(colored.red("Need to provide an instance to provision().")) - if not vmx or not user or not password: + 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 provision().")) print(colored.green('Provisioning instance:{}'.format(instance))) - vmrun = VMrun(vmx, user, password) + 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 provision_config: - for i, pro in enumerate(provision_config): + if instance.provision: + for i, pro in enumerate(instance.provision): provision_type = pro.get('type') if provision_type == 'file': source = pro.get('source') From 5f7160a7916bd2646bcab1ddb37a68628bb06423 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 20:50:09 -0800 Subject: [PATCH 095/116] refactor variable names/comments --- mech/utils.py | 79 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/mech/utils.py b/mech/utils.py index 443962b..9371529 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -116,7 +116,7 @@ def remove_mechfile_entry(name, mechfile_should_exist=True): def save_mechfile(mechfile): - """Save the mechfile object to a file called '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) @@ -382,12 +382,12 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= if not save and box.startswith(tempfile.gettempdir()): os.unlink(box) - vmx = locate(instance_path, '*.vmx') - if not vmx: + vmx_path = locate(instance_path, '*.vmx') + if not vmx_path: sys.exit(colored.red("Cannot locate a VMX file")) - update_vmx(vmx, numvcpus=numvcpus, memsize=memsize, no_nat=no_nat) - return vmx + 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, @@ -663,36 +663,53 @@ def provision(instance, show=False): print(colored.blue("Nothing to provision")) -def provision_file(virtual_machine, source, destination): +def provision_file(vmrun, source, destination): """Provision from file. - This simply copies a file from host to guest. + + 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))) - return virtual_machine.copy_file_from_host_to_guest(source, destination) + return vmrun.copy_file_from_host_to_guest(source, destination) + +def provision_shell(vmrun, inline, script_path, args=None): + """Provision from shell. -def provision_shell(virtual_machine, inline, path, args=None): - """Provision from shell.""" + Args: + vmrun (VMrun): instance of the VMrun 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 = [] - tmp_path = virtual_machine.create_tempfile_in_guest() - LOGGER.debug('inline:%s path:%s args:%s tmp_path:%s', inline, path, args, tmp_path) + 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): - print(colored.blue("Configuring script {}...".format(path))) - if virtual_machine.copy_file_from_host_to_guest(path, tmp_path) is None: + if script_path and os.path.isfile(script_path): + print(colored.blue("Configuring script {}...".format(script_path))) + 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://')): - print(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: - response = requests.get(path) + response = requests.get(script_path) response.raise_for_status() inline = response.read() except requests.HTTPError: @@ -700,7 +717,7 @@ def provision_shell(virtual_machine, inline, path, args=None): except requests.ConnectionError: return else: - print(colored.red("Cannot open {}".format(path))) + print(colored.red("Cannot open {}".format(script_path))) return if not inline: @@ -712,25 +729,25 @@ def provision_shell(virtual_machine, inline, path, args=None): try: the_file.write(str.encode(inline)) the_file.close() - if virtual_machine.copy_file_from_host_to_guest(the_file.name, tmp_path) is None: + if vmrun.copy_file_from_host_to_guest(the_file.name, tmp_path) is None: return finally: os.unlink(the_file.name) print(colored.blue("Configuring environment...")) - if virtual_machine.run_script_in_guest('/bin/sh', "chmod +x '{}'".format(tmp_path)) is None: + if vmrun.run_script_in_guest('/bin/sh', "chmod +x '{}'".format(tmp_path)) is None: print(colored.red("Warning: Could not configure script in the environment.")) return print(colored.blue("Executing program...")) - return virtual_machine.run_program_in_guest(tmp_path, args) + return vmrun.run_program_in_guest(tmp_path, args) finally: - virtual_machine.delete_file_in_guest(tmp_path, quiet=True) + vmrun.delete_file_in_guest(tmp_path, quiet=True) def config_ssh_string(config_ssh): - """Build the ssh-config string.""" + """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': @@ -739,6 +756,12 @@ def config_ssh_string(config_ssh): 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: @@ -794,7 +817,7 @@ def get_win32_executable(): return get_fallback_executable() -def get_provider(vmrun_exe): +def get_provider(vmrun_executable): """ Identifies the right hosttype for vmrun command (ws | fusion | player) """ @@ -803,12 +826,14 @@ def get_provider(vmrun_exe): 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_exe, + proc = subprocess.Popen([vmrun_executable, '-T', provider, 'list'], From 3ec0d52b09a307e879d2789c8527aabacbc8888e Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 20:54:56 -0800 Subject: [PATCH 096/116] add badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cb77f73..743fb8c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # mech +![](https://github.com/actions/mkinney/mech/workflows/Python%20packaage/badge.svg) + 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. From 716641943cd634cb4f8aa6c62f07ac277300ec35 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 20:57:09 -0800 Subject: [PATCH 097/116] fix badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 743fb8c..c1f757d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # mech -![](https://github.com/actions/mkinney/mech/workflows/Python%20packaage/badge.svg) +![](https://github.com/mkinney/mech/workflows/Python%20packaage/badge.svg) 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. From 2b496ccd38e35a4cc16b4409bf3426fef4753ca6 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 20:58:11 -0800 Subject: [PATCH 098/116] retrying badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c1f757d..eedc2e0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # mech -![](https://github.com/mkinney/mech/workflows/Python%20packaage/badge.svg) +![Python package](https://github.com/mkinney/mech/workflows/Python%20package/badge.svg?branch=multi-pr) 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. From 84c374dd652ad619fb2f43391a2580c672bcfb77 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 21:10:27 -0800 Subject: [PATCH 099/116] add codecov.io --- .github/workflows/python-package.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 51df5e4..bdc619f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -20,7 +20,6 @@ jobs: 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 @@ -30,3 +29,15 @@ jobs: - 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 From b4eecccedd45d7778868f4ee74b5faee6e6ed18f Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sat, 15 Feb 2020 21:19:43 -0800 Subject: [PATCH 100/116] add codecov badge in readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index eedc2e0..b8de45d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # mech ![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. From d336310bcd00cb79ec07172ca4e68fe688f7489d Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 16 Feb 2020 10:28:58 -0800 Subject: [PATCH 101/116] add init from json int test --- mech/test_int.py | 13 +++- mech/utils.py | 4 +- tests/int/README.md | 2 +- tests/int/all | 2 +- ..._from_file.bats => init_from_boxfile.bats} | 8 +-- tests/int/init_from_jsonfile.bats | 63 +++++++++++++++++++ .../bento_ubuntu_18-04.json | 21 +++++++ 7 files changed, 103 insertions(+), 10 deletions(-) rename tests/int/{init_from_file.bats => init_from_boxfile.bats} (89%) create mode 100755 tests/int/init_from_jsonfile.bats create mode 100644 tests/int/init_from_jsonfile/bento_ubuntu_18-04.json diff --git a/mech/test_int.py b/mech/test_int.py index 1462b38..19ab866 100644 --- a/mech/test_int.py +++ b/mech/test_int.py @@ -61,9 +61,16 @@ def test_int_two_ubuntu(): @pytest.mark.int -def test_int_init_from_file(): - """Test init_from_file tests.""" - return_value, out = subprocess.getstatusoutput('cd tests/int && ./init_from_file.bats') +def test_int_init_from_boxfile(): + """Test init_from_boxfile tests.""" + return_value, out = subprocess.getstatusoutput('cd tests/int && ./init_from_boxfile.bats') + assert return_value == 0 + + +@pytest.mark.int +def test_int_init_from_jsonfile(): + """Test init_from_jsonfile tests.""" + return_value, out = subprocess.getstatusoutput('cd tests/int && ./init_from_jsonfile.bats') assert return_value == 0 diff --git a/mech/utils.py b/mech/utils.py index 9371529..1414e3b 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -260,9 +260,11 @@ def build_mechfile_entry(location, box=None, name=None, box_version=None, # if an exception is not thrown, then set values and continue # to the end of the function catalog = json.loads(the_file.read()) - except ValueError: # includes simplejson.decoder.JSONDecodeError + LOGGER.debug('catalog:%s', catalog) + except ValueError as e: # includes simplejson.decoder.JSONDecodeError # this means the location/file is probably a .box file LOGGER.debug('mechfile_entry:%s', mechfile_entry) + LOGGER.debug(e) return mechfile_entry except IOError: # cannot open file diff --git a/tests/int/README.md b/tests/int/README.md index 7dd531b..f6746a2 100644 --- a/tests/int/README.md +++ b/tests/int/README.md @@ -15,7 +15,7 @@ files from the internet and start up/stop/destroy VMs. # Scripts -- `./init_from_file.bats` - create box from file +- `./init_from_boxfile.bats` - create box from file - `./no_mechfile.bats` - simple validations without a Mechfile - `./provision.bats` - provision validation - `./quick.bats` - quick validations (ex: help, version) diff --git a/tests/int/all b/tests/int/all index de00476..5b9db17 100755 --- a/tests/int/all +++ b/tests/int/all @@ -7,7 +7,7 @@ ./no_mechfile.bats ./simple.bats ./two_ubuntu.bats -./init_from_file.bats +./init_from_boxfile.bats ./provision.bats ./shared_folders.bats ./add_and_remove_instances.bats diff --git a/tests/int/init_from_file.bats b/tests/int/init_from_boxfile.bats similarity index 89% rename from tests/int/init_from_file.bats rename to tests/int/init_from_boxfile.bats index 184bb21..637bde4 100755 --- a/tests/int/init_from_file.bats +++ b/tests/int/init_from_boxfile.bats @@ -1,12 +1,12 @@ #!/usr/bin/env bats # -# init_from_file.bats - init from a box file +# init_from_boxfile.bats - init from a box file # # Note: must be run from this directory -# like this: ./init_from_file.bats +# like this: ./init_from_boxfile.bats -@test "mech init, up, destroy of ubuntu from file" { - cd init_from_file +@test "mech init, up, destroy of ubuntu from box file" { + cd init_from_boxfile ubuntu='ubuntu-18.04' box_file="/tmp/${ubuntu}.box" diff --git a/tests/int/init_from_jsonfile.bats b/tests/int/init_from_jsonfile.bats new file mode 100755 index 0000000..3a2838c --- /dev/null +++ b/tests/int/init_from_jsonfile.bats @@ -0,0 +1,63 @@ +#!/usr/bin/env bats +# +# init_from_jsonfile.bats - init from a json file +# +# Note: must be run from this directory +# like this: ./init_from_jsonfile.bats + +@test "mech init, up, destroy of ubuntu from json file" { + cd init_from_jsonfile + + ubuntu='ubuntu-18.04' + box_file="/tmp/${ubuntu}.box" + # setup + # ensure there is no Mechfile first + if [ -f Mechfile ]; then + rm -f Mechfile + fi + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + # download the file if we don't have it already + # that way we "cache" the file + if ! [ -f "$box_file" ]; then + wget -O "$box_file" "https://vagrantcloud.com/bento/boxes/${ubuntu}/versions/201912.04.0/providers/vmware_desktop.box" + fi + + run mech init --box "bento/${ubuntu}" "file:bento_ubuntu_18-04.json" + regex1="Initializing" + regex2="has been initialized" + regex3="mech up" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + [ -f Mechfile ] + + run mech up + regex1="Checking box" + regex2="Extracting" + regex3="Added network" + regex4="Bringing machine" + regex5="Getting IP" + regex6="Sharing folders" + regex7="started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + [[ "$output" =~ $regex4 ]] + [[ "$output" =~ $regex5 ]] + [[ "$output" =~ $regex6 ]] + [[ "$output" =~ $regex7 ]] + + run mech destroy -f + regex1="Deleting" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # clean up + rm Mechfile || true + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + cd .. +} diff --git a/tests/int/init_from_jsonfile/bento_ubuntu_18-04.json b/tests/int/init_from_jsonfile/bento_ubuntu_18-04.json new file mode 100644 index 0000000..9b39b3f --- /dev/null +++ b/tests/int/init_from_jsonfile/bento_ubuntu_18-04.json @@ -0,0 +1,21 @@ +{ + "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 + } + ] + } + ] +} From bc7477311109b5b82c164d52575c7afa31694d10 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 16 Feb 2020 11:45:06 -0800 Subject: [PATCH 102/116] add more utils unittests --- mech/test_utils.py | 110 +++++++++++++++++++++++++++++++++++++++++++++ mech/utils.py | 4 +- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/mech/test_utils.py b/mech/test_utils.py index 408bf91..a2cccbc 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -29,6 +29,71 @@ def test_mech_dir(mock_os_getcwd): 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') @@ -612,3 +677,48 @@ def test_provision_with_unknown_type(mock_installed_tools, capfd): 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' + mock_os_path_exists = False + with patch.dict('os.environ', {'PATH': '/tmp:/tmp2'}): + got = mech.utils.get_darwin_executable() + assert expected == got diff --git a/mech/utils.py b/mech/utils.py index 1414e3b..112a129 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -204,6 +204,7 @@ def load_mechfile(should_exist=True): return mechfile except ValueError: print(colored.red("Invalid Mechfile." + os.linesep)) + return {} else: if should_exist: sys.exit(colored.red(textwrap.fill( @@ -261,8 +262,9 @@ def build_mechfile_entry(location, box=None, name=None, box_version=None, # to the end of the function catalog = json.loads(the_file.read()) LOGGER.debug('catalog:%s', catalog) - except ValueError as e: # includes simplejson.decoder.JSONDecodeError + 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 From f40c0eea420ae3ee7e28e039165ecbd3776a2bc0 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 16 Feb 2020 12:41:40 -0800 Subject: [PATCH 103/116] add more unittests for utils --- mech/test_utils.py | 34 +++++++++++++++++++++++++++++++++- mech/utils.py | 6 +++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/mech/test_utils.py b/mech/test_utils.py index a2cccbc..08d1f9b 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -718,7 +718,39 @@ def side_effect(filename): return True mock_os_path_exists.side_effect = side_effect expected = '/tmp/vmrun' - mock_os_path_exists = False 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 + expected = None + with raises(SystemExit): + got = mech.utils.init_box(name='first') + assert got == expected + + +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 diff --git a/mech/utils.py b/mech/utils.py index 112a129..e5d9e0c 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -456,8 +456,12 @@ def add_box_url(name, box, box_version, url, force=False, save=True, requests_kw LOGGER.debug('name:%s box:%s box_version:%s url:%s', name, box, box_version, url) boxname = os.path.basename(url) 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', - box_parts[0], box_parts[1], box_version))) + first_box_part, second_box_part, box_version))) exists = os.path.exists(box_dir) if not exists or force: if exists: From 86480618c3db57df4ec48703b3ef90a59d506ba5 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 16 Feb 2020 13:01:29 -0800 Subject: [PATCH 104/116] remove options that are not implemented --- README.md | 6 ------ _mech | 13 ------------- mech/conftest.py | 18 ----------------- mech/mech.py | 30 ++--------------------------- mech/mech_box.py | 9 +-------- mech/test_mech.py | 48 ---------------------------------------------- mech/utils.py | 14 -------------- mech_completion.sh | 6 +++--- 8 files changed, 6 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index b8de45d..3302b9b 100644 --- a/README.md +++ b/README.md @@ -55,12 +55,6 @@ Options: --gui Start GUI --disable-shared-folder Do not share folder with VM --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) --no-cache Do not save the downloaded box --memsize 1024 Specify the size of memory for VM --numvcpus 1 Specify the number of vcpus for VM diff --git a/_mech b/_mech index a88823d..68dfb77 100644 --- a/_mech +++ b/_mech @@ -97,16 +97,10 @@ function _mech_init { _arguments \ "--box=[BOXNAME Name of the box (ex: bento/ubuntu-18.04)]" \ "--box-version=[VERSION Constrain version of the added box]" \ - "--cacert=[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)]" \ "-f[Overwrite exsting Mechfile]" \ "--force[Overwrite existing Mechfile]" \ "-h[Show help information]" \ "--help[Show help information]" \ - "--insecure[Do not validate SSL certificates]" \ "--name=[NAME Name of the instance (ex: first)]" } @@ -149,7 +143,6 @@ function _mech_port { "--guest[PORT Output the host port that maps to the given guest port]" \ "-h[Show help information]" \ "--help[Show help information]" \ - "--machine-readable[Display machine-readable output]" \ '::options:->options' case $state in @@ -284,17 +277,11 @@ function _mech_up { typeset -A opt_args _arguments -C \ - "--cacert=[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)]" \ "--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]" \ - "--insecure[Do not validate SSL certificates]" \ "--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)]" \ diff --git a/mech/conftest.py b/mech/conftest.py index 8b84859..9031333 100644 --- a/mech/conftest.py +++ b/mech/conftest.py @@ -85,13 +85,7 @@ def mech_add_arguments(): """Return the default 'mech add' arguments.""" return { '--force': False, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, '--box-version': None, - '--checksum': None, - '--checksum-type': None, '--name': None, '--box': None, '': None, @@ -103,13 +97,7 @@ def mech_box_arguments(): """Return the default 'mech box' arguments.""" return { '--force': False, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, '--box-version': None, - '--checksum': None, - '--checksum-type': None, '': None, } @@ -119,13 +107,7 @@ def mech_init_arguments(): """Return the default 'mech init' arguments.""" return { '--force': False, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, '--box-version': None, - '--checksum': None, - '--checksum-type': None, '--name': None, '--box': None, '': None, diff --git a/mech/mech.py b/mech/mech.py index c1e5f37..eab541c 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -125,14 +125,8 @@ def init(self, arguments): # pylint: disable=no-self-use Options: --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) --box-version VERSION Constrain version of the added box - --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) -f, --force Overwrite existing Mechfile -h, --help Print this help - --insecure Do not validate SSL certificates --name INSTANCE Name of the instance (myinst1) """ name = arguments['--name'] @@ -144,7 +138,6 @@ def init(self, arguments): # pylint: disable=no-self-use 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) @@ -158,8 +151,7 @@ def init(self, arguments): # pylint: disable=no-self-use location=location, box=box, name=name, - box_version=box_version, - requests_kwargs=requests_kwargs) + box_version=box_version) 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!"))) @@ -175,12 +167,6 @@ def add(self, arguments): # pylint: disable=no-self-use Options: --box BOXNAME Name of the box (ex: bento/ubuntu-18.04) --box-version VERSION Constrain version of the added box - --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) - --insecure Do not validate SSL certificates -h, --help Print this help """ name = arguments[''] @@ -191,8 +177,6 @@ def add(self, arguments): # pylint: disable=no-self-use if not name or name == "": sys.exit(colored.red("Need to provide a name for the instance to add to the Mechfile.")) - requests_kwargs = utils.get_requests_kwargs(arguments) - 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))) @@ -201,8 +185,7 @@ def add(self, arguments): # pylint: disable=no-self-use location=location, box=box, name=name, - box_version=box_version, - requests_kwargs=requests_kwargs) + box_version=box_version) print(colored.green("Added to the Mechfile.")) def remove(self, arguments): @@ -251,15 +234,9 @@ def up(self, arguments): # pylint: disable=invalid-name To change shared folders, modify the Mechfile directly. Options: - --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) --disable-provisioning Do not provision --disable-shared-folders Do not share folders with VM --gui Start GUI - --insecure Do not validate SSL certificates --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) @@ -270,7 +247,6 @@ def up(self, arguments): # pylint: disable=invalid-name disable_shared_folders = arguments['--disable-shared-folders'] disable_provisioning = arguments['--disable-provisioning'] save = not arguments['--no-cache'] - requests_kwargs = utils.get_requests_kwargs(arguments) memsize = arguments['--memsize'] numvcpus = arguments['--numvcpus'] @@ -303,7 +279,6 @@ def up(self, arguments): # pylint: disable=invalid-name box_version=inst.box_version, location=location, instance_path=inst.path, - requests_kwargs=requests_kwargs, save=save, numvcpus=numvcpus, memsize=memsize, @@ -884,7 +859,6 @@ def port(self, arguments): Options: --guest PORT Output the host port that maps to the given guest port -h, --help Print this help - --machine-readable Display machine-readable output """ instance_name = arguments[''] diff --git a/mech/mech_box.py b/mech/mech_box.py index c774b6b..3a2cda3 100644 --- a/mech/mech_box.py +++ b/mech/mech_box.py @@ -64,12 +64,6 @@ def add(self, arguments): # pylint: disable=no-self-use Options: --box-version VERSION Constrain version of the added box - --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) - --insecure Do not validate SSL certificates -f, --force Overwrite an existing box if it exists -h, --help Print this help """ @@ -78,9 +72,8 @@ def add(self, arguments): # pylint: disable=no-self-use box_version = arguments['--box-version'] force = arguments['--force'] - requests_kwargs = utils.get_requests_kwargs(arguments) utils.add_box(name=None, box=None, location=location, box_version=box_version, - force=force, requests_kwargs=requests_kwargs) + force=force) def list(self, arguments): # pylint: disable=no-self-use,unused-argument """ diff --git a/mech/test_mech.py b/mech/test_mech.py index 004d7fd..e08ed14 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -805,12 +805,6 @@ def test_mech_up_without_name(mock_load_mechfile): '--gui': False, '--disable-shared-folders': False, '--disable-provisioning': False, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--checksum': None, - '--checksum-type': None, '--no-cache': None, '--memsize': None, '--numvcpus': None, @@ -833,12 +827,6 @@ def test_mech_up_with_name_not_in_mechfile(mock_load_mechfile, '--gui': False, '--disable-shared-folders': False, '--disable-provisioning': False, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--checksum': None, - '--checksum-type': None, '--no-cache': None, '--memsize': None, '--numvcpus': None, @@ -865,12 +853,6 @@ def test_mech_up_already_started(mock_locate, mock_load_mechfile, mock_init_box, '--gui': False, '--disable-shared-folders': True, '--disable-provisioning': True, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--checksum': None, - '--checksum-type': None, '--no-cache': None, '--memsize': None, '--numvcpus': None, @@ -904,12 +886,6 @@ def test_mech_up_already_started_but_could_not_get_ip(mock_locate, mock_load_mec '--gui': False, '--disable-shared-folders': True, '--disable-provisioning': True, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--checksum': None, - '--checksum-type': None, '--no-cache': None, '--memsize': None, '--numvcpus': None, @@ -942,12 +918,6 @@ def test_mech_up_already_started_but_on_unknnown_ip(mock_locate, mock_load_mechf '--gui': False, '--disable-shared-folders': True, '--disable-provisioning': True, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--checksum': None, - '--checksum-type': None, '--no-cache': None, '--memsize': None, '--numvcpus': None, @@ -979,12 +949,6 @@ def test_mech_up_problem(mock_locate, mock_load_mechfile, mock_init_box, '--gui': False, '--disable-shared-folders': True, '--disable-provisioning': True, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--checksum': None, - '--checksum-type': None, '--no-cache': None, '--memsize': None, '--numvcpus': None, @@ -1017,12 +981,6 @@ def test_mech_up_with_provisioning(mock_locate, mock_load_mechfile, mock_init_bo '--gui': False, '--disable-shared-folders': True, '--disable-provisioning': False, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--checksum': None, - '--checksum-type': None, '--no-cache': None, '--memsize': None, '--numvcpus': None, @@ -1058,12 +1016,6 @@ def test_mech_up_wth_shared_folders(mock_locate, mock_load_mechfile, mock_init_b '--gui': False, '--disable-shared-folders': False, '--disable-provisioning': True, - '--insecure': False, - '--cacert': None, - '--capath': None, - '--cert': None, - '--checksum': None, - '--checksum-type': None, '--no-cache': None, '--memsize': None, '--numvcpus': None, diff --git a/mech/utils.py b/mech/utils.py index e5d9e0c..817bfb3 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -586,20 +586,6 @@ def add_to_mechfile(location=None, box=None, name=None, box_version=None, reques return save_mechfile_entry(this_mech_entry, name, mechfile_should_exist=False) -def get_requests_kwargs(arguments): - """Get the requests key word 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 provision(instance, show=False): """Provision an instance. diff --git a/mech_completion.sh b/mech_completion.sh index 8a24a54..27dabf1 100644 --- a/mech_completion.sh +++ b/mech_completion.sh @@ -43,7 +43,7 @@ _mech() { return 0 ;; init) - local commands="--box --box-version --cacert --capath --cert --checksum --checksum-type --force -h --help --insecure --name" + local commands="--box --box-version --force -h --help --name" COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) ) return 0 ;; @@ -63,7 +63,7 @@ _mech() { return 0 ;; port) - local commands="--guest -h --help --machine-readable" + local commands="--guest -h --help" COMPREPLY=( $(compgen -W "${commands} $(get_instances)" -- ${cur}) ) return 0 ;; @@ -97,7 +97,7 @@ _mech() { return 0 ;; up) - local commands="--cacert --capath --cert --checksum --checksum-type --disable-provisioning --disable-shared-folders --gui -h --help --insecure --memsize --no-cache --no-nat --numvcpus" + 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 ;; From 00aba98269c0bd3c767d9fb18a6e42f97784e901 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 16 Feb 2020 14:06:36 -0800 Subject: [PATCH 105/116] add upgradevm option; but there seems to be a bug in vmrun --- mech/mech.py | 37 ++++++++++++++++++++++++++ mech/test_mech.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++- mech/utils.py | 2 +- mech/vmrun.py | 4 ++- 4 files changed, 107 insertions(+), 3 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index eab541c..2cfdeda 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -76,6 +76,7 @@ class Mech(MechCommand): 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` @@ -509,6 +510,42 @@ def pause(self, arguments): else: print(colored.red("VM ({}) not created.".format(instance))) + def upgrade(self, arguments): + """ + Upgrade the vm file format and virtual hardware for the instance(s). + + Usage: mech upgrade [options] [] + + Note: The VMs must be created and stopped. + + 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) + + if inst.created: + vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + 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: + print(colored.red("VM ({}) not created.".format(instance))) + def resume(self, arguments): """ Resume a paused/suspended instances. diff --git a/mech/test_mech.py b/mech/test_mech.py index e08ed14..69ea5b6 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -590,7 +590,7 @@ def test_mech_pause(mock_locate, mock_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' powered on.""" + """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) @@ -605,6 +605,71 @@ def test_mech_pause_not_created(mock_locate, mock_load_mechfile, assert re.search(r' not created', out, re.MULTILINE) +@patch('mech.vmrun.VMrun.upgradevm', return_value=True) +@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(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') diff --git a/mech/utils.py b/mech/utils.py index 817bfb3..781a1b2 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -606,7 +606,7 @@ def provision(instance, show=False): 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 provision().")) - print(colored.green('Provisioning instance:{}'.format(instance))) + 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 diff --git a/mech/vmrun.py b/mech/vmrun.py index db2d971..6158b1d 100644 --- a/mech/vmrun.py +++ b/mech/vmrun.py @@ -587,7 +587,9 @@ 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 install_tools(self, quiet=False): From 533b81aba2c8cf3baebba5bfbfc1b66723bc5153 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 16 Feb 2020 14:14:03 -0800 Subject: [PATCH 106/116] add a few more lines of unittest coverage --- mech/test_mech.py | 76 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/mech/test_mech.py b/mech/test_mech.py index 69ea5b6..d0e1b86 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -492,6 +492,26 @@ def test_mech_suspend(mock_locate, mock_load_mechfile, 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, @@ -605,13 +625,39 @@ def test_mech_pause_not_created(mock_locate, mock_load_mechfile, assert re.search(r' not created', out, re.MULTILINE) -@patch('mech.vmrun.VMrun.upgradevm', return_value=True) +@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(mock_locate, mock_load_mechfile, - mock_check_tools_state, mock_vmrun_upgradevm, - capfd, mechfile_two_entries): +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} @@ -1525,6 +1571,28 @@ def test_mech_scp_guest_to_host(mock_locate, 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} From c4611da89d37b737c3ae770cc8a150b4ba257958 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 16 Feb 2020 14:46:51 -0800 Subject: [PATCH 107/116] add some unittests to vmrun --- mech/test_utils.py | 4 +--- mech/test_vmrun.py | 29 +++++++++++++++++++++++++++++ mech/vmrun.py | 2 +- 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 mech/test_vmrun.py diff --git a/mech/test_utils.py b/mech/test_utils.py index 08d1f9b..fbb4350 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -734,10 +734,8 @@ def test_catalog_to_mechfile_when_empty_catalog(): def test_init_box_cannot_find_valid_box(mock_locate): """Test init_box.""" mock_locate.return_value = None - expected = None with raises(SystemExit): - got = mech.utils.init_box(name='first') - assert got == expected + mech.utils.init_box(name='first') def test_add_mechfile_with_empty_mechfile(): diff --git a/mech/test_vmrun.py b/mech/test_vmrun.py new file mode 100644 index 0000000..5571304 --- /dev/null +++ b/mech/test_vmrun.py @@ -0,0 +1,29 @@ +# Copyright (c) 2020 Mike Kinney + +"""Tests for VMrun class.""" + +from unittest.mock import patch, MagicMock + +import mech.vmrun + + +@patch('mech.vmrun.VMrun.vmrun', return_value='') +def test_vmrun_start(mock_vmrun): + """Test start method.""" + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', user='auser', password='apass') + got = vmrun.start() + mock_vmrun.assert_called() + assert got == '' + + +@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', user='auser', password='apass') + got = vmrun.vmrun('list', vmrun.vmx_file) + assert got is None + mock_popen.assert_called() diff --git a/mech/vmrun.py b/mech/vmrun.py index 6158b1d..8d67fce 100644 --- a/mech/vmrun.py +++ b/mech/vmrun.py @@ -46,7 +46,7 @@ class VMrun(): # pylint: disable=too-many-public-methods def __init__(self, vmx_file=None, # pylint: disable=too-many-arguments user=None, password=None, executable=None, provider=None): - """Constructof for the instance. Set some sane defaults.""" + """Constructor - set sane defaults.""" self.vmx_file = vmx_file self.user = user self.password = password From 96d506b760d9f3984b9f0e4ceac2267063493e97 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 16 Feb 2020 14:52:33 -0800 Subject: [PATCH 108/116] set executable/provider in test --- mech/test_vmrun.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mech/test_vmrun.py b/mech/test_vmrun.py index 5571304..3244078 100644 --- a/mech/test_vmrun.py +++ b/mech/test_vmrun.py @@ -10,7 +10,8 @@ @patch('mech.vmrun.VMrun.vmrun', return_value='') def test_vmrun_start(mock_vmrun): """Test start method.""" - vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', user='auser', password='apass') + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', user='auser', + password='apass', executable='/tmp/vmrun', provider='ws') got = vmrun.start() mock_vmrun.assert_called() assert got == '' @@ -23,7 +24,8 @@ def test_vmrun_vmrun(mock_popen): 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', user='auser', password='apass') + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', user='auser', password='apass', + executable='/tmp/vmrun', provider='ws') got = vmrun.vmrun('list', vmrun.vmx_file) assert got is None mock_popen.assert_called() From c41cdde7619b24a4056532c777002ec8ab60df20 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 16 Feb 2020 15:35:43 -0800 Subject: [PATCH 109/116] added unit test for vmrun.get_guest_ip_address --- mech/test_vmrun.py | 39 ++++++++++++++++++++++++++++++++++++++- mech/vmrun.py | 26 ++++++++++++++++---------- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/mech/test_vmrun.py b/mech/test_vmrun.py index 3244078..4cf02b0 100644 --- a/mech/test_vmrun.py +++ b/mech/test_vmrun.py @@ -2,7 +2,7 @@ """Tests for VMrun class.""" -from unittest.mock import patch, MagicMock +from unittest.mock import patch, MagicMock, mock_open import mech.vmrun @@ -29,3 +29,40 @@ def test_vmrun_vmrun(mock_popen): got = vmrun.vmrun('list', vmrun.vmx_file) assert got is None mock_popen.assert_called() + + +@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', user='auser', password='apass', + 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', user='auser', password='apass', + 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() diff --git a/mech/vmrun.py b/mech/vmrun.py index 8d67fce..69df056 100644 --- a/mech/vmrun.py +++ b/mech/vmrun.py @@ -541,26 +541,32 @@ def read_variable(self, var_name, mode=None, quiet=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.run_script_in_guest( - '/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) + 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: temp_file.close() - self.copy_file_from_guest_to_host('/tmp/ip_address', temp_file.name, quiet=quiet) + 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(temp_file.name) - ip_address = self.vmrun('getGuestIPAddress', self.vmx_file, - '-wait' if wait else None, quiet=quiet) - if ip_address == 'unknown': - ip_address = '' + 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 ############################################################################ From 1f638302820b0faf2a109e878e265c81951b9c4d Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 16 Feb 2020 16:05:11 -0800 Subject: [PATCH 110/116] only create vmrun object with user/pass when we need to --- mech/mech.py | 28 +++++++++++++++------------- mech/mech_instance.py | 1 + mech/mech_snapshot.py | 6 +++--- mech/test_vmrun.py | 10 +++++----- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/mech/mech.py b/mech/mech.py index 2cfdeda..96d32d5 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -288,7 +288,8 @@ def up(self, arguments): # pylint: disable=invalid-name inst.vmx = vmx inst.created = True - vmrun = VMrun(vmx, user=inst.user, password=inst.password) + # 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: @@ -345,6 +346,7 @@ def ps(self, arguments): # pylint: disable=invalid-name,no-self-use 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: @@ -375,7 +377,7 @@ def status(self, arguments): inst = MechInstance(instance) if inst.created: - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) lookup = inst.enable_ip_lookup ip_address = vmrun.get_guest_ip_address(wait=False, quiet=True, lookup=lookup) @@ -428,7 +430,7 @@ def destroy(self, arguments): 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, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) vmrun.stop(mode='hard', quiet=True) vmrun.delete_vm() if os.path.exists(inst.path): @@ -464,7 +466,7 @@ def down(self, arguments): inst = MechInstance(instance) if inst.created: - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) if not force and vmrun.installed_tools(): stopped = vmrun.stop() else: @@ -502,7 +504,7 @@ def pause(self, arguments): inst = MechInstance(instance) if inst.created: - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) if vmrun.pause() is None: print(colored.red("Not paused", vmrun)) else: @@ -534,7 +536,7 @@ def upgrade(self, arguments): inst = MechInstance(instance) if inst.created: - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) state = vmrun.check_tools_state(quiet=True) if state == "running": print("VM must be stopped before doing upgrade.") @@ -576,7 +578,7 @@ def resume(self, arguments): # if we have started this instance before, try to unpause if inst.created: - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) if vmrun.unpause(quiet=True) is not None: print(colored.blue("Getting IP address...")) @@ -594,7 +596,7 @@ def resume(self, arguments): else: # Otherwise try starting - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) started = vmrun.start() if started is None: print(colored.red("VM not started")) @@ -643,7 +645,7 @@ def suspend(self, arguments): inst = MechInstance(instance) if inst.created: - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) if vmrun.suspend() is None: print(colored.red("Not suspended", vmrun)) else: @@ -797,7 +799,7 @@ def ip(self, arguments): # pylint: disable=invalid-name,no-self-use inst = MechInstance(instance) if inst.created: - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) lookup = inst.enable_ip_lookup ip_address = vmrun.get_guest_ip_address(lookup=lookup) if ip_address: @@ -860,7 +862,7 @@ def reload(self, arguments): inst = MechInstance(instance) if inst.created: - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) print(colored.blue("Reloading machine...")) started = vmrun.reset() @@ -912,7 +914,7 @@ def port(self, arguments): print('Instance ({}):'. format(instance)) nat_found = False - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) for line in vmrun.list_host_networks().split('\n'): network = line.split() if len(network) > 2 and network[2] == 'nat': @@ -950,7 +952,7 @@ def list(self, arguments): for name in self.mechfile: inst = MechInstance(name, self.mechfile) if inst.created: - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + 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: diff --git a/mech/mech_instance.py b/mech/mech_instance.py index 34a4031..3fc08a7 100644 --- a/mech/mech_instance.py +++ b/mech/mech_instance.py @@ -124,6 +124,7 @@ def __repr__(self): def config_ssh(self): """Configure ssh to work. Create a insecure private key file for ssh/scp.""" + # Note: need user/password for generating the config file vmrun = VMrun(self.vmx, user=self.user, password=self.password) lookup = self.enable_ip_lookup ip_address = vmrun.get_guest_ip_address(wait=False, diff --git a/mech/mech_snapshot.py b/mech/mech_snapshot.py index bb681cf..09aa5ea 100644 --- a/mech/mech_snapshot.py +++ b/mech/mech_snapshot.py @@ -64,7 +64,7 @@ def delete(self, arguments): # pylint: disable=no-self-use instance = arguments[''] inst = MechInstance(instance) - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) if vmrun.delete_snapshot(name) is None: print(colored.red("Cannot delete name")) else: @@ -95,7 +95,7 @@ def list(self, arguments): inst = MechInstance(instance) print('Snapshots for instance:{}'.format(instance)) if inst.created: - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) print(vmrun.list_snapshots()) else: print(colored.red('Instance ({}) is not created.'.format(instance))) @@ -123,7 +123,7 @@ def save(self, arguments): # pylint: disable=no-self-use inst = MechInstance(instance) if inst.created: - vmrun = VMrun(inst.vmx, user=inst.user, password=inst.password) + vmrun = VMrun(inst.vmx) if vmrun.snapshot(name) is None: sys.exit(colored.red("Warning: Could not take snapshot.")) else: diff --git a/mech/test_vmrun.py b/mech/test_vmrun.py index 4cf02b0..16a2f5a 100644 --- a/mech/test_vmrun.py +++ b/mech/test_vmrun.py @@ -10,8 +10,8 @@ @patch('mech.vmrun.VMrun.vmrun', return_value='') def test_vmrun_start(mock_vmrun): """Test start method.""" - vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', user='auser', - password='apass', executable='/tmp/vmrun', provider='ws') + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', + provider='ws') got = vmrun.start() mock_vmrun.assert_called() assert got == '' @@ -24,7 +24,7 @@ def test_vmrun_vmrun(mock_popen): 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', user='auser', password='apass', + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', provider='ws') got = vmrun.vmrun('list', vmrun.vmx_file) assert got is None @@ -34,7 +34,7 @@ def test_vmrun_vmrun(mock_popen): @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', user='auser', password='apass', + 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' @@ -44,7 +44,7 @@ def test_vmrun_get_guest_ip_address_no_lookup(mock_vmrun): @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', user='auser', password='apass', + vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', provider='ws') got = vmrun.get_guest_ip_address() assert got == '' From 0e931774f344a516368c99a6327aa05015d82449 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Sun, 16 Feb 2020 18:19:32 -0800 Subject: [PATCH 111/116] added add-me option --- mech/conftest.py | 18 +++++++ mech/mech.py | 24 +++++++-- mech/mech_instance.py | 18 ++++--- mech/test_int.py | 7 +++ mech/test_mech.py | 43 ++++++++++++++++ mech/test_utils.py | 9 ++++ mech/utils.py | 115 ++++++++++++++++++++++++++++-------------- tests/int/README.md | 1 + tests/int/all | 1 + tests/int/auth.bats | 107 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 293 insertions(+), 50 deletions(-) create mode 100755 tests/int/auth.bats diff --git a/mech/conftest.py b/mech/conftest.py index 9031333..f4f0cb2 100644 --- a/mech/conftest.py +++ b/mech/conftest.py @@ -17,6 +17,22 @@ def mechfile_one_entry(): } +@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.""" @@ -88,6 +104,7 @@ def mech_add_arguments(): '--box-version': None, '--name': None, '--box': None, + '--add-me': None, '': None, } @@ -110,6 +127,7 @@ def mech_init_arguments(): '--box-version': None, '--name': None, '--box': None, + '--add-me': None, '': None, } diff --git a/mech/mech.py b/mech/mech.py index 96d32d5..6390ff8 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -115,21 +115,27 @@ def init(self, arguments): # pylint: disable=no-self-use Usage: mech init [options] Notes: - The location can be a: + - 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 + - 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. + 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 -f, --force Overwrite existing Mechfile -h, --help Print this help --name INSTANCE Name of the instance (myinst1) """ + add_me = arguments['--add-me'] name = arguments['--name'] box_version = arguments['--box-version'] box = arguments['--box'] @@ -152,7 +158,8 @@ def init(self, arguments): # pylint: disable=no-self-use location=location, box=box, name=name, - box_version=box_version) + box_version=box_version, + add_me=add_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!"))) @@ -165,7 +172,12 @@ def add(self, arguments): # pylint: disable=no-self-use Example box: bento/ubuntu-18.04 + Note: 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 @@ -173,6 +185,7 @@ def add(self, arguments): # pylint: disable=no-self-use name = arguments[''] box_version = arguments['--box-version'] box = arguments['--box'] + add_me = arguments['--add-me'] location = arguments[''] if not name or name == "": @@ -186,7 +199,8 @@ def add(self, arguments): # pylint: disable=no-self-use location=location, box=box, name=name, - box_version=box_version) + box_version=box_version, + add_me=add_me) print(colored.green("Added to the Mechfile.")) def remove(self, arguments): @@ -316,6 +330,8 @@ def up(self, arguments): # pylint: disable=invalid-name "unknown IP address".format(instance))) if not disable_provisioning: utils.provision(inst, show=False) + if inst.auth: + utils.add_auth(inst) # allows "mech start" to alias to "mech up" start = up diff --git a/mech/mech_instance.py b/mech/mech_instance.py index 3fc08a7..d0d63b6 100644 --- a/mech/mech_instance.py +++ b/mech/mech_instance.py @@ -93,6 +93,7 @@ def __init__(self, name, mechfile=None): self.provision = mechfile[name].get('provision', None) self.enable_ip_lookup = False self.config = {} + self.auth = mechfile[name].get('auth', {}) self.shared_folders = mechfile[name].get('shared_folders', []) self.user = DEFAULT_USER self.password = DEFAULT_PASSWORD @@ -114,13 +115,16 @@ def __repr__(self): '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}'. - 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, sep=sep)) + '{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. Create a insecure private key file for ssh/scp.""" diff --git a/mech/test_int.py b/mech/test_int.py index 19ab866..57c948f 100644 --- a/mech/test_int.py +++ b/mech/test_int.py @@ -86,3 +86,10 @@ def test_int_shared_folders(): """Test shared_folders tests.""" return_value, out = subprocess.getstatusoutput('cd tests/int && ./shared_folders.bats') assert return_value == 0 + + +@pytest.mark.int +def test_int_auth(): + """Test auth tests.""" + return_value, out = subprocess.getstatusoutput('cd tests/int && ./auth.bats') + assert return_value == 0 diff --git a/mech/test_mech.py b/mech/test_mech.py index d0e1b86..0cecb6c 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -980,6 +980,48 @@ def test_mech_up_already_started(mock_locate, mock_load_mechfile, mock_init_box, 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.init_box', return_value='/tmp/first/one.vmx') +@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_init_box, + 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, + '': 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_init_box.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.init_box', return_value='/tmp/first/one.vmx') @@ -1327,6 +1369,7 @@ def test_mech_init(mock_os_getcwd, mock_os_path_exists, 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) diff --git a/mech/test_utils.py b/mech/test_utils.py index fbb4350..e37af6c 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -752,3 +752,12 @@ def test_add_box_url(mock_locate, mock_requests_get, catalog_as_json): 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'}} + got = mech.utils.get_info_for_auth() + assert got == expected diff --git a/mech/utils.py b/mech/utils.py index 781a1b2..1e0e6bb 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -225,11 +225,9 @@ def default_shared_folders(): def build_mechfile_entry(location, box=None, name=None, box_version=None, - shared_folders=None, requests_kwargs=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) - if requests_kwargs is None: - requests_kwargs = {} mechfile_entry = {} if location is None: @@ -282,7 +280,7 @@ def build_mechfile_entry(location, box=None, name=None, box_version=None, colored.blue("Loading metadata for box '{}'{}".format( location, " ({})".format(box_version) if box_version else ""))) url = 'https://app.vagrantup.com/{}/boxes/{}'.format(account, box) - response = requests.get(url, **requests_kwargs) + response = requests.get(url) response.raise_for_status() catalog = response.json() except (requests.HTTPError, ValueError) as exc: @@ -339,14 +337,11 @@ def tar_cmd(*args, **kwargs): def init_box(name, box=None, box_version=None, location=None, force=False, save=True, - instance_path=None, requests_kwargs=None, numvcpus=None, - memsize=None, no_nat=False): + 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. """ - if requests_kwargs is None: - requests_kwargs = {} 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( @@ -355,8 +350,7 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= box_version=box_version, location=location, force=force, - save=save, - requests_kwargs=requests_kwargs) + save=save) if not name_version_box: sys.exit(colored.red("Cannot find a valid box with a VMX file in it")) @@ -395,10 +389,8 @@ def init_box(name, box=None, box_version=None, location=None, force=False, save= def add_box(name=None, box=None, box_version=None, location=None, - force=False, save=True, requests_kwargs=None): + force=False, save=True): """Add a box.""" - if requests_kwargs is None: - requests_kwargs = {} # build the dict LOGGER.debug('name:%s box:%s box_version:%s location:%s', name, box, box_version, location) @@ -406,8 +398,7 @@ def add_box(name=None, box=None, box_version=None, location=None, box=box, name=name, location=location, - box_version=box_version, - requests_kwargs=requests_kwargs) + box_version=box_version) return add_mechfile( mechfile_entry, @@ -416,15 +407,12 @@ def add_box(name=None, box=None, box_version=None, location=None, location=location, box_version=box_version, force=force, - save=save, - requests_kwargs=requests_kwargs) + save=save) def add_mechfile(mechfile_entry, name=None, box=None, box_version=None, - location=None, force=False, save=True, requests_kwargs=None): + location=None, force=False, save=True): """Add a mechfile entry.""" - if requests_kwargs is None: - requests_kwargs = {} LOGGER.debug('mechfile_entry:%s name:%s box:%s box_version:%s location:%s', mechfile_entry, name, box, box_version, location) @@ -441,18 +429,15 @@ def add_mechfile(mechfile_entry, name=None, box=None, box_version=None, if url: return add_box_url(name=name, box=box, box_version=box_version, - url=url, force=force, save=save, - requests_kwargs=requests_kwargs) + 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, box, box_version, url, force=False, save=True, requests_kwargs=None): +def add_box_url(name, box, box_version, url, force=False, save=True): """Add a box using the URL.""" - if requests_kwargs is None: - requests_kwargs = {} LOGGER.debug('name:%s box:%s box_version:%s url:%s', name, box, box_version, url) boxname = os.path.basename(url) box_parts = box.split('/') @@ -471,7 +456,7 @@ def add_box_url(name, box, box_version, url, force=False, save=True, requests_kw "Attempting to download...".format(box))) try: print(colored.blue("URL: {}".format(url))) - response = requests.get(url, stream=True, **requests_kwargs) + response = requests.get(url, stream=True) response.raise_for_status() try: length = int(response.headers['content-length']) @@ -499,8 +484,7 @@ def add_box_url(name, box, box_version, url, force=False, save=True, requests_kw name=name, box_version=box_version, force=force, - save=save, - requests_kwargs=requests_kwargs) + save=save) else: # Otherwise it must be a valid box: return add_box_file(box=box, box_version=box_version, @@ -556,36 +540,89 @@ def add_box_file(box=None, box_version=None, filename=None, url=None, force=Fals return box, box_version -def init_mechfile(location=None, box=None, name=None, box_version=None, requests_kwargs=None): +def get_info_for_auth(): + """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}} + + +def init_mechfile(location=None, box=None, name=None, box_version=None, add_me=None): """Initialize the Mechfile.""" - if requests_kwargs is None: - requests_kwargs = {} - LOGGER.debug("name:%s box:%s box_version:%s location:%s", name, box, box_version, location) + LOGGER.debug("name:%s box:%s box_version:%s location:%s add_me:%s", name, box, + box_version, location, add_me) mechfile_entry = build_mechfile_entry( location=location, box=box, name=name, - box_version=box_version, - requests_kwargs=requests_kwargs) + box_version=box_version) + if add_me: + mechfile_entry.update(get_info_for_auth()) 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, requests_kwargs=None): +def add_to_mechfile(location=None, box=None, name=None, box_version=None, add_me=None): """Add entry to the Mechfile.""" - if requests_kwargs is None: - requests_kwargs = {} LOGGER.debug("name:%s box:%s box_version:%s location:%s", name, box, box_version, location) this_mech_entry = build_mechfile_entry( location=location, box=box, name=name, - box_version=box_version, - requests_kwargs=requests_kwargs) + box_version=box_version) + if add_me: + this_mech_entry.update(get_info_for_auth()) LOGGER.debug('this_mech_entry:%s', this_mech_entry) return save_mechfile_entry(this_mech_entry, name, mechfile_should_exist=False) +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: + cmd = ('sudo useradd -m -s/bin/bash {username};' + 'sudo usermod -aG sudo {username};' + 'sudo mkdir /home/{username}/.ssh;' + 'echo "{pub_key_contents}" | ' + 'sudo tee -a /home/{username}/.ssh/authorized_keys;' + '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) + 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 provision(instance, show=False): """Provision an instance. diff --git a/tests/int/README.md b/tests/int/README.md index f6746a2..105d2ee 100644 --- a/tests/int/README.md +++ b/tests/int/README.md @@ -23,3 +23,4 @@ files from the internet and start up/stop/destroy VMs. - `./two_ubuntu.bats` - validate two ubuntu instances - `./shared_folders.bats` - validate shared folders functionality - `./add_and_remove_instances.bats` - validate add/removing instance from Mechfile +- `./auth.bats` - validate using the 'add-me' works as expected diff --git a/tests/int/all b/tests/int/all index 5b9db17..ef21d4b 100755 --- a/tests/int/all +++ b/tests/int/all @@ -11,3 +11,4 @@ ./provision.bats ./shared_folders.bats ./add_and_remove_instances.bats +./auth.bats diff --git a/tests/int/auth.bats b/tests/int/auth.bats new file mode 100755 index 0000000..4a287ce --- /dev/null +++ b/tests/int/auth.bats @@ -0,0 +1,107 @@ +#!/usr/bin/env bats +# +# auth.bats - run some auth tests +# +# Note: must be run from this directory +# like this: ./auth.bats + +@test "mech add-me/auth tests: up, test ssh command, and destroy" { + if ! [ -d auth ]; then + mkdir auth + fi + cd auth + + # setup + # ensure there is no Mechfile first + if [ -f Mechfile ]; then + rm -f Mechfile + fi + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + # Note: The '-a' is the 'add-me' option. + run mech init -a bento/ubuntu-18.04 + regex1="Initializing" + regex2="Loading metadata" + regex3="has been initialized" + regex4="mech up" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + [[ "$output" =~ $regex4 ]] + [ -f Mechfile ] + + run mech list -d + regex1="id_rsa.pub" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech up + regex1="Loading metadata" + regex2="could not be found" + regex3="vmware_desktop" + regex4="integrity filename" + regex5=".vmx" + regex6="Extracting" + regex7="Added network" + regex8="Bringing machine" + regex9="Getting IP" + regex10="Sharing folders" + regex11="started" + regex12="Added auth" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + [[ "$output" =~ $regex4 ]] + [[ "$output" =~ $regex5 ]] + [[ "$output" =~ $regex6 ]] + [[ "$output" =~ $regex7 ]] + [[ "$output" =~ $regex8 ]] + [[ "$output" =~ $regex9 ]] + [[ "$output" =~ $regex10 ]] + [[ "$output" =~ $regex11 ]] + [[ "$output" =~ $regex12 ]] + + # make sure we can re-run 'up' + run mech up + regex1="Bringing machine" + regex2="Getting IP" + regex3="Sharing folders" + regex4="was already started" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + [[ "$output" =~ $regex4 ]] + + # make sure we can run a command + first_ip=`mech ip first` + run ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=QUIET ${first_ip} -C uptime + regex1="load average" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech add -a second bento/ubuntu-18.04 + regex1="Added to the Mechfile" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + run mech up second + regex1="second" + regex2="Added auth" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + + run mech destroy -f + regex1="Deleting" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # clean up + rm Mechfile + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + cd .. +} From ad91f687fa92b8afb63d3c76598137158c73ccb6 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 17 Feb 2020 07:20:34 -0800 Subject: [PATCH 112/116] add unit tests for vmrun class --- mech/test_vmrun.py | 585 ++++++++++++++++++++++++++++++++++++++++++++- mech/vmrun.py | 17 +- 2 files changed, 588 insertions(+), 14 deletions(-) diff --git a/mech/test_vmrun.py b/mech/test_vmrun.py index 16a2f5a..c2c1df1 100644 --- a/mech/test_vmrun.py +++ b/mech/test_vmrun.py @@ -7,16 +7,6 @@ import mech.vmrun -@patch('mech.vmrun.VMrun.vmrun', return_value='') -def test_vmrun_start(mock_vmrun): - """Test start method.""" - vmrun = mech.vmrun.VMrun('/tmp/first/some.vmx', executable='/tmp/vmrun', - provider='ws') - got = vmrun.start() - mock_vmrun.assert_called() - assert got == '' - - @patch('subprocess.Popen') def test_vmrun_vmrun(mock_popen): """Test vmrun method.""" @@ -31,6 +21,393 @@ def test_vmrun_vmrun(mock_popen): 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.""" @@ -66,3 +443,191 @@ def test_vmrun_get_guest_ip_address_lookup(mock_run_script_in_guest, 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/vmrun.py b/mech/vmrun.py index 69df056..6fdf3c1 100644 --- a/mech/vmrun.py +++ b/mech/vmrun.py @@ -45,7 +45,8 @@ class VMrun(): # pylint: disable=too-many-public-methods """ def __init__(self, vmx_file=None, # pylint: disable=too-many-arguments - user=None, password=None, executable=None, provider=None): + user=None, password=None, executable=None, provider=None, + test_mode=False): """Constructor - set sane defaults.""" self.vmx_file = vmx_file self.user = user @@ -65,6 +66,9 @@ def __init__(self, vmx_file=None, # pylint: disable=too-many-arguments 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.""" @@ -91,6 +95,9 @@ def vmrun(self, cmd, *args, **kwargs): "'", "\\'")) if ' ' in c else c for c in cmds)) + if self.test_mode: + return cmds + startupinfo = None if os.name == "nt": startupinfo = subprocess.STARTUPINFO() @@ -472,9 +479,9 @@ def run_script_in_guest( # pylint: disable=too-many-arguments '-interactive' if interactive else None, quiet=quiet) - def delete_file_in_guest(self, file, quiet=False): + 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 create_directory_in_guest(self, path, quiet=False): '''Create a directory in Guest OS''' @@ -712,6 +719,8 @@ def vprobe_list_globals(self, quiet=False): ############################################################################ def installed_tools(self, quiet=False): - '''Return if VMware tools are either 'installed' or 'running'.''' + '''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') From 5f12d24e2eca9256cef0635e9869e25b57ef3dbc Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 17 Feb 2020 12:57:04 -0800 Subject: [PATCH 113/116] add use-me/remove-vagrant options --- mech/conftest.py | 2 + mech/mech.py | 135 +++++++++++-------------- mech/mech_instance.py | 42 +++++--- mech/test_mech.py | 36 +++---- mech/test_utils.py | 4 +- mech/utils.py | 224 ++++++++++++++++++++++++++++++++++++------ mech/vmrun.py | 25 +++-- 7 files changed, 315 insertions(+), 153 deletions(-) diff --git a/mech/conftest.py b/mech/conftest.py index f4f0cb2..f7aa8f5 100644 --- a/mech/conftest.py +++ b/mech/conftest.py @@ -105,6 +105,7 @@ def mech_add_arguments(): '--name': None, '--box': None, '--add-me': None, + '--use-me': None, '': None, } @@ -128,6 +129,7 @@ def mech_init_arguments(): '--name': None, '--box': None, '--add-me': None, + '--use-me': None, '': None, } diff --git a/mech/mech.py b/mech/mech.py index 6390ff8..393b737 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -29,10 +29,8 @@ import os import sys import logging -import tempfile import textwrap import shutil -import subprocess from clint.textui import colored @@ -116,16 +114,18 @@ def init(self, arguments): # pylint: disable=no-self-use 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'). + + 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 @@ -134,8 +134,10 @@ def init(self, arguments): # pylint: disable=no-self-use -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 """ add_me = arguments['--add-me'] + use_me = arguments['--use-me'] name = arguments['--name'] box_version = arguments['--box-version'] box = arguments['--box'] @@ -159,7 +161,8 @@ def init(self, arguments): # pylint: disable=no-self-use box=box, name=name, box_version=box_version, - add_me=add_me) + 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!"))) @@ -172,20 +175,23 @@ def add(self, arguments): # pylint: disable=no-self-use Example box: bento/ubuntu-18.04 - Note: 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. + 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 == "": @@ -200,7 +206,8 @@ def add(self, arguments): # pylint: disable=no-self-use box=box, name=name, box_version=box_version, - add_me=add_me) + add_me=add_me, + use_me=use_me) print(colored.green("Added to the Mechfile.")) def remove(self, arguments): @@ -228,8 +235,9 @@ def remove(self, arguments): else: sys.exit(colored.red("There is no instance called ({}) in the Mechfile.".format(name))) - # add alias for 'mech delete' + # add aliases for 'mech delete' delete = remove + rm = remove def up(self, arguments): # pylint: disable=invalid-name """ @@ -247,6 +255,9 @@ def up(self, arguments): # pylint: disable=invalid-name 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 @@ -257,11 +268,13 @@ def up(self, arguments): # pylint: disable=invalid-name --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'] + remove_vagrant = arguments['--remove-vagrant'] memsize = arguments['--memsize'] numvcpus = arguments['--numvcpus'] @@ -288,18 +301,18 @@ def up(self, arguments): # pylint: disable=invalid-name if not location: location = inst.box_file - 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) - if vmx: - inst.vmx = vmx + # 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 @@ -328,10 +341,17 @@ def up(self, arguments): # pylint: disable=invalid-name else: print(colored.yellow("VM ({}) was already started on an " "unknown IP address".format(instance))) + if not disable_provisioning: utils.provision(inst, show=False) - if inst.auth: + + # 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') # allows "mech start" to alias to "mech up" start = up @@ -714,31 +734,7 @@ def ssh(self, arguments): # pylint: disable=no-self-use inst = MechInstance(instance) if inst.created: - config_ssh = inst.config_ssh() - temp_file = tempfile.NamedTemporaryFile(delete=False) - try: - temp_file.write(utils.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)) - return subprocess.call(cmds) - finally: - os.unlink(temp_file.name) + utils.ssh(inst, command, plain, extra) else: print("VM not created.") @@ -758,46 +754,25 @@ def scp(self, arguments): # pylint: disable=no-self-use 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: sys.exit(colored.red("Both src and dst are host destinations")) if dst_is_host: - instance = dst_instance + instance_name = dst_instance else: dst = dst_instance if src_is_host: - instance = src_instance + instance_name = src_instance else: src = src_instance - inst = MechInstance(instance) + if instance_name is None: + sys.exit(colored.red("Could not determine instance name.")) + + inst = MechInstance(instance_name) if inst.created: - config_ssh = inst.config_ssh() - temp_file = tempfile.NamedTemporaryFile(delete=False) - - try: - temp_file.write(utils.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 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(temp_file.name) + utils.scp(inst, src, dst, dst_is_host, extra) else: print(colored.red('VM not created.')) diff --git a/mech/mech_instance.py b/mech/mech_instance.py index d0d63b6..c63112e 100644 --- a/mech/mech_instance.py +++ b/mech/mech_instance.py @@ -93,10 +93,11 @@ def __init__(self, name, mechfile=None): self.provision = mechfile[name].get('provision', None) self.enable_ip_lookup = False self.config = {} - self.auth = mechfile[name].get('auth', {}) + 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 @@ -107,6 +108,22 @@ def __init__(self, name, mechfile=None): 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') + 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' @@ -127,9 +144,8 @@ def __repr__(self): auth=self.auth, sep=sep)) def config_ssh(self): - """Configure ssh to work. Create a insecure private key file for ssh/scp.""" - # Note: need user/password for generating the config file - vmrun = VMrun(self.vmx, user=self.user, password=self.password) + """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 @@ -140,12 +156,16 @@ def config_ssh(self): "Additionally, check the output of `mech status` to verify " "that the machine is in the state that you expect."))) - insecure_private_key = os.path.abspath(os.path.join( - utils.mech_dir(), "insecure_private_key")) - if not os.path.exists(insecure_private_key): - with open(insecure_private_key, 'w') as the_file: - the_file.write(INSECURE_PRIVATE_KEY) - os.chmod(insecure_private_key, 0o400) + 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, @@ -153,7 +173,7 @@ def config_ssh(self): "UserKnownHostsFile": "/dev/null", "StrictHostKeyChecking": "no", "PasswordAuthentication": "no", - "IdentityFile": insecure_private_key, + "IdentityFile": key, "IdentitiesOnly": "yes", "LogLevel": "FATAL", } diff --git a/mech/test_mech.py b/mech/test_mech.py index 0cecb6c..a544b29 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -920,6 +920,7 @@ def test_mech_up_without_name(mock_load_mechfile): '--memsize': None, '--numvcpus': None, '--no-nat': None, + '--remove-vagrant': None, '': '', } with raises(AttributeError, match=r"Must provide a name for the instance."): @@ -942,6 +943,7 @@ def test_mech_up_with_name_not_in_mechfile(mock_load_mechfile, '--memsize': None, '--numvcpus': None, '--no-nat': None, + '--remove-vagrant': None, '': 'notfirst', } with raises(SystemExit, match=r" was not found in the Mechfile"): @@ -950,10 +952,9 @@ def test_mech_up_with_name_not_in_mechfile(mock_load_mechfile, @patch('mech.vmrun.VMrun.get_guest_ip_address', return_value="192.168.1.100") @patch('mech.vmrun.VMrun.start', return_value='') -@patch('mech.utils.init_box', return_value='/tmp/first/one.vmx') @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_init_box, +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'.""" @@ -968,12 +969,12 @@ def test_mech_up_already_started(mock_locate, mock_load_mechfile, mock_init_box, '--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_init_box.assert_called() mock_vmrun_start.assert_called() mock_vmrun_get_ip.assert_called() out, _ = capfd.readouterr() @@ -984,10 +985,9 @@ def test_mech_up_already_started(mock_locate, mock_load_mechfile, mock_init_box, @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.init_box', return_value='/tmp/first/one.vmx') @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_init_box, +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, @@ -1004,6 +1004,7 @@ def test_mech_up_already_started_with_add_me(mock_locate, mock_load_mechfile, mo '--memsize': None, '--numvcpus': None, '--no-nat': None, + '--remove-vagrant': None, '': None, } mock_file = mock_open(read_data='some_pub_key_data') @@ -1012,7 +1013,6 @@ def test_mech_up_already_started_with_add_me(mock_locate, mock_load_mechfile, mo mock_file.assert_called() mock_locate.assert_called() mock_load_mechfile.assert_called() - mock_init_box.assert_called() mock_vmrun_start.assert_called() mock_installed_tools.assert_called() mock_run_script_in_guest.assert_called() @@ -1024,11 +1024,9 @@ def test_mech_up_already_started_with_add_me(mock_locate, mock_load_mechfile, mo @patch('mech.vmrun.VMrun.get_guest_ip_address', return_value='') @patch('mech.vmrun.VMrun.start', return_value='') -@patch('mech.utils.init_box', return_value='/tmp/first/one.vmx') @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_init_box, mock_vmrun_start, mock_vmrun_get_ip, capfd, mechfile_one_entry): """Test 'mech up'.""" @@ -1043,12 +1041,12 @@ def test_mech_up_already_started_but_could_not_get_ip(mock_locate, mock_load_mec '--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_init_box.assert_called() mock_vmrun_start.assert_called() mock_vmrun_get_ip.assert_called() out, _ = capfd.readouterr() @@ -1057,10 +1055,9 @@ def test_mech_up_already_started_but_could_not_get_ip(mock_locate, mock_load_mec @patch('mech.vmrun.VMrun.get_guest_ip_address', return_value=False) @patch('mech.vmrun.VMrun.start', return_value=True) -@patch('mech.utils.init_box', return_value='/tmp/first/one.vmx') @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_init_box, +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'.""" @@ -1075,12 +1072,12 @@ def test_mech_up_already_started_but_on_unknnown_ip(mock_locate, mock_load_mechf '--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_init_box.assert_called() mock_vmrun_start.assert_called() mock_vmrun_get_ip.assert_called() out, _ = capfd.readouterr() @@ -1088,10 +1085,9 @@ def test_mech_up_already_started_but_on_unknnown_ip(mock_locate, mock_load_mechf @patch('mech.vmrun.VMrun.start', return_value=None) -@patch('mech.utils.init_box', return_value='/tmp/first/one.vmx') @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_init_box, +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""" @@ -1106,12 +1102,12 @@ def test_mech_up_problem(mock_locate, mock_load_mechfile, mock_init_box, '--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_init_box.assert_called() mock_vmrun_start.assert_called() out, _ = capfd.readouterr() assert re.search(r'not started', out, re.MULTILINE) @@ -1120,10 +1116,9 @@ def test_mech_up_problem(mock_locate, mock_load_mechfile, mock_init_box, @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.init_box', return_value='/tmp/first/one.vmx') @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_init_box, +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'.""" @@ -1138,12 +1133,12 @@ def test_mech_up_with_provisioning(mock_locate, mock_load_mechfile, mock_init_bo '--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_init_box.assert_called() mock_vmrun_start.assert_called() mock_vmrun_get_ip.assert_called() mock_provision.assert_called() @@ -1154,10 +1149,9 @@ def test_mech_up_with_provisioning(mock_locate, mock_load_mechfile, mock_init_bo @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.init_box', return_value='/tmp/first/one.vmx') @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_init_box, +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): @@ -1173,12 +1167,12 @@ def test_mech_up_wth_shared_folders(mock_locate, mock_load_mechfile, mock_init_b '--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_init_box.assert_called() mock_vmrun_start.assert_called() mock_vmrun_get_ip.assert_called() mock_vmrun_enable_shared_folders.assert_called() diff --git a/mech/test_utils.py b/mech/test_utils.py index e37af6c..d30e0c7 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -542,6 +542,7 @@ def test_provision_file_could_not_copy_file_to_guest(mock_installed_tools, 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) @@ -597,6 +598,7 @@ def test_provision_shell(mock_installed_tools, mock_copy_file, 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() @@ -758,6 +760,6 @@ def test_add_box_url(mock_locate, mock_requests_get, catalog_as_json): @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'}} + 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/utils.py b/mech/utils.py index 1e0e6bb..bb342b8 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -29,6 +29,8 @@ import os import re +import random +import string import sys import json import tarfile @@ -540,42 +542,51 @@ def add_box_file(box=None, box_version=None, filename=None, url=None, force=Fals return box, box_version -def get_info_for_auth(): +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}} + 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): +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", name, box, - box_version, location, add_me) + 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()) + 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): +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", name, box, box_version, location) + 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()) + 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.""" @@ -599,14 +610,23 @@ def add_auth(instance): with open(pub_key, 'r') as the_file: pub_key_contents = the_file.read().strip() if pub_key_contents: - cmd = ('sudo useradd -m -s/bin/bash {username};' - 'sudo usermod -aG sudo {username};' + # 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) + ).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) @@ -623,6 +643,112 @@ def add_auth(instance): 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. + """ + 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)) + return subprocess.call(cmds) + 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.call(cmds) + 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.")) + + if instance.user is None: + sys.exit(colored.red("A user is required.")) + + print(colored.green('Removing username ({}) from instance:{}...'.format(username, + instance.name))) + + 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. @@ -640,8 +766,8 @@ def provision(instance, show=False): if not instance: sys.exit(colored.red("Need to provide an instance to provision().")) - 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 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))) @@ -662,7 +788,9 @@ def provision(instance, show=False): "destination:{}".format(instance, provision_type, source, destination))) else: - if provision_file(vmrun, source, destination) is None: + results = provision_file(vmrun, instance, source, destination) + LOGGER.debug('results:%s', results) + if results is None: print(colored.red("Not Provisioned")) return provisioned += 1 @@ -679,7 +807,7 @@ def provision(instance, show=False): "args:{}".format(instance, provision_type, inline, path, args))) else: - if provision_shell(vmrun, inline, path, args) is None: + if provision_shell(vmrun, instance, inline, path, args) is None: print(colored.red("Not Provisioned")) return provisioned += 1 @@ -694,7 +822,7 @@ def provision(instance, show=False): print(colored.blue("Nothing to provision")) -def provision_file(vmrun, source, destination): +def provision_file(vmrun, instance, source, destination): """Provision from file. Args: @@ -707,14 +835,25 @@ def provision_file(vmrun, source, destination): """ print(colored.blue("Copying ({}) to ({})".format(source, destination))) - return vmrun.copy_file_from_host_to_guest(source, destination) + if instance.use_psk: + results = scp(instance, source, '{}:{}'.format(instance.name, 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' + return ssh(instance, cmd) -def provision_shell(vmrun, inline, script_path, args=None): +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 @@ -722,7 +861,10 @@ def provision_shell(vmrun, inline, script_path, args=None): """ if args is None: args = [] - tmp_path = vmrun.create_tempfile_in_guest() + 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: @@ -732,9 +874,15 @@ def provision_shell(vmrun, inline, script_path, args=None): try: if script_path and os.path.isfile(script_path): print(colored.blue("Configuring script {}...".format(script_path))) - 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 + if instance.use_psk: + results = scp(instance, script_path, '{}:{}'.format(instance.name, 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 script_path: if any(script_path.startswith(s) for s in ('https://', 'http://', 'ftp://')): @@ -760,21 +908,37 @@ def provision_shell(vmrun, inline, script_path, args=None): try: the_file.write(str.encode(inline)) the_file.close() - if vmrun.copy_file_from_host_to_guest(the_file.name, tmp_path) is None: - return + if instance.use_psk: + scp(instance, the_file.name, '{}:{}'.format(instance.name, tmp_path), True) + else: + if vmrun.copy_file_from_host_to_guest(the_file.name, tmp_path) is None: + return finally: os.unlink(the_file.name) print(colored.blue("Configuring environment...")) - if vmrun.run_script_in_guest('/bin/sh', "chmod +x '{}'".format(tmp_path)) is None: - print(colored.red("Warning: Could not configure script in the environment.")) - return + make_executable = "chmod +x '{}'".format(tmp_path) + 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 print(colored.blue("Executing program...")) - return vmrun.run_program_in_guest(tmp_path, args) + if instance.use_psk: + args_string = ' '.join([str(elem) for elem in args]) + return ssh(instance, tmp_path, args_string) + else: + return vmrun.run_program_in_guest(tmp_path, args) finally: - vmrun.delete_file_in_guest(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): diff --git a/mech/vmrun.py b/mech/vmrun.py index 6fdf3c1..5a08410 100644 --- a/mech/vmrun.py +++ b/mech/vmrun.py @@ -46,13 +46,14 @@ class VMrun(): # pylint: disable=too-many-public-methods def __init__(self, vmx_file=None, # pylint: disable=too-many-arguments user=None, password=None, executable=None, provider=None, - test_mode=False): + test_mode=False, use_psk=False): """Constructor - set sane defaults.""" self.vmx_file = vmx_file self.user = user self.password = password self.executable = executable self.provider = provider + self.use_psk = use_psk if self.executable is None: if sys.platform == 'darwin': @@ -469,15 +470,19 @@ def run_script_in_guest( # pylint: disable=too-many-arguments 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) + 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''' From afe6089e7db2d77d1a3ab7df572ef9d99ad66197 Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 17 Feb 2020 16:31:40 -0800 Subject: [PATCH 114/116] revamp the ssh output --- CONTRIBUTING.md | 5 ++- mech/mech.py | 11 +++-- mech/mech_instance.py | 2 +- mech/test_int.py | 7 ++++ mech/test_mech.py | 23 ++++++----- mech/test_utils.py | 3 ++ mech/utils.py | 31 +++++++++++---- pytest.ini | 4 ++ tests/int/README.md | 1 + tests/int/all | 1 + tests/int/provision_using_me.bats | 53 +++++++++++++++++++++++++ tests/int/provision_using_me/file1.sh | 1 + tests/int/provision_using_me/file1.txt | 1 + tests/int/provision_using_me/file2.sh | 1 + tests/int/provision_using_me/file2.txt | 1 + tests/int/provision_using_me/readme.txt | 5 +++ 16 files changed, 125 insertions(+), 25 deletions(-) create mode 100755 tests/int/provision_using_me.bats create mode 100644 tests/int/provision_using_me/file1.sh create mode 100644 tests/int/provision_using_me/file1.txt create mode 100644 tests/int/provision_using_me/file2.sh create mode 100644 tests/int/provision_using_me/file2.txt create mode 100644 tests/int/provision_using_me/readme.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93ce1b7..a906b8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ source venv/bin/activate python setup.py install # if doing development -pip install docopt clint requests flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist +pip install docopt clint requests flake8 pytest pytest_mock mock pytest-cov pylint pytest-xdist pytest-timeout # also optional pip install autopep8 @@ -61,6 +61,9 @@ pytest --cov-report html:cov_html --cov=mech -m"int or not int" # 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) diff --git a/mech/mech.py b/mech/mech.py index 393b737..0d8535f 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -342,9 +342,6 @@ def up(self, arguments): # pylint: disable=invalid-name print(colored.yellow("VM ({}) was already started on an " "unknown IP address".format(instance))) - if not disable_provisioning: - utils.provision(inst, show=False) - # if not already using preshared key, switch to it if not inst.use_psk and inst.auth: utils.add_auth(inst) @@ -353,6 +350,9 @@ def up(self, arguments): # pylint: disable=invalid-name 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 @@ -734,7 +734,10 @@ def ssh(self, arguments): # pylint: disable=no-self-use inst = MechInstance(instance) if inst.created: - utils.ssh(inst, command, plain, extra) + rc, stdout, stderr = utils.ssh(inst, command, plain, extra) + LOGGER.debug('command:%s rc:%d stdout:%s stderr:%s', rc, stdout, stderr) + if stdout: + print(stdout) else: print("VM not created.") diff --git a/mech/mech_instance.py b/mech/mech_instance.py index c63112e..f4f133b 100644 --- a/mech/mech_instance.py +++ b/mech/mech_instance.py @@ -117,7 +117,7 @@ def __init__(self, name, mechfile=None): 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') + mech_use = self.auth.get('mech_use', False) username = self.auth.get('username') if username and username != '' and mech_use: self.user = username diff --git a/mech/test_int.py b/mech/test_int.py index 57c948f..f1b27f9 100644 --- a/mech/test_int.py +++ b/mech/test_int.py @@ -81,6 +81,13 @@ def test_int_provision(): assert return_value == 0 +@pytest.mark.int +def test_int_provision_using_me(): + """Test provision using psk tests.""" + return_value, out = subprocess.getstatusoutput('cd tests/int && ./provision_using_me.bats') + assert return_value == 0 + + @pytest.mark.int def test_int_shared_folders(): """Test shared_folders tests.""" diff --git a/mech/test_mech.py b/mech/test_mech.py index a544b29..b6d2ac5 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -533,14 +533,17 @@ def test_mech_suspend_not_created(mock_locate, mock_load_mechfile, @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.130") -@patch('subprocess.call', return_value='00:03:30 up 2 min, load average: 0.00, 0.00, 0.00') +@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_call, mock_get_ip, mock_installed_tools, + 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 = { @@ -553,10 +556,9 @@ def test_mech_ssh(mock_locate, mock_load_mechfile, a_mock = mock_open() with patch('builtins.open', a_mock, create=True): 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() - mock_subprocess_call.assert_called() + mock_subprocess_run.assert_called() mock_installed_tools.assert_called() mock_get_ip.assert_called() mock_chmod.assert_called() @@ -1540,7 +1542,7 @@ def test_mech_ps_not_started_vm(mock_getcwd, mock_locate, assert re.search(r'not created', out, re.MULTILINE) -@patch('subprocess.call', return_value=True) +@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") @@ -1549,7 +1551,7 @@ def test_mech_ps_not_started_vm(mock_getcwd, mock_locate, def test_mech_scp_host_to_guest(mock_locate, mock_load_mechfile, mock_get_ip, mock_installed_tools, mock_chmod, - mock_subprocess_call, + mock_subprocess_run, mechfile_two_entries): """Test 'mech scp'.""" mock_load_mechfile.return_value = mechfile_two_entries @@ -1564,17 +1566,16 @@ def test_mech_scp_host_to_guest(mock_locate, 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_call.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.call', return_value=True) +@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") @@ -1583,7 +1584,7 @@ def test_mech_scp_host_to_guest(mock_locate, def test_mech_scp_guest_to_host(mock_locate, mock_load_mechfile, mock_get_ip, mock_installed_tools, mock_chmod, - mock_subprocess_call, + mock_subprocess_run, mechfile_two_entries): """Test 'mech scp'.""" mock_load_mechfile.return_value = mechfile_two_entries @@ -1601,7 +1602,7 @@ def test_mech_scp_guest_to_host(mock_locate, # Note: Could not figure out how to capture output from subprocess.call. mock_locate.assert_called() mock_load_mechfile.assert_called() - mock_subprocess_call.assert_called() + mock_subprocess_run.assert_called() mock_installed_tools.assert_called() mock_get_ip.assert_called() mock_chmod.assert_called() diff --git a/mech/test_utils.py b/mech/test_utils.py index d30e0c7..e33858c 100644 --- a/mech/test_utils.py +++ b/mech/test_utils.py @@ -497,6 +497,8 @@ def test_provision_file_no_provisioning(mock_installed_tools, mock_provision_fil 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) @@ -519,6 +521,7 @@ def test_provision_file(mock_installed_tools, mock_copy_file, capfd): 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) diff --git a/mech/utils.py b/mech/utils.py index bb342b8..45ee310 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -650,6 +650,7 @@ def ssh(instance, command, plain=None, extra=None): 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) @@ -673,7 +674,16 @@ def ssh(instance, command, plain=None, extra=None): c.replace( "'", "\\'")) if ' ' in c else c for c in cmds)) - return subprocess.call(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) @@ -710,7 +720,7 @@ def scp(instance, src, dst, dst_is_host, extra=None): c.replace( "'", "\\'")) if ' ' in c else c for c in cmds)) - return subprocess.call(cmds) + return subprocess.run(cmds, capture_output=True) finally: os.unlink(temp_file.name) @@ -804,7 +814,7 @@ def provision(instance, show=False): args = [args] if show: print(colored.green(" instance:{} provision_type:{} inline:{} path:{} " - "args:{}".format(instance, provision_type, + "args:{}".format(instance.name, provision_type, inline, path, args))) else: if provision_shell(vmrun, instance, inline, path, args) is None: @@ -817,7 +827,7 @@ def provision(instance, show=False): return else: print(colored.green("VM ({}) Provision {} " - "entries".format(instance, provisioned))) + "entries".format(instance.name, provisioned))) else: print(colored.blue("Nothing to provision")) @@ -836,7 +846,7 @@ def provision_file(vmrun, instance, source, destination): """ print(colored.blue("Copying ({}) to ({})".format(source, destination))) if instance.use_psk: - results = scp(instance, source, '{}:{}'.format(instance.name, destination), True) + results = scp(instance, source, destination, True) else: results = vmrun.copy_file_from_host_to_guest(source, destination) return results @@ -845,7 +855,10 @@ def provision_file(vmrun, instance, source, destination): def create_tempfile_in_guest(instance): """Create a tempfile in the guest.""" cmd = 'tmpfile=$(mktemp); echo $tmpfile' - return ssh(instance, cmd) + result = ssh(instance, cmd) + stdout = result.stdout.decode('utf-8').strip() + LOGGER.debug('MIKE MIKE MIKE result:%s stdout:%s', result, stdout) + return stdout def provision_shell(vmrun, instance, inline, script_path, args=None): @@ -875,7 +888,7 @@ def provision_shell(vmrun, instance, inline, script_path, args=None): 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, '{}:{}'.format(instance.name, tmp_path), True) + results = scp(instance, script_path, tmp_path, True) if results is None: print(colored.red("Warning: Could not copy file to guest.")) return @@ -909,7 +922,7 @@ def provision_shell(vmrun, instance, inline, script_path, args=None): the_file.write(str.encode(inline)) the_file.close() if instance.use_psk: - scp(instance, the_file.name, '{}:{}'.format(instance.name, tmp_path), True) + 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 @@ -918,6 +931,7 @@ def provision_shell(vmrun, instance, inline, script_path, args=None): 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.")) @@ -930,6 +944,7 @@ def provision_shell(vmrun, instance, inline, script_path, args=None): 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) diff --git a/pytest.ini b/pytest.ini index f9f59c2..4b858ae 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,6 +2,10 @@ addopts = -n4 -m "not int" +# Temp uncomment this next line to find the slow unittest +# Note: timout is in seconds +#timeout = 10 + filterwarnings = ignore::DeprecationWarning diff --git a/tests/int/README.md b/tests/int/README.md index 105d2ee..152688c 100644 --- a/tests/int/README.md +++ b/tests/int/README.md @@ -18,6 +18,7 @@ files from the internet and start up/stop/destroy VMs. - `./init_from_boxfile.bats` - create box from file - `./no_mechfile.bats` - simple validations without a Mechfile - `./provision.bats` - provision validation +- `./provision_using_me.bats` - provision using psk validation - `./quick.bats` - quick validations (ex: help, version) - `./simple.bats` - simple validations of most basic functionality - `./two_ubuntu.bats` - validate two ubuntu instances diff --git a/tests/int/all b/tests/int/all index ef21d4b..7b0be4e 100755 --- a/tests/int/all +++ b/tests/int/all @@ -9,6 +9,7 @@ ./two_ubuntu.bats ./init_from_boxfile.bats ./provision.bats +./provision_using_me.bats ./shared_folders.bats ./add_and_remove_instances.bats ./auth.bats diff --git a/tests/int/provision_using_me.bats b/tests/int/provision_using_me.bats new file mode 100755 index 0000000..0e6e60d --- /dev/null +++ b/tests/int/provision_using_me.bats @@ -0,0 +1,53 @@ +#!/usr/bin/env bats +# +# provision_using_me.bats - run provision tests using psk +# +# Note: must be run from this directory +# like this: ./provision_using_me.bats + +@test "provision using psk testing" { + cd provision_using_me + + # setup + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + mech destroy -f || true + + run mech up -r + regex1="VM (first) started" + regex2="VM (second) started" + regex3="VM (third) started" + regex4="Added auth" + regex5="Removing username" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + [[ "$output" =~ $regex4 ]] + [[ "$output" =~ $regex5 ]] + + # validate files were copied + run mech ssh -c "ls -al /tmp/file1.txt" first + regex1="vagrant" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # ensure that provision runs on 'mech up' + run mech up + regex1="VM (first) Provision 2 entries" + regex2="VM (second) Provision 3 entries" + regex3="VM (third) Provision 0 entries" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + [[ "$output" =~ $regex2 ]] + [[ "$output" =~ $regex3 ]] + + run mech destroy -f + regex1="Deleting" + [ "$status" -eq 0 ] + [[ "$output" =~ $regex1 ]] + + # clean up + find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true + + cd .. +} diff --git a/tests/int/provision_using_me/file1.sh b/tests/int/provision_using_me/file1.sh new file mode 100644 index 0000000..1419ae0 --- /dev/null +++ b/tests/int/provision_using_me/file1.sh @@ -0,0 +1 @@ +echo "hello from file1.sh" > /tmp/file1.sh.out diff --git a/tests/int/provision_using_me/file1.txt b/tests/int/provision_using_me/file1.txt new file mode 100644 index 0000000..f6904ef --- /dev/null +++ b/tests/int/provision_using_me/file1.txt @@ -0,0 +1 @@ +hello from file1.txt diff --git a/tests/int/provision_using_me/file2.sh b/tests/int/provision_using_me/file2.sh new file mode 100644 index 0000000..9488f12 --- /dev/null +++ b/tests/int/provision_using_me/file2.sh @@ -0,0 +1 @@ +echo "hello from file2.sh" > /tmp/file2.sh.out diff --git a/tests/int/provision_using_me/file2.txt b/tests/int/provision_using_me/file2.txt new file mode 100644 index 0000000..a68955d --- /dev/null +++ b/tests/int/provision_using_me/file2.txt @@ -0,0 +1 @@ +hello from file2.txt diff --git a/tests/int/provision_using_me/readme.txt b/tests/int/provision_using_me/readme.txt new file mode 100644 index 0000000..91cb288 --- /dev/null +++ b/tests/int/provision_using_me/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 From 7b4dfd7493544a03810e901f01b0b136465a7b5c Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Mon, 17 Feb 2020 18:47:08 -0800 Subject: [PATCH 115/116] fix issues that int tests caught; update readme with current help; limit pytest jobs to 3 for more consistent runs --- README.md | 45 +++++++++++++++++++++++---------------------- mech/mech.py | 5 ++++- mech/test_mech.py | 21 +++++++++++---------- mech/utils.py | 6 ++---- pytest.ini | 9 +++++---- 5 files changed, 45 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 3302b9b..6483d22 100644 --- a/README.md +++ b/README.md @@ -19,26 +19,27 @@ Options: --debug Show debug messages. Common commands: - (list|ls) lists all available boxes - init initializes a new Mech environment by creating a Mechfile + box manages boxes: add, list remove, etc. destroy stops and deletes all traces of the instances - (up|start) starts instances (aka virtual machines) (down|stop|halt) stops the instances - suspend suspends the instances - pause pauses the instances - ssh connects to an instance via SSH - ssh-config outputs OpenSSH valid configuration to connect to the instances - scp copies files to/from the machine via SCP - ip outputs ip of an instance - box manages boxes: add, list remove, etc. global-status outputs status of all virutal machines on this host - status outputs status of the instances - ps list running processes for an instance + 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. - port displays information about guest port mappings + 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` @@ -52,14 +53,15 @@ Starts and provisions the mech environment. Usage: mech up [options] [] Options: - --gui Start GUI - --disable-shared-folder Do not share folder with VM - --provision Enable provisioning - --no-cache Do not save the downloaded box - --memsize 1024 Specify the size of memory for VM - --numvcpus 1 Specify the number of vcpus for VM + --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: @@ -74,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 diff --git a/mech/mech.py b/mech/mech.py index 0d8535f..4c12e97 100644 --- a/mech/mech.py +++ b/mech/mech.py @@ -735,9 +735,12 @@ def ssh(self, arguments): # pylint: disable=no-self-use if inst.created: rc, stdout, stderr = utils.ssh(inst, command, plain, extra) - LOGGER.debug('command:%s rc:%d stdout:%s stderr:%s', rc, stdout, stderr) + 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.") diff --git a/mech/test_mech.py b/mech/test_mech.py index b6d2ac5..00d3c57 100644 --- a/mech/test_mech.py +++ b/mech/test_mech.py @@ -532,7 +532,7 @@ def test_mech_suspend_not_created(mock_locate, mock_load_mechfile, @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.130") +@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') @@ -554,15 +554,16 @@ def test_mech_ssh(mock_locate, mock_load_mechfile, } 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.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') + 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') diff --git a/mech/utils.py b/mech/utils.py index 45ee310..0bca057 100644 --- a/mech/utils.py +++ b/mech/utils.py @@ -795,7 +795,7 @@ def provision(instance, show=False): destination = pro.get('destination') if show: print(colored.green("instance:{} provision_type:{} source:{} " - "destination:{}".format(instance, provision_type, + "destination:{}".format(instance.name, provision_type, source, destination))) else: results = provision_file(vmrun, instance, source, destination) @@ -855,9 +855,7 @@ def provision_file(vmrun, instance, source, destination): def create_tempfile_in_guest(instance): """Create a tempfile in the guest.""" cmd = 'tmpfile=$(mktemp); echo $tmpfile' - result = ssh(instance, cmd) - stdout = result.stdout.decode('utf-8').strip() - LOGGER.debug('MIKE MIKE MIKE result:%s stdout:%s', result, stdout) + _, stdout, _ = ssh(instance, cmd) return stdout diff --git a/pytest.ini b/pytest.ini index 4b858ae..8853bad 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,10 +1,11 @@ [pytest] -addopts = -n4 -m "not int" +addopts = -n3 -m "not int" -# Temp uncomment this next line to find the slow unittest -# Note: timout is in seconds -#timeout = 10 +# 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 From 048386b469ae18badc5d12963164d1d58c43bfea Mon Sep 17 00:00:00 2001 From: Mike Kinney Date: Tue, 18 Feb 2020 17:41:37 -0800 Subject: [PATCH 116/116] convert bats int tests to pure python --- CONTRIBUTING.md | 9 +- full_test | 2 + mech/conftest.py | 38 ++ mech/test_int.py | 102 ----- mech/test_int_add_and_remove_instances.py | 176 +++++++++ mech/test_int_auth.py | 79 ++++ mech/test_int_init_from_boxfile.py | 68 ++++ mech/test_int_init_from_jsonfile.py | 87 ++++ mech/test_int_multiple_instances.py | 112 ++++++ mech/test_int_provision.py | 111 ++++++ mech/test_int_provision_using_psk.py | 65 +++ mech/test_int_shared_folders.py | 142 +++++++ mech/test_int_simple.py | 371 ++++++++++++++++++ smoke_test | 2 + tests/int/README.md | 26 +- tests/int/add_and_remove_instances.bats | 125 ------ tests/int/all | 15 - tests/int/auth.bats | 107 ----- tests/int/init_from_boxfile.bats | 63 --- tests/int/init_from_jsonfile.bats | 63 --- .../bento_ubuntu_18-04.json | 21 - tests/int/no_mechfile.bats | 22 -- tests/int/provision.bats | 93 ----- tests/int/provision/readme.txt | 5 - tests/int/provision_using_me.bats | 53 --- .../file1.sh | 0 .../file1.txt | 0 .../file2.sh | 0 .../file2.txt | 0 .../readme.txt | 0 tests/int/quick.bats | 33 -- tests/int/shared_folders.bats | 86 ---- tests/int/simple.bats | 319 --------------- tests/int/two_ubuntu.bats | 86 ---- tests/int/two_ubuntu/Mechfile | 14 - 35 files changed, 1260 insertions(+), 1235 deletions(-) create mode 100755 full_test delete mode 100644 mech/test_int.py create mode 100644 mech/test_int_add_and_remove_instances.py create mode 100644 mech/test_int_auth.py create mode 100644 mech/test_int_init_from_boxfile.py create mode 100644 mech/test_int_init_from_jsonfile.py create mode 100644 mech/test_int_multiple_instances.py create mode 100644 mech/test_int_provision.py create mode 100644 mech/test_int_provision_using_psk.py create mode 100644 mech/test_int_shared_folders.py create mode 100644 mech/test_int_simple.py create mode 100755 smoke_test delete mode 100755 tests/int/add_and_remove_instances.bats delete mode 100755 tests/int/all delete mode 100755 tests/int/auth.bats delete mode 100755 tests/int/init_from_boxfile.bats delete mode 100755 tests/int/init_from_jsonfile.bats delete mode 100644 tests/int/init_from_jsonfile/bento_ubuntu_18-04.json delete mode 100755 tests/int/no_mechfile.bats delete mode 100755 tests/int/provision.bats delete mode 100644 tests/int/provision/readme.txt delete mode 100755 tests/int/provision_using_me.bats rename tests/int/{provision_using_me => provision_using_psk}/file1.sh (100%) rename tests/int/{provision_using_me => provision_using_psk}/file1.txt (100%) rename tests/int/{provision_using_me => provision_using_psk}/file2.sh (100%) rename tests/int/{provision_using_me => provision_using_psk}/file2.txt (100%) rename tests/int/{provision_using_me => provision_using_psk}/readme.txt (100%) delete mode 100755 tests/int/quick.bats delete mode 100755 tests/int/shared_folders.bats delete mode 100755 tests/int/simple.bats delete mode 100755 tests/int/two_ubuntu.bats delete mode 100644 tests/int/two_ubuntu/Mechfile diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a906b8c..9ea08df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,10 +39,6 @@ pip install autopep8 # Configure git to use pre-commit hook flake8 --install-hook git -# install bats (on mac) -# see https://github.com/bats-core/bats-core -brew install bats-core - # for running unit tests: pytest @@ -58,6 +54,10 @@ pytest --cov-report html:cov_html --cov=mech # 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 @@ -74,6 +74,7 @@ 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/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/conftest.py b/mech/conftest.py index f7aa8f5..47e0dcf 100644 --- a/mech/conftest.py +++ b/mech/conftest.py @@ -2,7 +2,12 @@ """Common pytest code.""" import json +import os import pytest +import subprocess + + +from shutil import rmtree @pytest.fixture @@ -148,7 +153,40 @@ def get_mock_data_written(a_mock): 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/test_int.py b/mech/test_int.py deleted file mode 100644 index f1b27f9..0000000 --- a/mech/test_int.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (c) 2020 Mike Kinney - -"""Mech integration tests""" -import re -import subprocess - - -import pytest - - -@pytest.mark.int -def test_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_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_add_and_remove_instances(): - """Test add and remove instances integration tests.""" - return_value, out = subprocess.getstatusoutput( - 'cd tests/int && ./add_and_remove_instances.bats') - assert return_value == 0 - - -@pytest.mark.int -def test_int_quick(): - """Test quick integration tests.""" - return_value, out = subprocess.getstatusoutput('cd tests/int && ./quick.bats') - assert return_value == 0 - - -@pytest.mark.int -def test_int_no_mechfile(): - """Test no_mechfile integration tests.""" - return_value, out = subprocess.getstatusoutput('cd tests/int && ./no_mechfile.bats') - assert return_value == 0 - - -@pytest.mark.int -def test_int_simple(): - """Test simple integration tests.""" - return_value, out = subprocess.getstatusoutput('cd tests/int && ./simple.bats') - assert return_value == 0 - - -@pytest.mark.int -def test_int_two_ubuntu(): - """Test two_ubuntu tests.""" - return_value, out = subprocess.getstatusoutput('cd tests/int && ./two_ubuntu.bats') - assert return_value == 0 - - -@pytest.mark.int -def test_int_init_from_boxfile(): - """Test init_from_boxfile tests.""" - return_value, out = subprocess.getstatusoutput('cd tests/int && ./init_from_boxfile.bats') - assert return_value == 0 - - -@pytest.mark.int -def test_int_init_from_jsonfile(): - """Test init_from_jsonfile tests.""" - return_value, out = subprocess.getstatusoutput('cd tests/int && ./init_from_jsonfile.bats') - assert return_value == 0 - - -@pytest.mark.int -def test_int_provision(): - """Test provision tests.""" - return_value, out = subprocess.getstatusoutput('cd tests/int && ./provision.bats') - assert return_value == 0 - - -@pytest.mark.int -def test_int_provision_using_me(): - """Test provision using psk tests.""" - return_value, out = subprocess.getstatusoutput('cd tests/int && ./provision_using_me.bats') - assert return_value == 0 - - -@pytest.mark.int -def test_int_shared_folders(): - """Test shared_folders tests.""" - return_value, out = subprocess.getstatusoutput('cd tests/int && ./shared_folders.bats') - assert return_value == 0 - - -@pytest.mark.int -def test_int_auth(): - """Test auth tests.""" - return_value, out = subprocess.getstatusoutput('cd tests/int && ./auth.bats') - assert return_value == 0 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/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 index 152688c..410eb55 100644 --- a/tests/int/README.md +++ b/tests/int/README.md @@ -1,27 +1,5 @@ # Integration tests for mech -This directory contains files for integration testing. -You can run them all by running `./all`. +This directory contains directories and files for integration testing. -# Notes: -1) Test must be run from this directory. -2) Tests *should* be able to be run concurrently. (but no promises) -3) If a test fails, there may be some vmx processes still running. - You should be able to change to that directory's test and inspect - the last state. -4) See [CONTRIBUTING](../../CONTRIBUTING.md) for getting setup to run tests. -5) These tests will take several minutes to run. They download -files from the internet and start up/stop/destroy VMs. - -# Scripts - -- `./init_from_boxfile.bats` - create box from file -- `./no_mechfile.bats` - simple validations without a Mechfile -- `./provision.bats` - provision validation -- `./provision_using_me.bats` - provision using psk validation -- `./quick.bats` - quick validations (ex: help, version) -- `./simple.bats` - simple validations of most basic functionality -- `./two_ubuntu.bats` - validate two ubuntu instances -- `./shared_folders.bats` - validate shared folders functionality -- `./add_and_remove_instances.bats` - validate add/removing instance from Mechfile -- `./auth.bats` - validate using the 'add-me' works as expected +All tests are run under `pytest` command. diff --git a/tests/int/add_and_remove_instances.bats b/tests/int/add_and_remove_instances.bats deleted file mode 100755 index 4108524..0000000 --- a/tests/int/add_and_remove_instances.bats +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bats -# -# add_and_remove_instances.bats - add and remove instance tests -# -# Note: must be run from this directory -# like this: ./add_and_remove_instances.bats - -@test "add and remove instances tests" { - if ! [ -d add_and_remove_instances ]; then - mkdir add_and_remove_instances - fi - cd add_and_remove_instances - - # setup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - if [ -f Mechfile ]; then - rm Mechfile - fi - - run mech ls - regex1="Could not find a Mechfile" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - run mech remove one - regex1="Could not find a Mechfile" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - # check required params - run mech add - regex1="Usage: mech add " - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - # check required params - run mech add mrlesmithjr/alpine311 - regex1="Usage: mech add " - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - # verify with add - run mech add one mrlesmithjr/alpine311 - regex1="Added to the Mechfile" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech ls - regex1=" one " - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # verify with init - rm Mechfile - run mech init mrlesmithjr/alpine311 - regex1="Added to the Mechfile" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech ls - regex1=" one " - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # verify re-adding is not an issue - run mech add one mrlesmithjr/alpine311 - regex1="Added to the Mechfile" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # verify init with a Mechfile fails - run mech init mrlesmithjr/alpine311 - regex1="already exists" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - # add a 2nd instance - run mech add two mrlesmithjr/alpine311 - regex1="Added to the Mechfile" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech ls - regex1=" one " - regex2=" two " - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - - # remove one - run mech remove one - regex1="Removed" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech ls - regex1=" one " - regex2=" two " - [ "$status" -eq 0 ] - ! [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - - # try to remove one again - run mech remove one - regex1="There is no instance" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - run mech remove two - regex1="Removed" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech ls - regex1=" one " - regex2=" two " - [ "$status" -eq 0 ] - ! [[ "$output" =~ $regex1 ]] - ! [[ "$output" =~ $regex2 ]] - - # clean up - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - cd .. -} diff --git a/tests/int/all b/tests/int/all deleted file mode 100755 index 7b0be4e..0000000 --- a/tests/int/all +++ /dev/null @@ -1,15 +0,0 @@ -# all - run all integration tests -# -# Note: must be run from this directory -# like this: ./all - -./quick.bats -./no_mechfile.bats -./simple.bats -./two_ubuntu.bats -./init_from_boxfile.bats -./provision.bats -./provision_using_me.bats -./shared_folders.bats -./add_and_remove_instances.bats -./auth.bats diff --git a/tests/int/auth.bats b/tests/int/auth.bats deleted file mode 100755 index 4a287ce..0000000 --- a/tests/int/auth.bats +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env bats -# -# auth.bats - run some auth tests -# -# Note: must be run from this directory -# like this: ./auth.bats - -@test "mech add-me/auth tests: up, test ssh command, and destroy" { - if ! [ -d auth ]; then - mkdir auth - fi - cd auth - - # setup - # ensure there is no Mechfile first - if [ -f Mechfile ]; then - rm -f Mechfile - fi - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - # Note: The '-a' is the 'add-me' option. - run mech init -a bento/ubuntu-18.04 - regex1="Initializing" - regex2="Loading metadata" - regex3="has been initialized" - regex4="mech up" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - [[ "$output" =~ $regex4 ]] - [ -f Mechfile ] - - run mech list -d - regex1="id_rsa.pub" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech up - regex1="Loading metadata" - regex2="could not be found" - regex3="vmware_desktop" - regex4="integrity filename" - regex5=".vmx" - regex6="Extracting" - regex7="Added network" - regex8="Bringing machine" - regex9="Getting IP" - regex10="Sharing folders" - regex11="started" - regex12="Added auth" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - [[ "$output" =~ $regex4 ]] - [[ "$output" =~ $regex5 ]] - [[ "$output" =~ $regex6 ]] - [[ "$output" =~ $regex7 ]] - [[ "$output" =~ $regex8 ]] - [[ "$output" =~ $regex9 ]] - [[ "$output" =~ $regex10 ]] - [[ "$output" =~ $regex11 ]] - [[ "$output" =~ $regex12 ]] - - # make sure we can re-run 'up' - run mech up - regex1="Bringing machine" - regex2="Getting IP" - regex3="Sharing folders" - regex4="was already started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - [[ "$output" =~ $regex4 ]] - - # make sure we can run a command - first_ip=`mech ip first` - run ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=QUIET ${first_ip} -C uptime - regex1="load average" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech add -a second bento/ubuntu-18.04 - regex1="Added to the Mechfile" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech up second - regex1="second" - regex2="Added auth" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - - run mech destroy -f - regex1="Deleting" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # clean up - rm Mechfile - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - cd .. -} diff --git a/tests/int/init_from_boxfile.bats b/tests/int/init_from_boxfile.bats deleted file mode 100755 index 637bde4..0000000 --- a/tests/int/init_from_boxfile.bats +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bats -# -# init_from_boxfile.bats - init from a box file -# -# Note: must be run from this directory -# like this: ./init_from_boxfile.bats - -@test "mech init, up, destroy of ubuntu from box file" { - cd init_from_boxfile - - ubuntu='ubuntu-18.04' - box_file="/tmp/${ubuntu}.box" - # setup - # ensure there is no Mechfile first - if [ -f Mechfile ]; then - rm -f Mechfile - fi - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - # download the file if we don't have it already - # that way we "cache" the file - if ! [ -f "$box_file" ]; then - wget -O "$box_file" "https://vagrantcloud.com/bento/boxes/${ubuntu}/versions/201912.04.0/providers/vmware_desktop.box" - fi - - run mech init --box "bento/${ubuntu}" "file:${box_file}" - regex1="Initializing" - regex2="has been initialized" - regex3="mech up" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - [ -f Mechfile ] - - run mech up - regex1="Checking box" - regex2="Extracting" - regex3="Added network" - regex4="Bringing machine" - regex5="Getting IP" - regex6="Sharing folders" - regex7="started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - [[ "$output" =~ $regex4 ]] - [[ "$output" =~ $regex5 ]] - [[ "$output" =~ $regex6 ]] - [[ "$output" =~ $regex7 ]] - - run mech destroy -f - regex1="Deleting" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # clean up - rm Mechfile || true - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - cd .. -} diff --git a/tests/int/init_from_jsonfile.bats b/tests/int/init_from_jsonfile.bats deleted file mode 100755 index 3a2838c..0000000 --- a/tests/int/init_from_jsonfile.bats +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bats -# -# init_from_jsonfile.bats - init from a json file -# -# Note: must be run from this directory -# like this: ./init_from_jsonfile.bats - -@test "mech init, up, destroy of ubuntu from json file" { - cd init_from_jsonfile - - ubuntu='ubuntu-18.04' - box_file="/tmp/${ubuntu}.box" - # setup - # ensure there is no Mechfile first - if [ -f Mechfile ]; then - rm -f Mechfile - fi - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - # download the file if we don't have it already - # that way we "cache" the file - if ! [ -f "$box_file" ]; then - wget -O "$box_file" "https://vagrantcloud.com/bento/boxes/${ubuntu}/versions/201912.04.0/providers/vmware_desktop.box" - fi - - run mech init --box "bento/${ubuntu}" "file:bento_ubuntu_18-04.json" - regex1="Initializing" - regex2="has been initialized" - regex3="mech up" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - [ -f Mechfile ] - - run mech up - regex1="Checking box" - regex2="Extracting" - regex3="Added network" - regex4="Bringing machine" - regex5="Getting IP" - regex6="Sharing folders" - regex7="started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - [[ "$output" =~ $regex4 ]] - [[ "$output" =~ $regex5 ]] - [[ "$output" =~ $regex6 ]] - [[ "$output" =~ $regex7 ]] - - run mech destroy -f - regex1="Deleting" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # clean up - rm Mechfile || true - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - cd .. -} diff --git a/tests/int/init_from_jsonfile/bento_ubuntu_18-04.json b/tests/int/init_from_jsonfile/bento_ubuntu_18-04.json deleted file mode 100644 index 9b39b3f..0000000 --- a/tests/int/init_from_jsonfile/bento_ubuntu_18-04.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "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 - } - ] - } - ] -} diff --git a/tests/int/no_mechfile.bats b/tests/int/no_mechfile.bats deleted file mode 100755 index 4235708..0000000 --- a/tests/int/no_mechfile.bats +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bats -# -# no_mechfile.bats - test without any Mechfile -# -# Note: must be run from this directory -# like this: ./no_mechfile.bats - -@test "no Mechfile" { - if ! [ -d no_mechfile ]; then - mkdir no_mechfile - fi - cd no_mechfile - - run mech ls - [ "$status" -eq 1 ] - regex="Couldn't find a Mechfile" - [[ "$output" =~ $regex ]] - - # cleanup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - cd .. -} diff --git a/tests/int/provision.bats b/tests/int/provision.bats deleted file mode 100755 index 0de9997..0000000 --- a/tests/int/provision.bats +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env bats -# -# provision.bats - run provision tests -# -# Note: must be run from this directory -# like this: ./provision.bats - -@test "provision testing" { - cd provision - - # setup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - mech destroy -f || true - - run mech up --disable-provisioning - regex1="VM (first) started" - regex2="VM (second) started" - regex3="VM (third) started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - - # show the provisioning - run mech provision -s - regex1="VM (first) Provision 2 entries" - regex2="VM (second) Provision 3 entries" - regex3="VM (third) Provision 0 entries" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - - # there should not be any files before file provisioning - run mech ssh -c "ls -al /tmp/file1.txt" first - regex1="No such file or directory" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - # provision all - run mech provision - regex1="VM (first) Provision 2 entries" - regex2="VM (second) Provision 3 entries" - regex3="VM (third) Provision 0 entries" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - - # validate files were copied - run mech ssh -c "ls -al /tmp/file1.txt" first - regex1="vagrant" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # provision first (again) - run mech provision first - regex1="VM (first) Provision 2 entries" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # provision second (again) - run mech provision second - regex1="VM (second) Provision 3 entries" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # provision third (again) - run mech provision third - regex1="VM (third) Provision 0 entries" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # ensure that provision runs on 'mech up' - run mech up - regex1="VM (first) Provision 2 entries" - regex2="VM (second) Provision 3 entries" - regex3="VM (third) Provision 0 entries" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - - run mech destroy -f - regex1="Deleting" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # clean up - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - cd .. -} diff --git a/tests/int/provision/readme.txt b/tests/int/provision/readme.txt deleted file mode 100644 index 73bc030..0000000 --- a/tests/int/provision/readme.txt +++ /dev/null @@ -1,5 +0,0 @@ -This test exercises mech provisioning. - -file provisioning just copies over files - -shell provisioning executes the files/inline code diff --git a/tests/int/provision_using_me.bats b/tests/int/provision_using_me.bats deleted file mode 100755 index 0e6e60d..0000000 --- a/tests/int/provision_using_me.bats +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bats -# -# provision_using_me.bats - run provision tests using psk -# -# Note: must be run from this directory -# like this: ./provision_using_me.bats - -@test "provision using psk testing" { - cd provision_using_me - - # setup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - mech destroy -f || true - - run mech up -r - regex1="VM (first) started" - regex2="VM (second) started" - regex3="VM (third) started" - regex4="Added auth" - regex5="Removing username" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - [[ "$output" =~ $regex4 ]] - [[ "$output" =~ $regex5 ]] - - # validate files were copied - run mech ssh -c "ls -al /tmp/file1.txt" first - regex1="vagrant" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # ensure that provision runs on 'mech up' - run mech up - regex1="VM (first) Provision 2 entries" - regex2="VM (second) Provision 3 entries" - regex3="VM (third) Provision 0 entries" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - - run mech destroy -f - regex1="Deleting" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # clean up - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - cd .. -} diff --git a/tests/int/provision_using_me/file1.sh b/tests/int/provision_using_psk/file1.sh similarity index 100% rename from tests/int/provision_using_me/file1.sh rename to tests/int/provision_using_psk/file1.sh diff --git a/tests/int/provision_using_me/file1.txt b/tests/int/provision_using_psk/file1.txt similarity index 100% rename from tests/int/provision_using_me/file1.txt rename to tests/int/provision_using_psk/file1.txt diff --git a/tests/int/provision_using_me/file2.sh b/tests/int/provision_using_psk/file2.sh similarity index 100% rename from tests/int/provision_using_me/file2.sh rename to tests/int/provision_using_psk/file2.sh diff --git a/tests/int/provision_using_me/file2.txt b/tests/int/provision_using_psk/file2.txt similarity index 100% rename from tests/int/provision_using_me/file2.txt rename to tests/int/provision_using_psk/file2.txt diff --git a/tests/int/provision_using_me/readme.txt b/tests/int/provision_using_psk/readme.txt similarity index 100% rename from tests/int/provision_using_me/readme.txt rename to tests/int/provision_using_psk/readme.txt diff --git a/tests/int/quick.bats b/tests/int/quick.bats deleted file mode 100755 index 3c010fd..0000000 --- a/tests/int/quick.bats +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bats -# -# quick.bats - run some quick tests -# -# Note: must be run from this directory -# like this: ./quick.bats - -@test "help" { - run mech --help - [ "$status" -eq 0 ] - - # cleanup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true -} - -@test "no args" { - run mech - [ "$status" -eq 1 ] - [ "$output" = "Usage: mech [options] [...]" ] - - # cleanup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true -} - -@test "version" { - run mech --version - [ "$status" -eq 0 ] - regex='mech v[0-9\.]+' - [[ "$output" =~ $regex ]] - - # cleanup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true -} diff --git a/tests/int/shared_folders.bats b/tests/int/shared_folders.bats deleted file mode 100755 index 2632a04..0000000 --- a/tests/int/shared_folders.bats +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bats -# -# shared_folders.bats - run shared_folders tests -# -# Note: must be run from this directory -# like this: ./shared_folders.bats - -@test "shared folders testing" { - cd shared_folders - - # setup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - run mech up - regex1="VM (first) started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first - regex1="Mechfile" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # make sure we can see files on 2nd "mech2" mount - date > /tmp/now - run mech ssh -c "ls -al /mnt/hgfs/mech2/now" first - regex1=" /mnt/hgfs/mech2/now" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - rm /tmp/now - - run mech stop - regex1="Stopped" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech up --disable-shared-folders - regex1="VM (first) started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first - regex1="No such file or directory" - [ "$status" -eq 2 ] - [[ "$output" =~ $regex1 ]] - - run mech pause - regex1="Paused" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech resume --disable-shared-folders - regex1="VM (first) started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first - regex1="No such file or directory" - [ "$status" -eq 2 ] - [[ "$output" =~ $regex1 ]] - - run mech pause - regex1="Paused" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech resume - regex1="VM (first) started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech ssh -c "ls -al /mnt/hgfs/mech/Mechfile" first - regex1="Mechfile" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech destroy -f - regex1="Deleting" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # clean up - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - cd .. -} diff --git a/tests/int/simple.bats b/tests/int/simple.bats deleted file mode 100755 index 6ac6924..0000000 --- a/tests/int/simple.bats +++ /dev/null @@ -1,319 +0,0 @@ -#!/usr/bin/env bats -# -# simple.bats - run some simple tests -# -# Note: must be run from this directory -# like this: ./simple.bats - -# Note: Using alpine because the image is the smallest I could find. -# This will download the box from internet. -@test "mech init, up, destroy of alpine" { - if ! [ -d simple ]; then - mkdir simple - fi - cd simple - - # setup - # ensure there is no Mechfile first - if [ -f Mechfile ]; then - rm -f Mechfile - fi - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - run mech init - [ "$status" -eq 1 ] - regex="Usage: mech init " - [[ "$output" =~ $regex ]] - - run mech init mrlesmithjr/alpine311 - regex1="Initializing" - regex2="Loading metadata" - regex3="has been initialized" - regex4="mech up" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - [[ "$output" =~ $regex4 ]] - [ -f Mechfile ] - - run mech up - regex1="Loading metadata" - regex2="could not be found" - regex3="vmware_desktop" - regex4="integrity filename" - regex5=".vmx" - regex6="Extracting" - regex7="Added network" - regex8="Bringing machine" - regex9="Getting IP" - regex10="Sharing folders" - regex11="started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - [[ "$output" =~ $regex4 ]] - [[ "$output" =~ $regex5 ]] - [[ "$output" =~ $regex6 ]] - [[ "$output" =~ $regex7 ]] - [[ "$output" =~ $regex8 ]] - [[ "$output" =~ $regex9 ]] - [[ "$output" =~ $regex10 ]] - [[ "$output" =~ $regex11 ]] - - # make sure we can re-run 'up' - run mech up - regex1="Bringing machine" - regex2="Getting IP" - regex3="Sharing folders" - regex4="was already started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - [[ "$output" =~ $regex4 ]] - - # make sure we can re-run 'up' with alias 'start' - run mech start - regex1="Bringing machine" - regex2="Getting IP" - regex3="Sharing folders" - regex4="was already started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - [[ "$output" =~ $regex4 ]] - - # validate ps required arg - run mech ps - regex1="Usage: mech ps" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - run mech ps first - regex1="cmd=/sbin/init" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech status - regex1="first" - regex2="Tools running" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - - run mech global-status - regex1="simple/.mech/first/" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech list - regex1="first" - regex2="alpine" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - - # validate alias works, too - run mech ls - regex1="first" - regex2="alpine" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - - run mech stop - regex1="Stopped" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # try to stop a Stopped instance - run mech stop - regex1="Not stopped" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech start - regex1="started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech pause - regex1="Paused" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech resume - regex1="resumed" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech suspend - regex1="Suspended" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech resume - regex1="started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # ssh: incomplete args - run mech ssh - regex1="Usage: mech ssh" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - run mech ssh -c 'uptime' first - regex1="load average" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # other form of command - run mech ssh --command 'uptime' first - regex1="load average" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # scp: incomplete args - run mech scp - regex1="Usage: mech scp" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - date > now - run mech scp now first:/tmp - regex1="100%" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - rm now - - run mech scp first:/tmp/now . - regex1="100%" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - rm now - - # ip: incomplete args - run mech ip - regex1="Usage: mech ip" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - run mech ip first - regex1="^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # ip: invalid arg - run mech ip first2 - regex1="not found in the Mechfile" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - run mech port - regex1="Total port forwardings: 0" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech box list - regex1="alpine" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # validate alias works, too - run mech box list - regex1="alpine" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - ## snapshots - run mech snapshot list - regex1="Total snapshots: 0" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # validate alias works, too - run mech snapshot ls - regex1="Total snapshots: 0" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # make sure params are used - run mech snapshot - regex1="Usage: mech snapshot " - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - # make sure params are used - run mech snapshot save - regex1="Usage: mech snapshot save" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - # make sure params are used - run mech snapshot save snap1 - regex1="Usage: mech snapshot save" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - run mech snapshot save snap1 first - regex1="taken" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # re-run with same params, should err - run mech snapshot save snap1 first - regex1="A snapshot with the name already exists" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - run mech snapshot ls - regex1="Total snapshots: 1" - regex2="snap1" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - - # make sure params are used - run mech snapshot delete - regex1="Usage: mech snapshot delete" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - # make sure params are used (alias) - run mech snapshot remove - regex1="Usage: mech snapshot delete" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - # make sure params are used - run mech snapshot delete snap1 - regex1="Usage: mech snapshot delete" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - # make sure params are valid - run mech snapshot delete snap1 first1 - regex1="Usage: mech snapshot delete" - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - run mech snapshot delete snap1 first - regex1=" deleted" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech destroy -f - regex1="Deleting" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - # clean up - rm Mechfile - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - cd .. -} diff --git a/tests/int/two_ubuntu.bats b/tests/int/two_ubuntu.bats deleted file mode 100755 index c800f48..0000000 --- a/tests/int/two_ubuntu.bats +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bats -# -# two_ubuntu.bats - run some tests with multiple ubuntu instances -# -# Note: must be run from the tests/int directory -# like this: ./two_ubuntu.bats - -@test "mech up/destroy of two ubuntu instances (first and second)" { - cd two_ubuntu - - # setup - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - run mech box ls - [ "$status" -eq 0 ] - - run mech box list - [ "$status" -eq 0 ] - - # ensure they can start the first time - run mech up - regex1="first" - regex2="second" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - - # ensure running 'up' again is not an issue - run mech up - regex1="first" - regex2="second" - regex3="already started" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - - run mech ls - regex1="first" - regex2="second" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - - run mech status - regex1="first" - regex2="second" - regex3="Tools running" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - - run mech destroy -f - regex1="first" - regex2="second" - regex3="Deleting" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - [[ "$output" =~ $regex2 ]] - [[ "$output" =~ $regex3 ]] - - run mech box list - regex1="ubuntu" - [ "$status" -eq 0 ] - [[ "$output" =~ $regex1 ]] - - run mech box remove bento/ubuntu-18.04 - regex1="Usage: mech box remove " - [ "$status" -eq 1 ] - [[ "$output" =~ $regex1 ]] - - run mech box remove bento/ubuntu-18.04 201912.04.0 - [ "$status" -eq 0 ] - - # make sure there is not ubuntu box - run mech box list - regex1="ubuntu" - [ "$status" -eq 0 ] - ! [[ "$output" =~ $regex1 ]] - - # clean up - find . -type d -name .mech -exec rm -rf {} \; 2> /dev/null || true - - cd .. -} diff --git a/tests/int/two_ubuntu/Mechfile b/tests/int/two_ubuntu/Mechfile deleted file mode 100644 index 573f088..0000000 --- a/tests/int/two_ubuntu/Mechfile +++ /dev/null @@ -1,14 +0,0 @@ -{ - "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" - } -}