summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Plugins/AWSTags.py
blob: 556805bdef35651219d938cbf2a4148acfd2204e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
"""Query tags from AWS via boto, optionally setting group membership."""

import os
import re
import sys
import Bcfg2.Server.Plugin
from boto import connect_ec2
from Bcfg2.Server.Cache import Cache
from Bcfg2.Compat import ConfigParser


class NoInstanceFound(Exception):
    """ Raised when there's no AWS instance for a given hostname """


class AWSTagPattern(object):
    """ Handler for a single Tag entry """

    def __init__(self, name, value, groups):
        self.name = re.compile(name)
        if value is not None:
            self.value = re.compile(value)
        else:
            self.value = value
        self.groups = groups

    def get_groups(self, tags):
        """ Get groups that apply to the given tag set """
        for key, value in tags.items():
            name_match = self.name.search(key)
            if name_match:
                if self.value is not None:
                    value_match = self.value.search(value)
                    if value_match:
                        return self._munge_groups(value_match)
                else:
                    return self._munge_groups(name_match)
                break
        return []

    def _munge_groups(self, match):
        """ Replace backreferences (``$1``, ``$2``) in Group tags with
        their values in the regex. """
        rv = []
        sub = match.groups()
        for group in self.groups:
            newg = group
            for idx in range(len(sub)):
                newg = newg.replace('$%s' % (idx + 1), sub[idx])
            rv.append(newg)
        return rv

    def __str__(self):
        if self.value:
            return "%s: %s=%s: %s" % (self.__class__.__name__, self.name,
                                      self.value, self.groups)
        else:
            return "%s: %s: %s" % (self.__class__.__name__, self.name,
                                   self.groups)


class PatternFile(Bcfg2.Server.Plugin.XMLFileBacked):
    """ representation of AWSTags config.xml """
    __identifier__ = None
    create = 'AWSTags'

    def __init__(self, filename, core=None):
        Bcfg2.Server.Plugin.XMLFileBacked.__init__(self, filename,
                                                   should_monitor=True)
        self.core = core
        self.tags = []

    def Index(self):
        Bcfg2.Server.Plugin.XMLFileBacked.Index(self)
        if (self.core and
            self.core.metadata_cache_mode in ['cautious', 'aggressive']):
            self.core.metadata_cache.expire()
        self.tags = []
        for entry in self.xdata.xpath('//Tag'):
            try:
                groups = [g.text for g in entry.findall('Group')]
                self.tags.append(AWSTagPattern(entry.get("name"),
                                               entry.get("value"),
                                               groups))
            except re.error:
                self.logger.error("AWSTags: Failed to initialize pattern %s: "
                                  "%s" % (entry.get("name"),
                                          sys.exc_info()[1]))

    def get_groups(self, tags):
        """ return a list of groups that should be added to the given
        client based on patterns that match the tags """
        ret = []
        for pattern in self.tags:
            ret.extend(pattern.get_groups(tags))
        return ret


class AWSTags(Bcfg2.Server.Plugin.Plugin,
              Bcfg2.Server.Plugin.ClientRunHooks,
              Bcfg2.Server.Plugin.Connector):
    """ Query tags from AWS via boto, optionally setting group membership """
    __rmi__ = Bcfg2.Server.Plugin.Plugin.__rmi__ + ['expire_cache']

    def __init__(self, core):
        Bcfg2.Server.Plugin.Plugin.__init__(self, core)
        Bcfg2.Server.Plugin.ClientRunHooks.__init__(self)
        Bcfg2.Server.Plugin.Connector.__init__(self)
        try:
            key_id = self.core.setup.cfp.get("awstags", "access_key_id")
            secret_key = self.core.setup.cfp.get("awstags",
                                                 "secret_access_key")
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            err = sys.exc_info()[1]
            raise Bcfg2.Server.Plugin.PluginInitError(
                "AWSTags is not configured in bcfg2.conf: %s" % err)
        self.debug_log("%s: Connecting to EC2" % self.name)
        self._ec2 = connect_ec2(aws_access_key_id=key_id,
                                aws_secret_access_key=secret_key)
        self._tagcache = Cache()
        try:
            self._keep_cache = self.core.setup.cfp.getboolean("awstags",
                                                              "cache")
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
            self._keep_cache = True

        self.config = PatternFile(os.path.join(self.data, 'config.xml'),
                                  core=core)

    def _load_instance(self, hostname):
        """ Load an instance from EC2 whose private DNS name matches
        the given hostname """
        self.debug_log("AWSTags: Loading instance with private-dns-name=%s" %
                       hostname)
        filters = {'private-dns-name': hostname}
        reservations = self._ec2.get_all_instances(filters=filters)
        if reservations:
            res = reservations[0]
            if res.instances:
                return res.instances[0]
        raise NoInstanceFound(
            "AWSTags: No instance found with private-dns-name=%s" %
            hostname)

    def _get_tags_from_ec2(self, hostname):
        """ Get tags for the given host from EC2. This does not use
        the local caching layer. """
        self.debug_log("AWSTags: Getting tags for %s from AWS" %
                       hostname)
        try:
            return self._load_instance(hostname).tags
        except NoInstanceFound:
            self.debug_log(sys.exc_info()[1])
            return dict()

    def get_tags(self, metadata):
        """ Get tags for the given host.  This caches the tags locally
        if 'cache' in the ``[awstags]`` section of ``bcfg2.conf`` is
        true. """
        if not self._keep_cache:
            return self._get_tags_from_ec2(metadata)

        if metadata.hostname not in self._tagcache:
            self._tagcache[metadata.hostname] = \
                self._get_tags_from_ec2(metadata.hostname)
        return self._tagcache[metadata.hostname]

    def expire_cache(self, key=None):
        """ Expire the cache for one host, or for all hosts.  This is
        exposed as an XML-RPC RMI. """
        self._tagcache.expire(key=key)

    def start_client_run(self, metadata):
        self.expire_cache(key=metadata.hostname)
        if self.core.metadata_cache_mode == 'aggressive':
            self.logger.warning("AWSTags is incompatible with aggressive "
                                "client metadata caching, try 'cautious' "
                                "or 'initial'")
            self.core.metadata_cache.expire(metadata.hostname)

    def get_additional_data(self, metadata):
        return self.get_tags(metadata)

    def get_additional_groups(self, metadata):
        return self.config.get_groups(self.get_tags(metadata))