1#  Copyright (c) 2015-2018 Cisco Systems, Inc.
2#
3#  Permission is hereby granted, free of charge, to any person obtaining a copy
4#  of this software and associated documentation files (the "Software"), to
5#  deal in the Software without restriction, including without limitation the
6#  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7#  sell copies of the Software, and to permit persons to whom the Software is
8#  furnished to do so, subject to the following conditions:
9#
10#  The above copyright notice and this permission notice shall be included in
11#  all copies or substantial portions of the Software.
12#
13#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18#  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19#  DEALINGS IN THE SOFTWARE.
20
21from molecule import logger
22from molecule import util
23from molecule.driver import base
24
25LOG = logger.get_logger(__name__)
26
27
28class Delegated(base.Base):
29    """
30    The class responsible for managing delegated instances.  Delegated is `not`
31    the default driver used in Molecule.
32
33    Under this driver, it is the developers responsibility to implement the
34    create and destroy playbooks.  ``Managed`` is the default behaviour of all
35    drivers.
36
37    .. code-block:: yaml
38
39        driver:
40          name: delegated
41
42    However, the developer must adhere to the instance-config API. The
43    developer's create playbook must provide the following instance-config
44    data, and the developer's destroy playbook must reset the instance-config.
45
46    .. code-block:: yaml
47
48        - address: ssh_endpoint
49          identity_file: ssh_identity_file  # mutually exclusive with password
50          instance: instance_name
51          port: ssh_port_as_string
52          user: ssh_user
53          password: ssh_password  # mutually exclusive with identity_file
54          become_method: valid_ansible_become_method  # optional
55          become_pass: password_if_required  # optional
56
57        - address: winrm_endpoint
58          instance: instance_name
59          connection: 'winrm'
60          port: winrm_port_as_string
61          user: winrm_user
62          password: winrm_password
63          winrm_transport: ntlm/credssp/kerberos
64          winrm_cert_pem: <path to the credssp public certificate key>
65          winrm_cert_key_pem: <path to the credssp private certificate key>
66          winrm_server_cert_validation: True/False
67
68    This article covers how to configure and use WinRM with Ansible:
69    https://docs.ansible.com/ansible/latest/user_guide/windows_winrm.html
70
71    Molecule can also skip the provisioning/deprovisioning steps.  It is the
72    developers responsibility to manage the instances, and properly configure
73    Molecule to connect to said instances.
74
75    .. code-block:: yaml
76
77        driver:
78          name: delegated
79          options:
80            managed: False
81            login_cmd_template: 'docker exec -ti {instance} bash'
82            ansible_connection_options:
83              ansible_connection: docker
84        platforms:
85          - name: instance-docker
86
87    .. code-block:: bash
88
89        $ docker run \\
90            -d \\
91            --name instance-docker \\
92            --hostname instance-docker \\
93            -it molecule_local/ubuntu:latest sleep infinity & wait
94
95    Use Molecule with delegated instances, which are accessible over ssh.
96
97    .. important::
98
99        It is the developer's responsibility to configure the ssh config file.
100
101    .. code-block:: yaml
102
103        driver:
104          name: delegated
105          options:
106            managed: False
107            login_cmd_template: 'ssh {instance} -F /tmp/ssh-config'
108            ansible_connection_options:
109              ansible_connection: ssh
110              ansible_ssh_common_args: '-F /path/to/ssh-config'
111        platforms:
112          - name: instance-vagrant
113
114    Provide the files Molecule will preserve post ``destroy`` action.
115
116    .. code-block:: yaml
117
118        driver:
119          name: delegated
120          safe_files:
121            - foo
122
123    And in order to use localhost as molecule's target:
124
125    .. code-block:: yaml
126
127        driver:
128          name: delegated
129          options:
130            managed: False
131            ansible_connection_options:
132              ansible_connection: local
133    """
134
135    def __init__(self, config):
136        super(Delegated, self).__init__(config)
137        self._name = 'delegated'
138
139    @property
140    def name(self):
141        return self._name
142
143    @name.setter
144    def name(self, value):
145        self._name = value
146
147    @property
148    def login_cmd_template(self):
149        if self.managed:
150            connection_options = ' '.join(self.ssh_connection_options)
151
152            return (
153                'ssh {{address}} '
154                '-l {{user}} '
155                '-p {{port}} '
156                '-i {{identity_file}} '
157                '{}'
158            ).format(connection_options)
159        return self.options['login_cmd_template']
160
161    @property
162    def default_safe_files(self):
163        return []
164
165    @property
166    def default_ssh_connection_options(self):
167        if self.managed:
168            return self._get_ssh_connection_options()
169        return []
170
171    def login_options(self, instance_name):
172        if self.managed:
173            d = {'instance': instance_name}
174
175            return util.merge_dicts(d, self._get_instance_config(instance_name))
176        return {'instance': instance_name}
177
178    def ansible_connection_options(self, instance_name):
179        if self.managed:
180            try:
181                d = self._get_instance_config(instance_name)
182                conn_dict = {}
183                conn_dict['ansible_user'] = d.get('user')
184                conn_dict['ansible_host'] = d.get('address')
185                conn_dict['ansible_port'] = d.get('port')
186                conn_dict['ansible_connection'] = d.get('connection', 'smart')
187                if d.get('become_method'):
188                    conn_dict['ansible_become_method'] = d.get('become_method')
189                if d.get('become_pass'):
190                    conn_dict['ansible_become_pass'] = d.get('become_pass')
191                if d.get('identity_file'):
192                    conn_dict['ansible_private_key_file'] = d.get('identity_file')
193                    conn_dict['ansible_ssh_common_args'] = ' '.join(
194                        self.ssh_connection_options
195                    )
196                if d.get('password'):
197                    conn_dict['ansible_password'] = d.get('password')
198                if d.get('winrm_transport'):
199                    conn_dict['ansible_winrm_transport'] = d.get('winrm_transport')
200                if d.get('winrm_cert_pem'):
201                    conn_dict['ansible_winrm_cert_pem'] = d.get('winrm_cert_pem')
202                if d.get('winrm_cert_key_pem'):
203                    conn_dict['ansible_winrm_cert_key_pem'] = d.get(
204                        'winrm_cert_key_pem'
205                    )
206                if d.get('winrm_server_cert_validation'):
207                    conn_dict['ansible_winrm_server_cert_validation'] = d.get(
208                        'winrm_server_cert_validation'
209                    )
210
211                return conn_dict
212
213            except StopIteration:
214                return {}
215            except IOError:
216                # Instance has yet to be provisioned , therefore the
217                # instance_config is not on disk.
218                return {}
219        return self.options['ansible_connection_options']
220
221    def _created(self):
222        if self.managed:
223            return super(Delegated, self)._created()
224        return 'unknown'
225
226    def _get_instance_config(self, instance_name):
227        instance_config_dict = util.safe_load_file(self._config.driver.instance_config)
228
229        return next(
230            item for item in instance_config_dict if item['instance'] == instance_name
231        )
232
233    def sanity_checks(self):
234        # Note(decentral1se): Cannot implement driver specifics are unknown
235        pass
236