Skip to content

Commit

Permalink
Implemented status subcommand (#706)
Browse files Browse the repository at this point in the history
* Better capsys variable naming

* Implemented status subcommand

Fixes: #703
  • Loading branch information
retr0h committed Jan 10, 2017
1 parent 156be56 commit 979d338
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 33 deletions.
11 changes: 0 additions & 11 deletions doc/source/index.rst
Expand Up @@ -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
==================

Expand Down
7 changes: 7 additions & 0 deletions doc/source/usage.rst
Expand Up @@ -190,6 +190,13 @@ Lint
:undoc-members:
:members: execute

Status
^^^^^^

.. autoclass:: molecule.command.status.Status
:undoc-members:
:members: execute

Syntax
^^^^^^

Expand Down
2 changes: 1 addition & 1 deletion molecule/command/__init__.py
Expand Up @@ -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
72 changes: 72 additions & 0 deletions 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()
4 changes: 2 additions & 2 deletions molecule/config.py
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
21 changes: 21 additions & 0 deletions molecule/driver/base.py
Expand Up @@ -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']
Expand Down
37 changes: 35 additions & 2 deletions molecule/driver/docker.py → molecule/driver/dockr.py
Expand Up @@ -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.
Expand All @@ -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):
Expand All @@ -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.')
2 changes: 1 addition & 1 deletion molecule/shell.py
Expand Up @@ -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)
12 changes: 12 additions & 0 deletions test/functional/test_docker.py
Expand Up @@ -19,6 +19,7 @@
# DEALINGS IN THE SOFTWARE.

import os
import re

import pytest
import sh
Expand Down Expand Up @@ -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):
Expand Down
38 changes: 38 additions & 0 deletions 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
46 changes: 44 additions & 2 deletions test/unit/driver/test_docker.py → test/unit/driver/test_dockr.py
Expand Up @@ -21,7 +21,7 @@
import pytest

from molecule import config
from molecule.driver import docker
from molecule.driver import dockr


@pytest.fixture
Expand All @@ -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):
Expand All @@ -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'
4 changes: 2 additions & 2 deletions test/unit/test_config.py
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit 979d338

Please sign in to comment.