1# Copyright: (c) 2014, Michael DeHaan <michael.dehaan@gmail.com>
2# Copyright: (c) 2018, Ansible Project
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
5
6from __future__ import (absolute_import, division, print_function)
7__metaclass__ = type
8
9
10from ansible import constants as C
11from ansible.errors import AnsibleError
12from ansible.module_utils.common._collections_compat import MutableMapping
13from ansible.plugins.loader import cache_loader
14from ansible.utils.display import Display
15
16
17display = Display()
18
19
20class FactCache(MutableMapping):
21
22    def __init__(self, *args, **kwargs):
23
24        self._plugin = cache_loader.get(C.CACHE_PLUGIN)
25        if not self._plugin:
26            raise AnsibleError('Unable to load the facts cache plugin (%s).' % (C.CACHE_PLUGIN))
27
28        super(FactCache, self).__init__(*args, **kwargs)
29
30    def __getitem__(self, key):
31        if not self._plugin.contains(key):
32            raise KeyError
33        return self._plugin.get(key)
34
35    def __setitem__(self, key, value):
36        self._plugin.set(key, value)
37
38    def __delitem__(self, key):
39        self._plugin.delete(key)
40
41    def __contains__(self, key):
42        return self._plugin.contains(key)
43
44    def __iter__(self):
45        return iter(self._plugin.keys())
46
47    def __len__(self):
48        return len(self._plugin.keys())
49
50    def copy(self):
51        """ Return a primitive copy of the keys and values from the cache. """
52        return dict(self)
53
54    def keys(self):
55        return self._plugin.keys()
56
57    def flush(self):
58        """ Flush the fact cache of all keys. """
59        self._plugin.flush()
60
61    def first_order_merge(self, key, value):
62        host_facts = {key: value}
63
64        try:
65            host_cache = self._plugin.get(key)
66            if host_cache:
67                host_cache.update(value)
68                host_facts[key] = host_cache
69        except KeyError:
70            pass
71
72        super(FactCache, self).update(host_facts)
73
74    def update(self, *args):
75        """
76        Backwards compat shim
77
78        We thought we needed this to ensure we always called the plugin's set() method but
79        MutableMapping.update() will call our __setitem__() just fine.  It's the calls to update
80        that we need to be careful of.  This contains a bug::
81
82            fact_cache[host.name].update(facts)
83
84        It retrieves a *copy* of the facts for host.name and then updates the copy.  So the changes
85        aren't persisted.
86
87        Instead we need to do::
88
89            fact_cache.update({host.name, facts})
90
91        Which will use FactCache's update() method.
92
93        We currently need this shim for backwards compat because the update() method that we had
94        implemented took key and value as arguments instead of taking a dict.  We can remove the
95        shim in 2.12 as MutableMapping.update() should do everything that we need.
96        """
97        if len(args) == 2:
98            # Deprecated.  Call the new function with this name
99            display.deprecated('Calling FactCache().update(key, value) is deprecated.  Use'
100                               ' FactCache().first_order_merge(key, value) if you want the old'
101                               ' behaviour or use FactCache().update({key: value}) if you want'
102                               ' dict-like behaviour.', version='2.12', collection_name='ansible.builtin')
103            return self.first_order_merge(*args)
104
105        elif len(args) == 1:
106            host_facts = args[0]
107
108        else:
109            raise TypeError('update expected at most 1 argument, got {0}'.format(len(args)))
110
111        super(FactCache, self).update(host_facts)
112