Skip to content

Commit

Permalink
Delegated driver acts as managed (#1301)
Browse files Browse the repository at this point in the history
The delegated driver defaults to `managed`, just like
every other driver in Molecule.  This driver now
adheres to an instance-config API by default.  Only,
when `managed` is `False` does the driver force the
developer to configure connectivity.

Fixes: #1292
  • Loading branch information
retr0h authored May 17, 2018
1 parent 404d388 commit e38ae54
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 12 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -890,7 +890,7 @@ Breaking Changes
* Added support for libvirt provider.
* Added ``molecule check`` to check playbook syntax.
* Testinfra parameters can now be set as vars in molecule.yml.
* Running testinfra tests in parallel is no longer the default behavior.
* Running testinfra tests in parallel is no longer the default behaviour.

1.5.1
=====
Expand Down
3 changes: 1 addition & 2 deletions molecule/command/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ def execute(self):
:return: None
"""
c = self._config
if (not c.state.created
and (c.driver.delegated and not c.driver.managed)):
if ((not c.state.created) and c.driver.managed):
msg = 'Instances not created. Please create instances first.'
util.sysexit_with_message(msg)

Expand Down
64 changes: 62 additions & 2 deletions molecule/driver/delegated.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
# DEALINGS IN THE SOFTWARE.

from molecule import logger
from molecule import util
from molecule.driver import base

LOG = logger.get_logger(__name__)
Expand All @@ -30,13 +31,26 @@ class Delegated(base.Base):
the default driver used in Molecule.
Under this driver, it is the developers responsibility to implement the
create and destroy actions.
create and destroy playbooks. `Managed` is the default behaviour of all
drivers.
.. code-block:: yaml
driver:
name: delegated
However, the developer must adhere to the instance-config API. The
developer's create playbook must provide the following instance-config
data, and the developer's destroy playbook must reset the instance-config.
.. code-block:: yaml
- address: ssh_endpoint
identity_file: ssh_identity_file
instance: instance_name
port: ssh_port_as_string
user: ssh_user
Molecule can also skip the provisioning/deprovisioning steps. It is the
developers responsibility to manage the instances, and properly configure
Molecule to connect to said instances.
Expand Down Expand Up @@ -80,7 +94,7 @@ class Delegated(base.Base):
platforms:
- name: instance-vagrant
Provide the files Molecule will preserve upon each subcommand execution.
Provide the files Molecule will preserve post `destroy` action.
.. code-block:: yaml
Expand All @@ -104,6 +118,14 @@ def name(self, value):

@property
def login_cmd_template(self):
if self.managed:
connection_options = ' '.join(self.ssh_connection_options)

return ('ssh {{address}} '
'-l {{user}} '
'-p {{port}} '
'-i {{identity_file}} '
'{}').format(connection_options)
return self.options['login_cmd_template']

@property
Expand All @@ -112,15 +134,53 @@ def default_safe_files(self):

@property
def default_ssh_connection_options(self):
if self.managed:
return self._get_ssh_connection_options()
return []

def login_options(self, instance_name):
if self.managed:
d = {'instance': instance_name}

return util.merge_dicts(d,
self._get_instance_config(instance_name))
return {'instance': instance_name}

def ansible_connection_options(self, instance_name):
if self.managed:
try:
d = self._get_instance_config(instance_name)

return {
'ansible_user':
d['user'],
'ansible_host':
d['address'],
'ansible_port':
d['port'],
'ansible_private_key_file':
d['identity_file'],
'connection':
'ssh',
'ansible_ssh_common_args':
' '.join(self.ssh_connection_options),
}
except StopIteration:
return {}
except IOError:
# Instance has yet to be provisioned , therefore the
# instance_config is not on disk.
return {}
return self.options['ansible_connection_options']

def _created(self):
if self.managed:
return super(Delegated, self)._created()
return 'unknown'

def _get_instance_config(self, instance_name):
instance_config_dict = util.safe_load_file(
self._config.driver.instance_config)

return next(item for item in instance_config_dict
if item['instance'] == instance_name)
10 changes: 10 additions & 0 deletions test/unit/command/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,13 @@ def command_driver_delegated_section_data():
}
}
}


@pytest.fixture
def command_driver_delegated_managed_section_data():
return {
'driver': {
'name': 'delegated',
'managed': True,
}
}
4 changes: 2 additions & 2 deletions test/unit/command/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ def test_execute(mocker, _instance):


@pytest.mark.parametrize(
'config_instance', ['command_driver_delegated_section_data'],
'config_instance', ['command_driver_delegated_managed_section_data'],
indirect=True)
def test_execute_raises_when_not_converged(patched_logger_critical, _instance):
def test_execute_raises_when_not_created(patched_logger_critical, _instance):
_instance._config.state.change_state('created', False)

with pytest.raises(SystemExit) as e:
Expand Down
4 changes: 4 additions & 0 deletions test/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,13 @@ def _molecule_dependency_galaxy_section_data():

@pytest.fixture
def _molecule_driver_section_data():
print 'I HERE'
return {
'driver': {
'name': 'docker',
'options': {
'managed': True,
},
},
}

Expand Down
149 changes: 144 additions & 5 deletions test/unit/driver/test_delegated.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,25 @@


@pytest.fixture
def _driver_section_data():
def _driver_managed_section_data():
return {
'driver': {
'name': 'delegated',
}
}


@pytest.fixture
def _driver_unmanaged_section_data():
return {
'driver': {
'name': 'delegated',
'options': {
'login_cmd_template': 'docker exec -ti {instance} bash',
'ansible_connection_options': {
'ansible_connection': 'docker'
}
},
'managed': False,
}
}
}
Expand All @@ -62,27 +72,50 @@ def test_name_property(_instance):


@pytest.mark.parametrize(
'config_instance', ['_driver_section_data'], indirect=True)
'config_instance', ['_driver_unmanaged_section_data'], indirect=True)
def test_options_property(_instance):
x = {
'ansible_connection_options': {
'ansible_connection': 'docker'
},
'login_cmd_template': 'docker exec -ti {instance} bash',
'managed': False,
}

assert x == _instance.options


@pytest.mark.parametrize(
'config_instance', ['_driver_managed_section_data'], indirect=True)
def test_options_property_when_managed(_instance):
x = {
'managed': True,
}

assert x == _instance.options


@pytest.mark.parametrize(
'config_instance', ['_driver_section_data'], indirect=True)
'config_instance', ['_driver_unmanaged_section_data'], indirect=True)
def test_login_cmd_template_property(_instance):
x = 'docker exec -ti {instance} bash'

assert x == _instance.login_cmd_template


@pytest.mark.parametrize(
'config_instance', ['_driver_managed_section_data'], indirect=True)
def test_login_cmd_template_property_when_managed(_instance):
x = ('ssh {address} -l {user} -p {port} -i {identity_file} '
'-o UserKnownHostsFile=/dev/null '
'-o ControlMaster=auto '
'-o ControlPersist=60s '
'-o IdentitiesOnly=yes '
'-o StrictHostKeyChecking=no')

assert x == _instance.login_cmd_template


def test_safe_files_property(_instance):
assert [] == _instance.safe_files

Expand All @@ -99,29 +132,121 @@ def test_managed_property(_instance):
assert _instance.managed


@pytest.mark.parametrize(
'config_instance', ['_driver_unmanaged_section_data'], indirect=True)
def test_default_ssh_connection_options_property(_instance):
assert [] == _instance.default_ssh_connection_options


@pytest.mark.parametrize(
'config_instance', ['_driver_managed_section_data'], indirect=True)
def test_default_ssh_connection_options_property_when_managed(_instance):
x = [
'-o UserKnownHostsFile=/dev/null',
'-o ControlMaster=auto',
'-o ControlPersist=60s',
'-o IdentitiesOnly=yes',
'-o StrictHostKeyChecking=no',
]

assert x == _instance.default_ssh_connection_options


@pytest.mark.parametrize(
'config_instance', ['_driver_unmanaged_section_data'], indirect=True)
def test_login_options(_instance):
assert {'instance': 'foo'} == _instance.login_options('foo')


@pytest.mark.parametrize(
'config_instance', ['_driver_section_data'], indirect=True)
'config_instance', ['_driver_managed_section_data'], indirect=True)
def test_login_options_when_managed(mocker, _instance):
m = mocker.patch(
'molecule.driver.delegated.Delegated._get_instance_config')
m.return_value = {
'instance': 'foo',
'address': '172.16.0.2',
'user': 'cloud-user',
'port': 22,
'identity_file': '/foo/bar',
}

x = {
'instance': 'foo',
'address': '172.16.0.2',
'user': 'cloud-user',
'port': 22,
'identity_file': '/foo/bar',
}
assert x == _instance.login_options('foo')


@pytest.mark.parametrize(
'config_instance', ['_driver_unmanaged_section_data'], indirect=True)
def test_ansible_connection_options(_instance):
x = {'ansible_connection': 'docker'}

assert x == _instance.ansible_connection_options('foo')


@pytest.mark.parametrize(
'config_instance', ['_driver_managed_section_data'], indirect=True)
def test_ansible_connection_options_when_managed(mocker, _instance):
m = mocker.patch(
'molecule.driver.delegated.Delegated._get_instance_config')
m.return_value = {
'instance': 'foo',
'address': '172.16.0.2',
'user': 'cloud-user',
'port': 22,
'identity_file': '/foo/bar',
}

x = {
'ansible_host':
'172.16.0.2',
'ansible_port':
22,
'ansible_user':
'cloud-user',
'ansible_private_key_file':
'/foo/bar',
'connection':
'ssh',
'ansible_ssh_common_args': ('-o UserKnownHostsFile=/dev/null '
'-o ControlMaster=auto '
'-o ControlPersist=60s '
'-o IdentitiesOnly=yes '
'-o StrictHostKeyChecking=no'),
}
assert x == _instance.ansible_connection_options('foo')


def test_ansible_connection_options_handles_missing_instance_config_managed(
mocker, _instance):
m = mocker.patch('molecule.util.safe_load_file')
m.side_effect = IOError

assert {} == _instance.ansible_connection_options('foo')


def test_ansible_connection_options_handles_missing_results_key_when_managed(
mocker, _instance):
m = mocker.patch('molecule.util.safe_load_file')
m.side_effect = StopIteration

assert {} == _instance.ansible_connection_options('foo')


def test_instance_config_property(_instance):
x = os.path.join(_instance._config.scenario.ephemeral_directory,
'instance_config.yml')

assert x == _instance.instance_config


@pytest.mark.parametrize(
'config_instance', ['_driver_unmanaged_section_data'], indirect=True)
def test_ssh_connection_options_property(_instance):
assert [] == _instance.ssh_connection_options

Expand Down Expand Up @@ -170,3 +295,17 @@ def test_created_unknown_when_managed_false(

def test_property(_instance):
assert 'false' == _instance._converged()


def test_get_instance_config(mocker, _instance):
m = mocker.patch('molecule.util.safe_load_file')
m.return_value = [{
'instance': 'foo',
}, {
'instance': 'bar',
}]

x = {
'instance': 'foo',
}
assert x == _instance._get_instance_config('foo')

0 comments on commit e38ae54

Please sign in to comment.