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
21import abc
22import os
23
24from molecule import status
25
26Status = status.get_status()
27
28
29class Base(object):
30    __metaclass__ = abc.ABCMeta
31
32    def __init__(self, config):
33        """
34        Base initializer for all :ref:`Driver` classes.
35
36        :param config: An instance of a Molecule config.
37        :returns: None
38        """
39        self._config = config
40
41    @property
42    @abc.abstractmethod
43    def name(self):  # pragma: no cover
44        """
45        Name of the driver and returns a string.
46
47        :returns: str
48        """
49        pass
50
51    @name.setter
52    @abc.abstractmethod
53    def name(self, value):  # pragma: no cover
54        """
55        Driver name setter and returns None.
56
57        :returns: None
58        """
59        pass
60
61    @property
62    def testinfra_options(self):
63        """
64        Testinfra specific options and returns a dict.
65
66        :returns: dict
67        """
68        return {
69            'connection': 'ansible',
70            'ansible-inventory': self._config.provisioner.inventory_file,
71        }
72
73    @abc.abstractproperty
74    def login_cmd_template(self):  # pragma: no cover
75        """
76        The login command template to be populated by ``login_options`` and
77        returns a string.
78
79        :returns: str
80        """
81        pass
82
83    @abc.abstractproperty
84    def default_ssh_connection_options(self):  # pragma: no cover
85        """
86        SSH client options and returns a list.
87
88        :returns: list
89        """
90        pass
91
92    @abc.abstractproperty
93    def default_safe_files(self):  # pragma: no cover
94        """
95        Generated files to be preserved and returns a list.
96
97        :returns: list
98        """
99        pass
100
101    @abc.abstractmethod
102    def login_options(self, instance_name):  # pragma: no cover
103        """
104        Options used in the login command and returns a dict.
105
106        :param instance_name: A string containing the instance to login to.
107        :returns: dict
108        """
109        pass
110
111    @abc.abstractmethod
112    def ansible_connection_options(self, instance_name):  # pragma: no cover
113        """
114        Ansible specific connection options supplied to inventory and returns a
115        dict.
116
117        :param instance_name: A string containing the instance to login to.
118        :returns: dict
119        """
120        pass
121
122    @abc.abstractmethod
123    def sanity_checks(self):
124        """
125        Sanity checks to ensure the driver can do work successfully. For
126        example, when using the Docker driver, we want to know that the Docker
127        daemon is running and we have the correct Docker Python dependency.
128        Each driver implementation can decide what is the most stable sanity
129        check for itself.
130
131        :returns: None
132        """
133        pass
134
135    @property
136    def options(self):
137        return self._config.config['driver']['options']
138
139    @property
140    def instance_config(self):
141        return os.path.join(
142            self._config.scenario.ephemeral_directory, 'instance_config.yml'
143        )
144
145    @property
146    def ssh_connection_options(self):
147        if self._config.config['driver']['ssh_connection_options']:
148            return self._config.config['driver']['ssh_connection_options']
149        return self.default_ssh_connection_options
150
151    @property
152    def safe_files(self):
153        return self.default_safe_files + self._config.config['driver']['safe_files']
154
155    @property
156    def delegated(self):
157        """
158        Is the driver delegated and returns a bool.
159
160        :returns: bool
161        """
162        return self.name == 'delegated'
163
164    @property
165    def managed(self):
166        """
167        Is the driver is managed and returns a bool.
168
169        :returns: bool
170        """
171        return self.options['managed']
172
173    def status(self):
174        """
175        Collects the instances state and returns a list.
176
177        .. important::
178
179            Molecule assumes all instances were created successfully by
180            Ansible, otherwise Ansible would return an error on create.  This
181            may prove to be a bad assumption.  However, configuring Molecule's
182            driver to match the options passed to the playbook may prove
183            difficult.  Especially in cases where the user is provisioning
184            instances off localhost.
185        :returns: list
186        """
187        status_list = []
188        for platform in self._config.platforms.instances:
189            instance_name = platform['name']
190            driver_name = self.name
191            provisioner_name = self._config.provisioner.name
192            scenario_name = self._config.scenario.name
193
194            status_list.append(
195                Status(
196                    instance_name=instance_name,
197                    driver_name=driver_name,
198                    provisioner_name=provisioner_name,
199                    scenario_name=scenario_name,
200                    created=self._created(),
201                    converged=self._converged(),
202                )
203            )
204
205        return status_list
206
207    def _get_ssh_connection_options(self):
208        return [
209            '-o UserKnownHostsFile=/dev/null',
210            '-o ControlMaster=auto',
211            '-o ControlPersist=60s',
212            '-o IdentitiesOnly=yes',
213            '-o StrictHostKeyChecking=no',
214        ]
215
216    def _created(self):
217        return str(self._config.state.created).lower()
218
219    def _converged(self):
220        return str(self._config.state.converged).lower()
221