diff --git a/doc/source/index.rst b/doc/source/index.rst index 7e613556c..64ae27dbc 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -11,17 +11,6 @@ Contents: changelog authors -.. _`Ansible`: https://docs.ansible.com -.. _`Test Kitchen`: http://kitchen.ci -.. _`playbook`: https://docs.ansible.com/ansible/playbooks.html -.. _`role`: http://docs.ansible.com/ansible/playbooks_roles.html -.. _`Serverspec`: http://serverspec.org -.. _`Testinfra`: https://testinfra.readthedocs.io -.. _`Vagrant`: http://docs.vagrantup.com/v2 -.. _`Docker`: https://www.docker.com -.. _`OpenStack`: https://www.openstack.org -.. _`libvirt`: http://libvirt.org - Indices and tables ================== diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 6492ad61d..964a38c1a 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -190,6 +190,13 @@ Lint :undoc-members: :members: execute +Status +^^^^^^ + +.. autoclass:: molecule.command.status.Status + :undoc-members: + :members: execute + Syntax ^^^^^^ diff --git a/molecule/command/__init__.py b/molecule/command/__init__.py index 7756e424e..37e20dc88 100644 --- a/molecule/command/__init__.py +++ b/molecule/command/__init__.py @@ -32,7 +32,7 @@ from molecule.command import init # noqa from molecule.command import lint # noqa # from molecule.command import login # noqa -# from molecule.command import status # noqa +from molecule.command import status # noqa from molecule.command import syntax # noqa from molecule.command import test # noqa from molecule.command import verify # noqa diff --git a/molecule/command/status.py b/molecule/command/status.py new file mode 100644 index 000000000..367d9deb0 --- /dev/null +++ b/molecule/command/status.py @@ -0,0 +1,72 @@ +# Copyright (c) 2015-2017 Cisco Systems, Inc. +# +# 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. + +import click +import tabulate + +from molecule import util +from molecule.command import base + + +class Status(base.Base): + def execute(self): + """ + Execute the actions necessary to perform a `molecule status` and + returns None. + + >>> molecule status + + Targeting a specific scenario: + + >>> molecule status --scenario-name foo + + Executing with `debug`: + + >>> molecule --debug status + + :return: None + """ + msg = 'Scenario: [{}]'.format(self._config.scenario.name) + util.print_info(msg) + self._print_tabulate_data(self._config.driver.status(), + ['Name', 'State', 'Driver']) + + def _print_tabulate_data(self, data, headers): + """ + Shows the tabulate data on the screen and returns None. + + :param data: + :param headers: + :returns: None + """ + print tabulate.tabulate(data, headers, tablefmt='orgtbl') + + +@click.command() +@click.pass_context +@click.option('--scenario-name', help='Name of the scenario to target.') +def status(ctx, scenario_name): # pragma: no cover + """ Displays status of instances. """ + args = ctx.obj.get('args') + command_args = {'subcommand': __name__, 'scenario_name': scenario_name} + + for config in base.get_configs(args, command_args): + s = Status(config) + s.execute() diff --git a/molecule/config.py b/molecule/config.py index 0790312ac..bffe6245b 100644 --- a/molecule/config.py +++ b/molecule/config.py @@ -27,7 +27,7 @@ from molecule import state from molecule.dependency import ansible_galaxy from molecule.dependency import gilt -from molecule.driver import docker +from molecule.driver import dockr from molecule.lint import ansible_lint from molecule.verifier import testinfra @@ -71,7 +71,7 @@ def dependency(self): @property def driver(self): if self.config['driver']['name'] == 'docker': - return docker.Docker(self) + return dockr.Dockr(self) @property def lint(self): diff --git a/molecule/driver/base.py b/molecule/driver/base.py index b9e5f99b4..08a7f7b06 100644 --- a/molecule/driver/base.py +++ b/molecule/driver/base.py @@ -43,6 +43,27 @@ def testinfra_options(self): """ pass # pragma: no cover + @abc.abstractmethod + def status(self): + """ + Determine instances status and return a list. + + :returns: list + """ + pass # pragma: no cover + + @abc.abstractmethod + def _delayed_import(self): + """ + Delay driver module imports and return a module. By delaying the + import, Molecule can import all drivers in the config module, and only + instantiate the configured one. Otherwise, Molecule would require + each driver's packages be installed. + + :returns: module + """ + pass # pragma: no cover + @property def name(self): return self._config.config['driver']['name'] diff --git a/molecule/driver/docker.py b/molecule/driver/dockr.py similarity index 60% rename from molecule/driver/docker.py rename to molecule/driver/dockr.py index 63bc89381..1c335fdce 100644 --- a/molecule/driver/docker.py +++ b/molecule/driver/dockr.py @@ -18,10 +18,13 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. +import collections +import sys + from molecule.driver import base -class Docker(base.Base): +class Dockr(base.Base): """ `Docker`_ is the default driver. @@ -38,7 +41,10 @@ class Docker(base.Base): """ def __init__(self, config): - super(Docker, self).__init__(config) + super(Dockr, self).__init__(config) + docker = self._delayed_import() + self._docker = docker.Client( + version='auto', **docker.utils.kwargs_from_env()) @property def testinfra_options(self): @@ -57,3 +63,30 @@ def connection_options(self): :returns: str """ return {'ansible_connection': 'docker'} + + def status(self): + Status = collections.namedtuple('Status', ['name', 'state', 'driver']) + status_list = [] + for instance in self._config.platforms: + instance_name = '{}-{}'.format( + instance.get('name'), self._config.scenario.name) + try: + d = self._docker.containers(filters={'name': instance_name})[0] + state = d.get('Status') + except IndexError: + state = 'Not Created' + status_list.append( + Status( + name=instance_name, + state=state, + driver=self.name.capitalize())) + + return status_list + + def _delayed_import(self): + try: + import docker + + return docker + except ImportError: # pragma: no cover + sys.exit('ERROR: Driver missing, install docker-py.') diff --git a/molecule/shell.py b/molecule/shell.py index 65f1c983c..5eabb7b7e 100644 --- a/molecule/shell.py +++ b/molecule/shell.py @@ -50,7 +50,7 @@ def cli(ctx, debug): # pragma: no cover cli.add_command(command.init.init) cli.add_command(command.lint.lint) # cli.add_command(command.login.login) -# cli.add_command(command.status.status) +cli.add_command(command.status.status) cli.add_command(command.syntax.syntax) cli.add_command(command.test.test) cli.add_command(command.verify.verify) diff --git a/test/functional/test_docker.py b/test/functional/test_docker.py index 5596de6e4..5671cb46d 100644 --- a/test/functional/test_docker.py +++ b/test/functional/test_docker.py @@ -19,6 +19,7 @@ # DEALINGS IN THE SOFTWARE. import os +import re import pytest import sh @@ -87,6 +88,17 @@ def test_command_lint(with_scenario): sh.molecule('lint') +@pytest.mark.parametrize( + 'with_scenario', ['docker'], indirect=['with_scenario']) +def test_command_status(with_scenario): + out = sh.molecule('status', '--scenario-name', 'default') + assert re.search('instance-1-default.*Not Created.*Docker', out.stdout) + + out = sh.molecule('status', '--scenario-name', 'multi-node') + assert re.search('instance-1-multi-node', out.stdout) + assert re.search('instance-2-multi-node', out.stdout) + + @pytest.mark.parametrize( 'with_scenario', ['docker'], indirect=['with_scenario']) def test_command_syntax(with_scenario): diff --git a/test/unit/command/test_status.py b/test/unit/command/test_status.py new file mode 100644 index 000000000..be9d5fc55 --- /dev/null +++ b/test/unit/command/test_status.py @@ -0,0 +1,38 @@ +# Copyright (c) 2015-2017 Cisco Systems, Inc. +# +# 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. + +from molecule.command import status + + +def test_execute(capsys, patched_print_info, config_instance): + s = status.Status(config_instance) + s.execute() + + msg = 'Scenario: [default]' + patched_print_info.assert_called_once_with(msg) + + stdout, _ = capsys.readouterr() + + assert 'Name' in stdout + assert 'State' in stdout + assert 'Driver' in stdout + + assert 'instance-1-default' in stdout + assert 'instance-2-default' in stdout diff --git a/test/unit/driver/test_docker.py b/test/unit/driver/test_dockr.py similarity index 60% rename from test/unit/driver/test_docker.py rename to test/unit/driver/test_dockr.py index 980f5a1ba..248481348 100644 --- a/test/unit/driver/test_docker.py +++ b/test/unit/driver/test_dockr.py @@ -21,7 +21,7 @@ import pytest from molecule import config -from molecule.driver import docker +from molecule.driver import dockr @pytest.fixture @@ -34,7 +34,7 @@ def docker_instance(molecule_file, platforms_data, driver_data): configs = [platforms_data, driver_data] c = config.Config(molecule_file, configs=configs) - return docker.Docker(c) + return dockr.Dockr(c) def test_config_private_member(docker_instance): @@ -57,3 +57,45 @@ def test_name_property(docker_instance): def test_options_property(docker_instance): assert {} == docker_instance.options + + +def test_status(mocker, docker_instance): + def side_effect(filters): + instance_name = filters['name'] + + return [{ + u'Status': u'Up About an hour', + u'State': u'running', + u'Command': u'sleep infinity', + u'Names': [u'/{}'.format(instance_name)], + }] + + m = mocker.patch('docker.client.Client.containers') + m.side_effect = side_effect + result = docker_instance.status() + + assert 2 == len(result) + + assert result[0].name == 'instance-1-default' + assert result[0].state == 'Up About an hour' + assert result[0].driver == 'Docker' + + assert result[1].name == 'instance-2-default' + assert result[1].state == 'Up About an hour' + assert result[1].driver == 'Docker' + + +def test_status_not_created(mocker, docker_instance): + m = mocker.patch('docker.client.Client.containers') + m.return_value = [] + result = docker_instance.status() + + assert 2 == len(result) + + assert result[0].name == 'instance-1-default' + assert result[0].state == 'Not Created' + assert result[0].driver == 'Docker' + + assert result[1].name == 'instance-2-default' + assert result[1].state == 'Not Created' + assert result[1].driver == 'Docker' diff --git a/test/unit/test_config.py b/test/unit/test_config.py index 097dabd89..522bd5dd2 100644 --- a/test/unit/test_config.py +++ b/test/unit/test_config.py @@ -28,7 +28,7 @@ from molecule import state from molecule.dependency import ansible_galaxy from molecule.dependency import gilt -from molecule.driver import docker +from molecule.driver import dockr from molecule.lint import ansible_lint from molecule.verifier import testinfra @@ -88,7 +88,7 @@ def test_dependency_property_is_gilt(config_instance, molecule_file): def test_driver_property(config_instance): - assert isinstance(config_instance.driver, docker.Docker) + assert isinstance(config_instance.driver, dockr.Dockr) def test_lint_property(config_instance): diff --git a/test/unit/test_util.py b/test/unit/test_util.py index 53cf4fb11..61db8fd51 100644 --- a/test/unit/test_util.py +++ b/test/unit/test_util.py @@ -35,62 +35,62 @@ def test_print_success(capsys): util.print_success('test') - result, _ = capsys.readouterr() + stdout, _ = capsys.readouterr() print('{}{}'.format(colorama.Fore.GREEN, 'test'.rstrip())) x, _ = capsys.readouterr() - assert x == result + assert x == stdout def test_print_info(capsys): util.print_info('test') - result, _ = capsys.readouterr() + stdout, _ = capsys.readouterr() print('--> {}{}'.format(colorama.Fore.CYAN, 'test'.rstrip())) x, _ = capsys.readouterr() - assert x == result + assert x == stdout def test_print_info_without_pretty(capsys): util.print_info('test', pretty=False) - result, _ = capsys.readouterr() + stdout, _ = capsys.readouterr() print('{}'.format('test'.rstrip())) x, _ = capsys.readouterr() - assert x == result + assert x == stdout def test_print_warn(capsys): util.print_warn('test') - result, _ = capsys.readouterr() + stdout, _ = capsys.readouterr() print('{}{}'.format(colorama.Fore.YELLOW, 'test'.rstrip())) x, _ = capsys.readouterr() - assert x == result + assert x == stdout def test_print_error(capsys): util.print_error('test') - _, result = capsys.readouterr() + _, stderr = capsys.readouterr() print('{}ERROR: {}'.format(colorama.Fore.RED, 'test'.rstrip())) x, _ = capsys.readouterr() - assert x == result + assert x == stderr def test_print_error_without_pretty(capsys): util.print_error('test', pretty=False) - x, result = capsys.readouterr() + x, stderr = capsys.readouterr() print('{}{}'.format(colorama.Fore.RED, 'test'.rstrip())) x, _ = capsys.readouterr() - assert x == result + assert x == stderr def test_print_debug(capsys):