summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Lint/Metadata.py
blob: 3c8f2831d13c14d94dd5ce672eb5ca7a6905c3ac (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
""" ``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.
    """

    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.  We
        count a group tag as a definition if it a) has profile or
        public set; or b) has any children."""
        allgroups = [
            g
            for g in self.metadata.groups_xml.xdata.xpath("//Groups/Group") +
            self.metadata.groups_xml.xdata.xpath("//Groups/Group//Group")
            if g.get("profile") or g.get("public") or g.getchildren()]
        self.duplicate_entries(allgroups, "group")

    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 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))