summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Lint/Metadata.py
blob: 3c1d49a4558ff25275c82c21a57f7887afac03e4 (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
""" ``bcfg2-lint`` plugin for :ref:`Metadata
<server-plugins-grouping-metadata>` """

from Bcfg2.Server.Lint import ServerPlugin


class Metadata(ServerPlugin):
    """ ``bcfg2-lint`` plugin for :ref:`Metadata
    <server-plugins-grouping-metadata>`.  This checks for several things:

    * ``<Client>`` tags nested inside other ``<Client>`` tags;
    * Deprecated options (like ``location="floating"``);
    * Profiles that don't exist, or that aren't profile groups;
    * Groups or clients that are defined multiple times;
    * Multiple default groups or a default group that isn't a profile
      group.
    """
    __serverplugin__ = 'Metadata'

    def Run(self):
        self.nested_clients()
        self.deprecated_options()
        self.bogus_profiles()
        self.duplicate_groups()
        self.duplicate_default_groups()
        self.duplicate_clients()
        self.default_is_profile()

    @classmethod
    def Errors(cls):
        return {"nested-client-tags": "warning",
                "deprecated-clients-options": "warning",
                "nonexistent-profile-group": "error",
                "non-profile-set-as-profile": "error",
                "duplicate-group": "error",
                "duplicate-client": "error",
                "multiple-default-groups": "error",
                "default-is-not-profile": "error"}

    def deprecated_options(self):
        """ Check for the ``location='floating'`` option, which has
        been deprecated in favor of ``floating='true'``. """
        if not hasattr(self.metadata, "clients_xml"):
            # using metadata database
            return
        clientdata = self.metadata.clients_xml.xdata
        for el in clientdata.xpath("//Client"):
            loc = el.get("location")
            if loc:
                if loc == "floating":
                    floating = True
                else:
                    floating = False
                self.LintError("deprecated-clients-options",
                               "The location='%s' option is deprecated.  "
                               "Please use floating='%s' instead:\n%s" %
                               (loc, floating, self.RenderXML(el)))

    def nested_clients(self):
        """ Check for a ``<Client/>`` tag inside a ``<Client/>`` tag,
        which is either redundant or will never match. """
        groupdata = self.metadata.groups_xml.xdata
        for el in groupdata.xpath("//Client//Client"):
            self.LintError("nested-client-tags",
                           "Client %s nested within Client tag: %s" %
                           (el.get("name"), self.RenderXML(el)))

    def bogus_profiles(self):
        """ Check for clients that have profiles that are either not
        flagged as profile groups in ``groups.xml``, or don't exist. """
        if not hasattr(self.metadata, "clients_xml"):
            # using metadata database
            return
        for client in self.metadata.clients_xml.xdata.findall('.//Client'):
            profile = client.get("profile")
            if profile not in self.metadata.groups:
                self.LintError("nonexistent-profile-group",
                               "%s has nonexistent profile group %s:\n%s" %
                               (client.get("name"), profile,
                                self.RenderXML(client)))
            elif not self.metadata.groups[profile].is_profile:
                self.LintError("non-profile-set-as-profile",
                               "%s is set as profile for %s, but %s is not a "
                               "profile group:\n%s" %
                               (profile, client.get("name"), profile,
                                self.RenderXML(client)))

    def duplicate_default_groups(self):
        """ Check for multiple default groups. """
        defaults = []
        for grp in self.metadata.groups_xml.xdata.xpath("//Groups/Group") + \
                self.metadata.groups_xml.xdata.xpath("//Groups/Group//Group"):
            if grp.get("default", "false").lower() == "true":
                defaults.append(self.RenderXML(grp))
        if len(defaults) > 1:
            self.LintError("multiple-default-groups",
                           "Multiple default groups defined:\n%s" %
                           "\n".join(defaults))

    def duplicate_clients(self):
        """ Check for clients that are defined more than once. """
        if not hasattr(self.metadata, "clients_xml"):
            # using metadata database
            return
        self.duplicate_entries(
            self.metadata.clients_xml.xdata.xpath("//Client"),
            "client")

    def duplicate_groups(self):
        """ Check  for groups that  are defined more than  once. There
        are two ways this can happen:

        1. The group is listed twice with contradictory options.
        2. The group is listed with no options *first*, and then with
           options later.

        In this context, 'first' refers to the order in which groups
        are parsed; see the loop condition below and
        _handle_groups_xml_event above for details. """
        groups = dict()
        duplicates = dict()
        for grp in self.metadata.groups_xml.xdata.xpath("//Groups/Group") + \
                self.metadata.groups_xml.xdata.xpath("//Groups/Group//Group"):
            grpname = grp.get("name")
            if grpname in duplicates:
                duplicates[grpname].append(grp)
            elif len(grp.attrib) > 1:  # group has options
                if grpname in groups:
                    duplicates[grpname] = [grp, groups[grpname]]
                else:
                    groups[grpname] = grp
            else:  # group has no options
                groups[grpname] = grp
        for grpname, grps in list(duplicates.items()):
            self.LintError("duplicate-group",
                           "Group %s is defined multiple times:\n%s" %
                           (grpname,
                            "\n".join(self.RenderXML(g) for g in grps)))

    def duplicate_entries(self, allentries, etype):
        """ Generic duplicate entry finder.

        :param allentries: A list of all entries to check for
                           duplicates.
        :type allentries: list of lxml.etree._Element
        :param etype: The entry type. This will be used to determine
                      the error name (``duplicate-<etype>``) and for
                      display to the end user.
        :type etype: string
        """
        entries = dict()
        for el in allentries:
            if el.get("name") in entries:
                entries[el.get("name")].append(self.RenderXML(el))
            else:
                entries[el.get("name")] = [self.RenderXML(el)]
        for ename, els in list(entries.items()):
            if len(els) > 1:
                self.LintError("duplicate-%s" % etype,
                               "%s %s is defined multiple times:\n%s" %
                               (etype.title(), ename, "\n".join(els)))

    def default_is_profile(self):
        """ Ensure that the default group is a profile group. """
        if (self.metadata.default and
                not self.metadata.groups[self.metadata.default].is_profile):
            xdata = \
                self.metadata.groups_xml.xdata.xpath("//Group[@name='%s']" %
                                                     self.metadata.default)[0]
            self.LintError("default-is-not-profile",
                           "Default group is not a profile group:\n%s" %
                           self.RenderXML(xdata))