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 os
22
23from molecule import logger
24from molecule import util
25
26LOG = logger.get_logger(__name__)
27VALID_KEYS = [
28    'created',
29    'converged',
30    'driver',
31    'prepared',
32    'sanity_checked',
33    'run_uuid',
34    'is_parallel',
35]
36
37
38class InvalidState(Exception):
39    """
40    Exception class raised when an error occurs in :class:`.State`.
41    """
42
43    pass
44
45
46class State(object):
47    """
48    A class which manages the state file.  Intended to be used as a singleton
49    throughout a given Molecule config.  The initial state is serialized to
50    disk if the file does not exist, otherwise is deserialized from the
51    existing state file.  Changes made to the object are immediately
52    serialized.
53
54    State is not a top level option in Molecule's config.  It's purpose is for
55    bookkeeping, and each :class:`.Config` object has a reference to a State_
56    object.
57
58    .. note::
59
60        Currently, it's use is significantly smaller than it was in v1 of
61        Molecule.
62    """
63
64    def __init__(self, config):
65        """
66        Initialize a new state class and returns None.
67
68        :param config: An instance of a Molecule config.
69        :returns: None
70        """
71        self._config = config
72        self._state_file = self._get_state_file()
73        self._data = self._get_data()
74        self._write_state_file()
75
76    def marshal(func):
77        def wrapper(self, *args, **kwargs):
78            func(self, *args, **kwargs)
79            self._write_state_file()
80
81        return wrapper
82
83    @property
84    def state_file(self):
85        return self._state_file
86
87    @property
88    def converged(self):
89        return self._data.get('converged')
90
91    @property
92    def created(self):
93        return self._data.get('created')
94
95    @property
96    def driver(self):
97        return self._data.get('driver')
98
99    @property
100    def prepared(self):
101        return self._data.get('prepared')
102
103    @property
104    def sanity_checked(self):
105        return self._data.get('sanity_checked')
106
107    @property
108    def run_uuid(self):
109        return self._data.get('run_uuid')
110
111    @property
112    def is_parallel(self):
113        return self._data.get('is_parallel')
114
115    @marshal
116    def reset(self):
117        self._data = self._default_data()
118
119    @marshal
120    def change_state(self, key, value):
121        """
122        Changes the state of the instance data with the given
123        ``key`` and the provided ``value``.
124
125        Wrapping with a decorator is probably not necessary.
126
127        :param key: A ``str`` containing the key to update
128        :param value: A value to change the ``key`` to
129        :return: None
130        """
131        if key not in VALID_KEYS:
132            raise InvalidState
133        self._data[key] = value
134
135    def _get_data(self):
136        if os.path.isfile(self.state_file):
137            return self._load_file()
138        return self._default_data()
139
140    def _default_data(self):
141        return {
142            'converged': False,
143            'created': False,
144            'driver': None,
145            'prepared': None,
146            'sanity_checked': False,
147            'run_uuid': self._config._run_uuid,
148            'is_parallel': self._config.is_parallel,
149        }
150
151    def _load_file(self):
152        return util.safe_load_file(self.state_file)
153
154    def _write_state_file(self):
155        util.write_file(self.state_file, util.safe_dump(self._data))
156
157    def _get_state_file(self):
158        return os.path.join(self._config.scenario.ephemeral_directory, 'state.yml')
159