Skip to content
This repository
Browse code

Metadata and other improvements:

* Added support for Client tag in groups.xml
* Added support for nested Group tags in groups.xml
* Added support for negated groups in groups.xml
* Added DatabaseBacked plugin mixin to easily allow plugins to connect
  to a database specified in global database settings in bcfg2.conf
* Added DBMetadata plugin that uses relational DB to store client
  records instead of writing to clients.xml
  • Loading branch information...
commit 8b438fda3ae2d9516dbfb6014c280b68036c17e1 1 parent 7a008a0
Chris St. Pierre authored July 30, 2012

Showing 29 changed files with 1,737 additions and 605 deletions. Show diff stats Hide diff stats

  1. 2  doc/appendix/guides/authentication.txt
  2. 2  doc/appendix/guides/nat_howto.txt
  3. 6  doc/server/backends.txt
  4. 45  doc/server/database.txt
  5. 1  doc/server/index.txt
  6. 2  doc/server/plugins/generators/tgenshi/clientsxml.txt
  7. 39  doc/server/plugins/grouping/dbmetadata.txt
  8. 186  doc/server/plugins/grouping/metadata.txt
  9. 3  schemas/clients.xsd
  10. 53  schemas/metadata.xsd
  11. 34  src/lib/Bcfg2/Options.py
  12. 17  src/lib/Bcfg2/Server/Admin/Bundle.py
  13. 45  src/lib/Bcfg2/Server/Admin/Client.py
  14. 63  src/lib/Bcfg2/Server/Admin/Group.py
  15. 5  src/lib/Bcfg2/Server/Admin/Init.py
  16. 33  src/lib/Bcfg2/Server/Admin/Syncdb.py
  17. 1  src/lib/Bcfg2/Server/Admin/__init__.py
  18. 15  src/lib/Bcfg2/Server/Core.py
  19. 39  src/lib/Bcfg2/Server/Plugin.py
  20. 128  src/lib/Bcfg2/Server/Plugins/DBMetadata.py
  21. 638  src/lib/Bcfg2/Server/Plugins/Metadata.py
  22. 11  src/lib/Bcfg2/Server/Plugins/Packages/Collection.py
  23. 2  src/lib/Bcfg2/Server/Reports/settings.py
  24. 62  src/lib/Bcfg2/Server/models.py
  25. 14  src/lib/Bcfg2/manage.py
  26. 71  src/lib/Bcfg2/settings.py
  27. 13  src/sbin/bcfg2-info
  28. 407  testsuite/Testlib/TestServer/TestPlugins/TestDBMetadata.py
  29. 405  testsuite/Testlib/TestServer/TestPlugins/TestMetadata.py
2  doc/appendix/guides/authentication.txt
@@ -62,7 +62,7 @@ How Authentication Works
62 62
 
63 63
 #. Next, the ip address is verified against the client record. If the
64 64
    address doesn't match, then the client must be set to
65  
-   location=floating
  65
+   floating='true'
66 66
 
67 67
 #. Finally, the password is verified. If the client is set to secure
68 68
    mode, the only its per-client password is accepted. If it is not set
2  doc/appendix/guides/nat_howto.txt
@@ -44,7 +44,7 @@ the Client entry in clients.xml will look something like this:
44 44
 .. code-block:: xml
45 45
 
46 46
     <Client profile="desktop" name="test1"
47  
-     uuid='9001ec29-1531-4b16-8198-a71bea093d0a' location='floating'/>
  47
+     uuid='9001ec29-1531-4b16-8198-a71bea093d0a' floating='true'/>
48 48
 
49 49
 Alternatively, the Client entry can be setup like this:
50 50
 
6  doc/server/backends.txt
@@ -2,9 +2,9 @@
2 2
 
3 3
 .. _server-backends:
4 4
 
5  
-========
6  
-Backends
7  
-========
  5
+===============
  6
+Server Backends
  7
+===============
8 8
 
9 9
 .. versionadded:: 1.3.0
10 10
 
45  doc/server/database.txt
... ...
@@ -0,0 +1,45 @@
  1
+.. -*- mode: rst -*-
  2
+
  3
+.. _server-database:
  4
+
  5
+========================
  6
+Global Database Settings
  7
+========================
  8
+
  9
+.. versionadded:: 1.3.0
  10
+
  11
+Several Bcfg2 plugins, including
  12
+:ref:`server-plugins-grouping-dbmetadata` and
  13
+:ref:`server-plugins-probes-index`, can connect use a relational
  14
+database to store data.  They use the global database settings in
  15
+``bcfg2.conf``, described in this document, to connect.
  16
+
  17
+.. note::
  18
+
  19
+    The :ref:`server-plugins-statistics-dbstats` plugin and the
  20
+    :ref:`reports-dynamic` do *not* currently use the global database
  21
+    settings.  They use their own separate database configuration.
  22
+
  23
+Configuration Options
  24
+=====================
  25
+
  26
+All of the following options should go in the ``[database]`` section
  27
+of ``/etc/bcfg2.conf``.
  28
+
  29
++-------------+------------------------------------------------------------+-------------------------------+
  30
+| Option name | Description                                                | Default                       |
  31
++=============+============================================================+===============================+
  32
+| engine      | The full name of the Django database backend to use. See   | "django.db.backends.sqlite3"  |
  33
+|             | https://docs.djangoproject.com/en/dev/ref/settings/#engine |                               |
  34
+|             | for available options                                      |                               |
  35
++-------------+------------------------------------------------------------+-------------------------------+
  36
+| name        | The name of the database                                   | "/var/lib/bcfg2/bcfg2.sqlite" |
  37
++-------------+------------------------------------------------------------+-------------------------------+
  38
+| user        | The user to connect to the database as                     | None                          |
  39
++-------------+------------------------------------------------------------+-------------------------------+
  40
+| password    | The password to connect to the database with               | None                          |
  41
++-------------+------------------------------------------------------------+-------------------------------+
  42
+| host        | The host to connect to                                     | "localhost"                   |
  43
++-------------+------------------------------------------------------------+-------------------------------+
  44
+| port        | The port to connect to                                     | None                          |
  45
++-------------+------------------------------------------------------------+-------------------------------+
1  doc/server/index.txt
@@ -30,3 +30,4 @@ clients.
30 30
    bcfg2-info
31 31
    selinux
32 32
    backends
  33
+   database
2  doc/server/plugins/generators/tgenshi/clientsxml.txt
@@ -65,7 +65,7 @@ Possible improvements:
65 65
                name="${name}"
66 66
                uuid="${name}"
67 67
                password="${metadata.Properties['passwords.xml'].xdata.find('password').find('bcfg2-client').find(name).text}"
68  
-               location="floating"
  68
+               floating="true"
69 69
                secure="true"
70 70
            />\
71 71
        {% end %}\
39  doc/server/plugins/grouping/dbmetadata.txt
... ...
@@ -0,0 +1,39 @@
  1
+.. -*- mode: rst -*-
  2
+
  3
+.. _server-plugins-grouping-dbmetadata:
  4
+
  5
+==========
  6
+DBMetadata
  7
+==========
  8
+
  9
+.. versionadded:: 1.3.0
  10
+
  11
+The DBMetadata plugin is an alternative to the
  12
+:ref:`server-plugins-grouping-metadata` plugin that stores client
  13
+records in a database rather than writing back to ``clients.xml``.
  14
+This provides several advantages:
  15
+
  16
+* ``clients.xml`` will never be written by the server, removing an
  17
+  area of contention between the user and server.
  18
+* ``clients.xml`` can be removed entirely for many sites.
  19
+* The Bcfg2 client list can be queried by other machines without
  20
+  obtaining and parsing ``clients.xml``.
  21
+* A single client list can be shared amongst multiple Bcfg2 servers.
  22
+
  23
+In general, DBMetadata works almost the same as Metadata.
  24
+``groups.xml`` is parsed identically.  If ``clients.xml`` is present,
  25
+it is parsed, but ``<Client>`` tags in ``clients.xml`` *do not* assert
  26
+client existence; they are only used to set client options *if* the
  27
+client exists (in the database).  That is, the two purposes of
  28
+``clients.xml`` -- to track which clients exist, and to set client
  29
+options -- have been separated.
  30
+
  31
+With the improvements in ``groups.xml`` parsing in 1.3, client groups
  32
+can now be set directly in ``groups.xml`` with ``<Client>`` tags. (See
  33
+:ref:`metadata-client-tag` for more details.)  As a result,
  34
+``clients.xml`` is only necessary with DBMetadata if you need to set
  35
+options (e.g., aliases, floating clients, per-client passwords, etc.)
  36
+on clients.
  37
+
  38
+DBMetadata uses the :ref:`Global Server Database Settings
  39
+<server-database>` to connect to its database.
186  doc/server/plugins/grouping/metadata.txt
@@ -6,11 +6,11 @@
6 6
 Metadata
7 7
 ========
8 8
 
9  
-The metadata mechanism has two types of information, client metadata and
10  
-group metadata. The client metadata describes which top level group a
11  
-client is associated with.The group metadata describes groups in terms
12  
-of what bundles and other groups they include. Each aspect grouping
13  
-and clients' memberships are reflected in the ``Metadata/groups.xml`` and
  9
+The metadata mechanism has two types of information, client metadata
  10
+and group metadata. The client metadata describes which top level
  11
+group a client is associated with.The group metadata describes groups
  12
+in terms of what bundles and other groups they include. Group data and
  13
+clients' memberships are reflected in the ``Metadata/groups.xml`` and
14 14
 ``Metadata/clients.xml`` files, respectively.
15 15
 
16 16
 Usage of Groups in Metadata
@@ -85,9 +85,9 @@ Additionally, the following properties can be specified:
85 85
 | address  | Establishes an extra IP address that   | ip address     |
86 86
 |          | resolves to this client.               |                |
87 87
 +----------+----------------------------------------+----------------+
88  
-| location | Requires requests to come from an IP   | fixed|floating |
89  
-|          | address that matches the client        |                |
90  
-|          | record.                                |                |
  88
+| floating | Allows requests to come from any IP,   | true|false     |
  89
+|          | rather than requiring requests to come |                |
  90
+|          | from an IP associated with the client  |                |
91 91
 +----------+----------------------------------------+----------------+
92 92
 | password | Establishes a per-node password that   | String         |
93 93
 |          | can be used instead of the global      |                |
@@ -101,6 +101,9 @@ Additionally, the following properties can be specified:
101 101
 |          | resolution.                            |                |
102 102
 +----------+----------------------------------------+----------------+
103 103
 
  104
+Floating can also be configured by setting ``location="floating"``,
  105
+but that is deprecated.
  106
+
104 107
 For detailed information on client authentication see
105 108
 :ref:`appendix-guides-authentication`
106 109
 
@@ -112,31 +115,88 @@ definitions. Here's a simple ``Metadata/groups.xml`` file:
112 115
 
113 116
 .. code-block:: xml
114 117
 
115  
-    <Groups version='3.0'>
  118
+    <Groups>
116 119
       <Group name='mail-server' profile='true'
117  
-                                public='false'
118 120
                                 comment='Top level mail server group' >
119 121
         <Bundle name='mail-server'/>
120 122
         <Bundle name='mailman-server'/>
121 123
         <Group name='apache-server'/>
122  
-        <Group name='rhel-as-5-x86'/>
123 124
         <Group name='nfs-client'/>
124 125
         <Group name='server'/>
  126
+        <Group name='rhel5'>
  127
+          <Group name='sendmail-server'/>
  128
+        </Group>
  129
+        <Group name='rhel6'>
  130
+          <Group name='postfix-server'/>
  131
+        </Group>
  132
+      </Group>
  133
+      <Group name='rhel'>
  134
+        <Group name='selinux-enabled'/>
125 135
       </Group>
126  
-      <Group name='rhel-as-5-x86'>
127  
-         <Group name='rhel'/>
  136
+      <Group name='oracle-server'>
  137
+        <Group name='selinux-enabled' negate='true'/>
128 138
       </Group>
129  
-      <Group name='apache-server'/>
130  
-      <Group name='nfs-client'/>
131  
-      <Group name='server'/>
132  
-      <Group name='rhel'/>
  139
+      <Client name='foo.eample.com'>
  140
+        <Group name='oracle-server'/>
  141
+        <Group name='apache-server'/>
  142
+      </Client>
133 143
     </Groups>
134 144
 
  145
+A Group or Client tag that does not contain any child tags is a
  146
+declaration of membership; a Group or Client tag that does contain
  147
+children is a conditional.  So the example above does not assign
  148
+either the ``rhel5`` or ``rhel6`` groups to machines in the
  149
+``mail-server`` group, but conditionally assigns the
  150
+``sendmail-server`` or ``postfix-server`` groups depending on the OS
  151
+of the client.  (Presumably in this example the OS groups are set by a
  152
+probe.)
  153
+
  154
+Consequently, a client that is RHEL 5 and a member of the
  155
+``mail-server`` profile group would also be a member of the
  156
+``apache-server``, ``nfs-client``, ``server``, and ``sendmail-server``
  157
+groups; a RHEL 6 client that is a member of the ``mail-server``
  158
+profile group would be a member of the ``apache-server``,
  159
+``nfs-client``, ``server``, and ``postfix-server`` groups.
  160
+
  161
+Client tags in ``groups.xml`` allow you to supplement the profile
  162
+group declarations in ``clients.xml`` and/or client group assignments
  163
+with the :ref:`server-plugins-grouping-grouppatterns` plugin.  They
  164
+should be used sparingly.  (They are more useful with the
  165
+:ref:`server-plugins-grouping-dbmetadata` plugin.)
  166
+
  167
+You can also declare that a group should be negated; this allows you
  168
+to set defaults and override them efficiently.  Negation is applied
  169
+after other group memberships are calculated, so it doesn't matter how
  170
+many times a client is assigned to a group or how many times it is
  171
+negated; a single group negation is sufficient to remove a client from
  172
+that group.  For instance, in the following example,
  173
+``foo.example.com`` is **not** a member of ``selinux-enabled``, even
  174
+though it is a member of the ``foo-server`` and ``every-server``
  175
+groups:
  176
+
  177
+.. code-block:: xml
  178
+
  179
+    <Groups>
  180
+      <Group name="foo-server">
  181
+        <Group name="apache-server"/>
  182
+        <Group name="selinux-enabled"/>
  183
+      </Group>
  184
+      <Group name="apache-server">
  185
+        <Group name="selinux-enabled"/>
  186
+      </Group>
  187
+      <Group name="every-server">
  188
+        <Group name="selinux-enabled"/>
  189
+      </Group>
  190
+      <Client name="foo.example.com">
  191
+        <Group name="selinux-enabled" negate="true"/>
  192
+      </Client>
  193
+
  194
+.. note::
135 195
 
136  
-Nested/chained groups definitions are conjunctive (logical and). For
137  
-instance, in the above example, a client associated with the Profile
138  
-Group ``mail-server`` is also a member of the ``apache-server``,
139  
-``rhel-as-5-x86``, ``nfs-client``, ``server``, and ``rhel`` groups.
  196
+    Nested Group conditionals, Client tags, and negated Group tags are
  197
+    all new in 1.3.0.
  198
+
  199
+Order of ``groups.xml`` does not matter.
140 200
 
141 201
 Groups describe clients in terms for abstract, disjoint aspects. Groups
142 202
 can be combined to form complex descriptions of clients that use
@@ -165,33 +225,63 @@ Metadata Group Tag
165 225
 
166 226
 The Group Tag has the following possible attributes:
167 227
 
168  
-+----------+------------------------------------------+--------------+
169  
-| Name     | Description                              | Values       |
170  
-+==========+==========================================+==============+
171  
-| name     | Name of the group                        | String       |
172  
-+----------+------------------------------------------+--------------+
173  
-| profile  | If a client can be directly associated   | True|False   |
174  
-|          | with this group                          |              |
175  
-+----------+------------------------------------------+--------------+
176  
-| public   | If a client can freely associate itself  | True|False   |
177  
-|          | with this group. For use with the        |              |
178  
-|          | *bcfg2 -p* option on the client.         |              |
179  
-+----------+------------------------------------------+--------------+
180  
-| category | A group can only contain one instance of | String       |
181  
-|          | a group in any one category. This        |              |
182  
-|          | provides the basis for representing      |              |
183  
-|          | groups which are conjugates of one       |              |
184  
-|          | another in a rigorous way. It also       |              |
185  
-|          | provides the basis for negation.         |              |
186  
-+----------+------------------------------------------+--------------+
187  
-| default  | Set as the profile to use for clients    | True|False   |
188  
-|          | that are not associated with a profile   |              |
189  
-|          | in ``clients.xml``                       |              |
190  
-+----------+------------------------------------------+--------------+
191  
-| comment  | English text description of group        | String       |
192  
-+----------+------------------------------------------+--------------+
193  
-
194  
-Groups can also contain other groups and bundles.
  228
++----------+----------------------------------------------+--------------+
  229
+| Name     | Description                                  | Values       |
  230
++==========+==============================================+==============+
  231
+| name     | Name of the group                            | String       |
  232
++----------+----------------------------------------------+--------------+
  233
+| profile  | If a client can be directly associated with  | True|False   |
  234
+|          | this group                                   |              |
  235
++----------+----------------------------------------------+--------------+
  236
+| public   | If a client can freely associate itself with | True|False   |
  237
+|          | this group. For use with the ``bcfg2 -p``    |              |
  238
+|          | option on the client.                        |              |
  239
++----------+----------------------------------------------+--------------+
  240
+| category | A group can only contain one instance of a   | String       |
  241
+|          | group in any one category. This provides the |              |
  242
+|          | basis for representing groups which are      |              |
  243
+|          | conjugates of one another in a rigorous way. |              |
  244
+|          | way.                                         |
  245
++----------+----------------------------------------------+--------------+
  246
+| default  | Set as the profile to use for clients that   | True|False   |
  247
+|          | are not associated with a profile in         |              |
  248
+|          | ``clients.xml``                              |              |
  249
++----------+----------------------------------------------+--------------+
  250
+| comment  | English text description of group            | String       |
  251
++----------+----------------------------------------------+--------------+
  252
+| negate   | When used as a conditional, only apply the   | True|False   |
  253
+|          | children if the named group does not match.  |              |
  254
+|          | When used as a declaration, do not apply     |              |
  255
+|          | the named group to matching clients.         |              |
  256
++----------+----------------------------------------------+--------------+
  257
+
  258
+The ``profile``, ``public``, ``category``, ``default``, and
  259
+``comment`` attributes are only parsed if a Group tag either a) is the
  260
+direct child of a Groups tag (i.e., at the top level of an XML file);
  261
+or b) has no children.  This matches legacy behavior in Bcfg2 1.2 and
  262
+earlier.
  263
+
  264
+Groups can also contain other groups, clients, and bundles.
  265
+
  266
+.. _metadata-client-tag:
  267
+
  268
+Metadata Client Tag
  269
+-------------------
  270
+
  271
+The Client Tag has the following possible attributes:
  272
+
  273
++----------+-----------------------------------------------+--------------+
  274
+| Name     | Description                                   | Values       |
  275
++==========+===============================================+==============+
  276
+| name     | Name of the client                            | String       |
  277
++----------+-----------------------------------------------+--------------+
  278
+| negate   | Only apply the child tags if the named client | True|False   |
  279
+|          | does not match.                               |              |
  280
++----------+-----------------------------------------------+--------------+
  281
+
  282
+Clients can also contain groups, other clients (although that's likely
  283
+nonsensical), and bundles.
  284
+
195 285
 
196 286
 Use of XInclude
197 287
 ===============
3  schemas/clients.xsd
@@ -26,7 +26,8 @@
26 26
     <xsd:attribute type='xsd:string' name='uuid'/>
27 27
     <xsd:attribute type='xsd:string' name='password'/>
28 28
     <xsd:attribute type='xsd:string' name='location'/>
29  
-    <xsd:attribute type='xsd:string' name='secure'/>
  29
+    <xsd:attribute type='xsd:boolean' name='floating'/>
  30
+    <xsd:attribute type='xsd:boolean' name='secure'/>
30 31
     <xsd:attribute type='xsd:string' name='pingtime' use='optional'/>
31 32
     <xsd:attribute type='xsd:string' name='address'/>
32 33
     <xsd:attribute type='xsd:string' name='version'/>
53  schemas/metadata.xsd
... ...
@@ -1,6 +1,6 @@
1 1
 <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
2 2
             xmlns:xi="http://www.w3.org/2001/XInclude" xml:lang="en">
3  
-  
  3
+
4 4
   <xsd:annotation>
5 5
     <xsd:documentation>
6 6
       metadata schema for bcfg2
@@ -13,38 +13,51 @@
13 13
   <xsd:import namespace="http://www.w3.org/2001/XInclude"
14 14
               schemaLocation="xinclude.xsd"/>
15 15
 
  16
+  <xsd:complexType name='bundleDeclaration'>
  17
+    <xsd:attribute type='xsd:string' name='name' use='required'/>
  18
+  </xsd:complexType>
  19
+
16 20
   <xsd:complexType name='groupType'>
17 21
     <xsd:choice minOccurs='0' maxOccurs='unbounded'>
18  
-      <xsd:element name='Bundle'>
19  
-        <xsd:complexType>
20  
-          <xsd:attribute type='xsd:string' name='name' use='required'/>
21  
-        </xsd:complexType>
22  
-      </xsd:element>
23  
-      <xsd:element name='Group' >
24  
-        <xsd:complexType>
25  
-          <xsd:attribute name='name' use='required'/>
26  
-        </xsd:complexType>
27  
-      </xsd:element>
  22
+      <xsd:element name='Bundle' type='bundleDeclaration'/>
  23
+      <xsd:element name='Group' type='groupType'/>
  24
+      <xsd:element name='Client' type='clientType'/>
  25
+      <xsd:element name='Groups' type='groupsType'/>
  26
+      <xsd:element name='Options' type='optionsType'/>
  27
+    </xsd:choice>
  28
+    <xsd:attribute type='xsd:string' name='name' use='required'/>
  29
+    <xsd:attribute type='xsd:boolean' name='profile'/>
  30
+    <xsd:attribute type='xsd:boolean' name='public'/>
  31
+    <xsd:attribute type='xsd:boolean' name='default'/>
  32
+    <xsd:attribute type='xsd:string' name='auth'/>
  33
+    <xsd:attribute type='xsd:string' name='category'/>
  34
+    <xsd:attribute type='xsd:string' name='comment'/>
  35
+    <xsd:attribute type='xsd:string' name='negate'/>
  36
+  </xsd:complexType>
  37
+
  38
+  <xsd:complexType name='clientType'>
  39
+    <xsd:choice minOccurs='0' maxOccurs='unbounded'>
  40
+      <xsd:element name='Bundle' type='bundleDeclaration'/>
  41
+      <xsd:element name='Group' type='groupType'/>
  42
+      <xsd:element name='Client' type='clientType'/>
  43
+      <xsd:element name='Groups' type='groupsType'/>
  44
+      <xsd:element name='Options' type='optionsType'/>
28 45
     </xsd:choice>
29  
-    <xsd:attribute type='xsd:boolean' name='profile' use='optional'/>
30  
-    <xsd:attribute type='xsd:boolean' name='public' use='optional'/>
31  
-    <xsd:attribute type='xsd:boolean' name='default' use='optional'/>
32 46
     <xsd:attribute type='xsd:string' name='name' use='required'/>
33  
-    <xsd:attribute type='xsd:string' name='auth' use='optional'/>
34  
-    <xsd:attribute type='xsd:string' name='category' use='optional'/>
35  
-    <xsd:attribute type='xsd:string' name='comment' use='optional'/>
  47
+    <xsd:attribute type='xsd:string' name='negate'/>
36 48
   </xsd:complexType>
37 49
 
38 50
   <xsd:complexType name='groupsType'>
39 51
     <xsd:choice minOccurs='0' maxOccurs='unbounded'>
40 52
       <xsd:element name='Group' type='groupType'/>
  53
+      <xsd:element name='Client' type='clientType'/>
41 54
       <xsd:element name='Groups' type='groupsType'/>
42 55
       <xsd:element ref="xi:include"/>
43 56
     </xsd:choice>
44 57
     <xsd:attribute name='version' type='xsd:string'/>
45  
-    <xsd:attribute name='origin' type='xsd:string'/>   
46  
-    <xsd:attribute name='revision' type='xsd:string'/>  
47  
-    <xsd:attribute ref='xml:base'/>  
  58
+    <xsd:attribute name='origin' type='xsd:string'/>
  59
+    <xsd:attribute name='revision' type='xsd:string'/>
  60
+    <xsd:attribute ref='xml:base'/>
48 61
   </xsd:complexType>
49 62
 
50 63
   <xsd:element name='Groups' type='groupsType'/>
34  src/lib/Bcfg2/Options.py
@@ -224,6 +224,7 @@ def get_bool(s):
224 224
         return False
225 225
     else:
226 226
         raise ValueError
  227
+        
227 228
 
228 229
 """
229 230
 Options:
@@ -424,6 +425,32 @@ def get_bool(s):
424 425
            default='best',
425 426
            cf=('server', 'backend'))
426 427
 
  428
+# database options
  429
+DB_ENGINE = \
  430
+    Option('Database engine',
  431
+           default='django.db.backends.sqlite3',
  432
+           cf=('database', 'engine'))
  433
+DB_NAME = \
  434
+    Option('Database name',
  435
+           default=os.path.join(SERVER_REPOSITORY.default, "bcfg2.sqlite"),
  436
+           cf=('database', 'name'))
  437
+DB_USER = \
  438
+    Option('Database username',
  439
+           default=None,
  440
+           cf=('database', 'user'))
  441
+DB_PASSWORD = \
  442
+    Option('Database password',
  443
+           default=None,
  444
+           cf=('database', 'password'))
  445
+DB_HOST = \
  446
+    Option('Database host',
  447
+           default='localhost',
  448
+           cf=('database', 'host'))
  449
+DB_PORT = \
  450
+    Option('Database port',
  451
+           default='',
  452
+           cf=('database', 'port'),)
  453
+
427 454
 # Client options
428 455
 CLIENT_KEY = \
429 456
     Option('Path to SSL key',
@@ -898,12 +925,15 @@ class OptionParser(OptionSet):
898 925
        OptionParser bootstraps option parsing,
899 926
        getting the value of the config file
900 927
     """
901  
-    def __init__(self, args, argv=None):
  928
+    def __init__(self, args, argv=None, quiet=False):
902 929
         if argv is None:
903 930
             argv = sys.argv[1:]
  931
+        # the bootstrap is always quiet, since it's running with a
  932
+        # default config file and so might produce warnings otherwise
904 933
         self.Bootstrap = OptionSet([('configfile', CFILE)], quiet=True)
905 934
         self.Bootstrap.parse(argv, do_getopt=False)
906  
-        OptionSet.__init__(self, args, configfile=self.Bootstrap['configfile'])
  935
+        OptionSet.__init__(self, args, configfile=self.Bootstrap['configfile'],
  936
+                           quiet=quiet)
907 937
         self.optinfo = copy.copy(args)
908 938
 
909 939
     def HandleEvent(self, event):
17  src/lib/Bcfg2/Server/Admin/Bundle.py
@@ -8,12 +8,11 @@
8 8
 
9 9
 
10 10
 class Bundle(Bcfg2.Server.Admin.MetadataCore):
11  
-    __shorthelp__ = "Create or delete bundle entries"
12  
-    # TODO: add/del functions
  11
+    __shorthelp__ = "List and view bundle entries"
13 12
     __longhelp__ = (__shorthelp__ + "\n\nbcfg2-admin bundle list-xml"
14 13
                                     "\nbcfg2-admin bundle list-genshi"
15 14
                                     "\nbcfg2-admin bundle show\n")
16  
-    __usage__ = ("bcfg2-admin bundle [options] [add|del] [group]")
  15
+    __usage__ = ("bcfg2-admin bundle [options] [list-xml|list-genshi|show]")
17 16
 
18 17
     def __call__(self, args):
19 18
         Bcfg2.Server.Admin.MetadataCore.__call__(self, args)
@@ -28,18 +27,6 @@ def __call__(self, args):
28 27
         if len(args) == 0:
29 28
             self.errExit("No argument specified.\n"
30 29
                          "Please see bcfg2-admin bundle help for usage.")
31  
-#        if args[0] == 'add':
32  
-#            try:
33  
-#                self.metadata.add_bundle(args[1])
34  
-#            except MetadataConsistencyError:
35  
-#                print("Error in adding bundle.")
36  
-#                raise SystemExit(1)
37  
-#        elif args[0] in ['delete', 'remove', 'del', 'rm']:
38  
-#            try:
39  
-#                self.metadata.remove_bundle(args[1])
40  
-#            except MetadataConsistencyError:
41  
-#                print("Error in deleting bundle.")
42  
-#                raise SystemExit(1)
43 30
         # Lists all available xml bundles
44 31
         elif args[0] in ['list-xml', 'ls-xml']:
45 32
             bundle_name = []
45  src/lib/Bcfg2/Server/Admin/Client.py
@@ -4,50 +4,23 @@
4 4
 
5 5
 
6 6
 class Client(Bcfg2.Server.Admin.MetadataCore):
7  
-    __shorthelp__ = "Create, delete, or modify client entries"
  7
+    __shorthelp__ = "Create, delete, or list client entries"
8 8
     __longhelp__ = (__shorthelp__ + "\n\nbcfg2-admin client add <client> "
9  
-                                    "attr1=val1 attr2=val2"
10  
-                                    "\nbcfg2-admin client update <client> "
11  
-                                    "attr1=val1 attr2=val2"
12 9
                                     "\nbcfg2-admin client list"
13 10
                                     "\nbcfg2-admin client del <client>\n")
14  
-    __usage__ = ("bcfg2-admin client [options] [add|del|update|list] [attr=val]")
  11
+    __usage__ = ("bcfg2-admin client [options] [add|del|list] [attr=val]")
15 12
 
16 13
     def __call__(self, args):
17 14
         Bcfg2.Server.Admin.MetadataCore.__call__(self, args)
18 15
         if len(args) == 0:
19 16
             self.errExit("No argument specified.\n"
20  
-                         "Please see bcfg2-admin client help for usage.")
  17
+                         "Usage: %s" % self.usage)
21 18
         if args[0] == 'add':
22  
-            attr_d = {}
23  
-            for i in args[2:]:
24  
-                attr, val = i.split('=', 1)
25  
-                if attr not in ['profile', 'uuid', 'password',
26  
-                                'location', 'secure', 'address',
27  
-                                'auth']:
28  
-                    print("Attribute %s unknown" % attr)
29  
-                    raise SystemExit(1)
30  
-                attr_d[attr] = val
31 19
             try:
32  
-                self.metadata.add_client(args[1], attr_d)
  20
+                self.metadata.add_client(args[1])
33 21
             except MetadataConsistencyError:
34 22
                 print("Error in adding client")
35 23
                 raise SystemExit(1)
36  
-        elif args[0] in ['update', 'up']:
37  
-            attr_d = {}
38  
-            for i in args[2:]:
39  
-                attr, val = i.split('=', 1)
40  
-                if attr not in ['profile', 'uuid', 'password',
41  
-                                'location', 'secure', 'address',
42  
-                                'auth']:
43  
-                    print("Attribute %s unknown" % attr)
44  
-                    raise SystemExit(1)
45  
-                attr_d[attr] = val
46  
-            try:
47  
-                self.metadata.update_client(args[1], attr_d)
48  
-            except MetadataConsistencyError:
49  
-                print("Error in updating client")
50  
-                raise SystemExit(1)
51 24
         elif args[0] in ['delete', 'remove', 'del', 'rm']:
52 25
             try:
53 26
                 self.metadata.remove_client(args[1])
@@ -55,7 +28,9 @@ def __call__(self, args):
55 28
                 print("Error in deleting client")
56 29
                 raise SystemExit(1)
57 30
         elif args[0] in ['list', 'ls']:
58  
-            tree = lxml.etree.parse(self.metadata.data + "/clients.xml")
59  
-            tree.xinclude()
60  
-            for node in tree.findall("//Client"):
61  
-                print(node.attrib["name"])
  31
+            for client in self.metadata.list_clients():
  32
+                print(client.hostname)
  33
+        else:
  34
+            print("No command specified")
  35
+            raise SystemExit(1)
  36
+
63  src/lib/Bcfg2/Server/Admin/Group.py
... ...
@@ -1,63 +0,0 @@
1  
-import lxml.etree
2  
-import Bcfg2.Server.Admin
3  
-from Bcfg2.Server.Plugins.Metadata import MetadataConsistencyError
4  
-
5  
-
6  
-class Group(Bcfg2.Server.Admin.MetadataCore):
7  
-    __shorthelp__ = "Create, delete, or modify group entries"
8  
-    __longhelp__ = (__shorthelp__ + "\n\nbcfg2-admin group add <group> "
9  
-                                    "attr1=val1 attr2=val2"
10  
-                                    "\nbcfg2-admin group update <group> "
11  
-                                    "attr1=val1 attr2=val2"
12  
-                                    "\nbcfg2-admin group list"
13  
-                                    "\nbcfg2-admin group del <group>\n")
14  
-    __usage__ = ("bcfg2-admin group [options] [add|del|update|list] [attr=val]")
15  
-
16  
-    def __call__(self, args):
17  
-        Bcfg2.Server.Admin.MetadataCore.__call__(self, args)
18  
-        if len(args) == 0:
19  
-            self.errExit("No argument specified.\n"
20  
-                         "Please see bcfg2-admin group help for usage.")
21  
-        if args[0] == 'add':
22  
-            attr_d = {}
23  
-            for i in args[2:]:
24  
-                attr, val = i.split('=', 1)
25  
-                if attr not in ['profile', 'public', 'default',
26  
-                               'name', 'auth', 'toolset', 'category',
27  
-                               'comment']:
28  
-                    print("Attribute %s unknown" % attr)
29  
-                    raise SystemExit(1)
30  
-                attr_d[attr] = val
31  
-            try:
32  
-                self.metadata.add_group(args[1], attr_d)
33  
-            except MetadataConsistencyError:
34  
-                print("Error in adding group")
35  
-                raise SystemExit(1)
36  
-        elif args[0] in ['update', 'up']:
37  
-            attr_d = {}
38  
-            for i in args[2:]:
39  
-                attr, val = i.split('=', 1)
40  
-                if attr not in ['profile', 'public', 'default',
41  
-                                'name', 'auth', 'toolset', 'category',
42  
-                                'comment']:
43  
-                    print("Attribute %s unknown" % attr)
44  
-                    raise SystemExit(1)
45  
-                attr_d[attr] = val
46  
-            try:
47  
-                self.metadata.update_group(args[1], attr_d)
48  
-            except MetadataConsistencyError:
49  
-                print("Error in updating group")
50  
-                raise SystemExit(1)
51  
-        elif args[0] in ['delete', 'remove', 'del', 'rm']:
52  
-            try:
53  
-                self.metadata.remove_group(args[1])
54  
-            except MetadataConsistencyError:
55  
-                print("Error in deleting group")
56  
-                raise SystemExit(1)
57  
-        elif args[0] in ['list', 'ls']:
58  
-            tree = lxml.etree.parse(self.metadata.data + "/groups.xml")
59  
-            for node in tree.findall("//Group"):
60  
-                print(node.attrib["name"])
61  
-        else:
62  
-            print("No command specified")
63  
-            raise SystemExit(1)
5  src/lib/Bcfg2/Server/Admin/Init.py
@@ -308,9 +308,8 @@ def _init_plugins(self):
308 308
         for plugin in self.plugins:
309 309
             if plugin == 'Metadata':
310 310
                 Bcfg2.Server.Plugins.Metadata.Metadata.init_repo(self.repopath,
311  
-                                                                 groups,
312  
-                                                                 self.os_sel,
313  
-                                                                 clients)
  311
+                                                                 groups_xml=groups % self.os_sel,
  312
+                                                                 clients_xml=clients)
314 313
             else:
315 314
                 try:
316 315
                     module = __import__("Bcfg2.Server.Plugins.%s" % plugin, '',
33  src/lib/Bcfg2/Server/Admin/Syncdb.py
... ...
@@ -0,0 +1,33 @@
  1
+import Bcfg2.settings
  2
+import Bcfg2.Options
  3
+import Bcfg2.Server.Admin
  4
+from django.core.management import setup_environ
  5
+
  6
+class Syncdb(Bcfg2.Server.Admin.Mode):
  7
+    __shorthelp__ = ("Sync the Django ORM with the configured database")
  8
+    __longhelp__ = __shorthelp__ + "\n\nbcfg2-admin syncdb"
  9
+    __usage__ = "bcfg2-admin syncdb"
  10
+    options = {'configfile': Bcfg2.Options.CFILE,
  11
+               'repo': Bcfg2.Options.SERVER_REPOSITORY}
  12
+
  13
+    def __call__(self, args):
  14
+        Bcfg2.Server.Admin.Mode.__call__(self, args)
  15
+
  16
+        # Parse options
  17
+        self.opts = Bcfg2.Options.OptionParser(self.options)
  18
+        self.opts.parse(args)
  19
+
  20
+        # we have to set up the django environment before we import
  21
+        # the syncdb command, but we have to wait to set up the
  22
+        # environment until we've read the config, which has to wait
  23
+        # until we've parsed options.  it's a windy, twisting road.
  24
+        Bcfg2.settings.read_config(cfile=self.opts['configfile'],
  25
+                                   repo=self.opts['repo'])
  26
+        setup_environ(Bcfg2.settings)
  27
+        import Bcfg2.Server.models
  28
+        Bcfg2.Server.models.load_models(cfile=self.opts['configfile'])
  29
+
  30
+        from django.core.management.commands import syncdb
  31
+
  32
+        cmd = syncdb.Command()
  33
+        cmd.handle_noargs(interactive=False)
1  src/lib/Bcfg2/Server/Admin/__init__.py
@@ -11,6 +11,7 @@
11 11
         'Query',
12 12
         'Reports',
13 13
         'Snapshots',
  14
+        'Syncdb',
14 15
         'Tidy',
15 16
         'Viz',
16 17
         'Xcmd'
15  src/lib/Bcfg2/Server/Core.py
... ...
@@ -1,5 +1,6 @@
1 1
 """Bcfg2.Server.Core provides the runtime support for Bcfg2 modules."""
2 2
 
  3
+import os
3 4
 import atexit
4 5
 import logging
5 6
 import select
@@ -9,6 +10,11 @@
9 10
 import inspect
10 11
 import lxml.etree
11 12
 from traceback import format_exc
  13
+
  14
+# this must be set before we import the Metadata plugin
  15
+os.environ['DJANGO_SETTINGS_MODULE'] = 'Bcfg2.settings'
  16
+
  17
+import Bcfg2.settings
12 18
 import Bcfg2.Server
13 19
 import Bcfg2.Logger
14 20
 import Bcfg2.Server.FileMonitor
@@ -95,6 +101,10 @@ def __init__(self, setup, start_fam_thread=False):
95 101
         # Create an event to signal worker threads to shutdown
96 102
         self.terminate = threading.Event()
97 103
 
  104
+        # generate Django ORM settings.  this must be done _before_ we
  105
+        # load plugins
  106
+        Bcfg2.settings.read_config(cfile=self.cfile, repo=self.datastore)
  107
+
98 108
         if '' in setup['plugins']:
99 109
             setup['plugins'].remove('')
100 110
 
@@ -195,8 +205,7 @@ def init_plugins(self, plugin):
195 205
         try:
196 206
             self.plugins[plugin] = plug(self, self.datastore)
197 207
         except PluginInitError:
198  
-            self.logger.error("Failed to instantiate plugin %s" % plugin,
199  
-                              exc_info=1)
  208
+            logger.error("Failed to instantiate plugin %s" % plugin, exc_info=1)
200 209
         except:
201 210
             self.logger.error("Unexpected instantiation failure for plugin %s" %
202 211
                               plugin, exc_info=1)
@@ -526,8 +535,6 @@ def GetProbes(self, address):
526 535
     def RecvProbeData(self, address, probedata):
527 536
         """Receive probe data from clients."""
528 537
         client, metadata = self.resolve_client(address)
529  
-        # clear dynamic groups
530  
-        self.metadata.cgroups[metadata.hostname] = []
531 538
         try:
532 539
             xpdata = lxml.etree.XML(probedata.encode('utf-8'),
533 540
                                     parser=Bcfg2.Server.XMLParser)
39  src/lib/Bcfg2/Server/Plugin.py
@@ -102,6 +102,16 @@ def debug_log(self, message, flag=None):
102 102
             self.logger.error(message)
103 103
 
104 104
 
  105
+class DatabaseBacked(object):
  106
+    def __init__(self):
  107
+        pass
  108
+
  109
+
  110
+class PluginDatabaseModel(object):
  111
+    class Meta:
  112
+        app_label = "Server"
  113
+
  114
+
105 115
 class Plugin(Debuggable):
106 116
     """This is the base class for all Bcfg2 Server plugins.
107 117
     Several attributes must be defined in the subclass:
@@ -139,8 +149,7 @@ def __init__(self, core, datastore):
139 149
 
140 150
     @classmethod
141 151
     def init_repo(cls, repo):
142  
-        path = "%s/%s" % (repo, cls.name)
143  
-        os.makedirs(path)
  152
+        os.makedirs(os.path.join(repo, cls.name))
144 153
 
145 154
     def shutdown(self):
146 155
         self.running = False
@@ -169,7 +178,7 @@ def BuildStructures(self, metadata):
169 178
 
170 179
 class Metadata(object):
171 180
     """Signal metadata capabilities for this plugin"""
172  
-    def add_client(self, client_name, attribs):
  181
+    def add_client(self, client_name):
173 182
         """Add client."""
174 183
         pass
175 184
 
@@ -181,6 +190,9 @@ def viz(self, hosts, bundles, key, colors):
181 190
         """Create viz str for viz admin mode."""
182 191
         pass
183 192
 
  193
+    def _handle_default_event(self, event):
  194
+        pass
  195
+
184 196
     def get_initial_metadata(self, client_name):
185 197
         raise PluginExecutionError
186 198
 
@@ -650,7 +662,7 @@ def Index(self):
650 662
 
651 663
     def add_monitor(self, fpath, fname):
652 664
         self.extras.append(fname)
653  
-        if self.fam:
  665
+        if self.fam and self.should_monitor:
654 666
             self.fam.AddMonitor(fpath, self)
655 667
 
656 668
     def __iter__(self):
@@ -666,22 +678,13 @@ class StructFile(XMLFileBacked):
666 678
 
667 679
     def _include_element(self, item, metadata):
668 680
         """ determine if an XML element matches the metadata """
  681
+        negate = item.get('negate', 'false').lower() == 'true'
669 682
         if item.tag == 'Group':
670  
-            if ((item.get('negate', 'false').lower() == 'true' and
671  
-                 item.get('name') not in metadata.groups) or
672  
-                (item.get('negate', 'false').lower() == 'false' and
673  
-                 item.get('name') in metadata.groups)):
674  
-                return True
675  
-            else:
676  
-                return False
  683
+            return ((negate and item.get('name') not in metadata.groups) or
  684
+                    (not negate and item.get('name') in metadata.groups))
677 685
         elif item.tag == 'Client':
678  
-            if ((item.get('negate', 'false').lower() == 'true' and
679  
-                 item.get('name') != metadata.hostname) or
680  
-                (item.get('negate', 'false').lower() == 'false' and
681  
-                 item.get('name') == metadata.hostname)):
682  
-                return True
683  
-            else:
684  
-                return False
  686
+            return ((negate and item.get('name') != metadata.hostname) or
  687
+                    (not negate and item.get('name') == metadata.hostname))
685 688
         elif isinstance(item, lxml.etree._Comment):
686 689
             return False
687 690
         else:
128  src/lib/Bcfg2/Server/Plugins/DBMetadata.py
... ...
@@ -0,0 +1,128 @@
  1
+import os
  2
+import sys
  3
+from UserDict import DictMixin
  4
+from django.db import models
  5
+import Bcfg2.Server.Lint
  6
+import Bcfg2.Server.Plugin
  7
+from Bcfg2.Server.Plugins.Metadata import *
  8
+
  9
+class MetadataClientModel(models.Model,
  10
+                          Bcfg2.Server.Plugin.PluginDatabaseModel):
  11
+    hostname = models.CharField(max_length=255, primary_key=True)
  12
+    version = models.CharField(max_length=31, null=True)
  13
+
  14
+
  15
+class ClientVersions(DictMixin):
  16
+    def __getitem__(self, key):
  17
+        try:
  18
+            return MetadataClientModel.objects.get(hostname=key).version
  19
+        except MetadataClientModel.DoesNotExist:
  20
+            raise KeyError(key)
  21
+
  22
+    def __setitem__(self, key, value):
  23
+        client = MetadataClientModel.objects.get_or_create(hostname=key)[0]
  24
+        client.version = value
  25
+        client.save()
  26
+
  27
+    def keys(self):
  28
+        return [c.hostname for c in MetadataClientModel.objects.all()]
  29
+
  30
+    def __contains__(self, key):
  31
+        try:
  32
+            client = MetadataClientModel.objects.get(hostname=key)
  33
+            return True
  34
+        except MetadataClientModel.DoesNotExist:
  35
+            return False
  36
+
  37
+
  38
+class DBMetadata(Metadata, Bcfg2.Server.Plugin.DatabaseBacked):
  39
+    __files__ = ["groups.xml"]
  40
+    experimental = True
  41
+    conflicts = ['Metadata']
  42
+
  43
+    def __init__(self, core, datastore, watch_clients=True):
  44
+        Metadata.__init__(self, core, datastore, watch_clients=watch_clients)
  45
+        Bcfg2.Server.Plugin.DatabaseBacked.__init__(self)
  46
+        if os.path.exists(os.path.join(self.data, "clients.xml")):
  47
+            self.logger.warning("DBMetadata: clients.xml found, parsing in "
  48
+                                "compatibility mode")
  49
+            self._handle_file("clients.xml")
  50
+        self.versions = ClientVersions()
  51
+
  52
+    def add_group(self, group_name, attribs):
  53
+        msg = "DBMetadata does not support adding groups"
  54
+        self.logger.error(msg)
  55
+        raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
  56
+
  57
+    def add_bundle(self, bundle_name):
  58
+        msg = "DBMetadata does not support adding bundles"
  59
+        self.logger.error(msg)
  60
+        raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
  61
+
  62
+    def add_client(self, client_name):
  63
+        """Add client to clients database."""
  64
+        client = MetadataClientModel(hostname=client_name)
  65
+        client.save()
  66
+        self.clients = self.list_clients()
  67
+        return client
  68
+
  69
+    def update_group(self, group_name, attribs):
  70
+        msg = "DBMetadata does not support updating groups"
  71
+        self.logger.error(msg)
  72
+        raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
  73
+
  74
+    def update_bundle(self, bundle_name):
  75
+        msg = "DBMetadata does not support updating bundles"
  76
+        self.logger.error(msg)
  77
+        raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
  78
+
  79
+    def update_client(self, client_name, attribs):
  80
+        msg = "DBMetadata does not support updating clients"
  81
+        self.logger.error(msg)
  82
+        raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
  83
+
  84
+    def list_clients(self):
  85
+        """ List all clients in client database """
  86
+        return set([c.hostname for c in MetadataClientModel.objects.all()])
  87
+
  88
+    def remove_group(self, group_name, attribs):
  89
+        msg = "DBMetadata does not support removing groups"
  90
+        self.logger.error(msg)
  91
+        raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
  92
+
  93
+    def remove_bundle(self, bundle_name):
  94
+        msg = "DBMetadata does not support removing bundles"
  95
+        self.logger.error(msg)
  96
+        raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
  97
+
  98
+    def remove_client(self, client_name):
  99
+        """Remove a client"""
  100
+        try:
  101
+            client = MetadataClientModel.objects.get(hostname=client_name)
  102
+        except MetadataClientModel.DoesNotExist:
  103
+            msg = "Client %s does not exist" % client_name
  104
+            self.logger.warning(msg)
  105
+            raise MetadataConsistencyError(msg)
  106
+        client.delete()
  107
+        self.clients = self.list_clients()
  108
+
  109
+    def _set_profile(self, client, profile, addresspair):
  110
+        if client not in self.clients:
  111
+            # adding a new client
  112
+            self.add_client(client)
  113
+            if client not in self.clientgroups:
  114
+                self.clientgroups[client] = [profile]
  115
+        else:
  116
+            msg = "DBMetadata does not support asserting client profiles"
  117
+            self.logger.error(msg)
  118
+            raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
  119
+
  120
+    def _handle_clients_xml_event(self, event):
  121
+        # clients.xml is parsed and the options specified in it are
  122
+        # understood, but it does _not_ assert client existence.
  123
+        Metadata._handle_clients_xml_event(self, event)
  124
+        self.clients = self.list_clients()
  125
+
  126
+
  127
+class DBMetadataLint(MetadataLint):
  128
+    pass
638  src/lib/Bcfg2/Server/Plugins/Metadata.py
@@ -2,6 +2,7 @@
2 2
 This file stores persistent metadata for the Bcfg2 Configuration Repository.
3 3
 """
4 4
 
  5
+import re
5 6
 import copy
6 7
 import fcntl
7 8
 import lxml.etree
@@ -10,8 +11,9 @@
10 11
 import sys
11 12
 import time
12 13
 import Bcfg2.Server
13  
-import Bcfg2.Server.FileMonitor
  14
+import Bcfg2.Server.Lint
14 15
 import Bcfg2.Server.Plugin
  16
+import Bcfg2.Server.FileMonitor
15 17
 from Bcfg2.version import Bcfg2VersionInfo
16 18
 
17 19
 def locked(fd):
@@ -38,10 +40,10 @@ class MetadataRuntimeError(Exception):
38 40
 class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked):
39 41
     """Handles xml config files and all XInclude statements"""
40 42
     def __init__(self, metadata, watch_clients, basefile):
41  
-        # we tell XMLFileBacked _not_ to add a monitor for this
42  
-        # file, because the main Metadata plugin has already added
43  
-        # one.  then we immediately set should_monitor to the proper
44  
-        # value, so that XIinclude'd files get properly watched
  43
+        # we tell XMLFileBacked _not_ to add a monitor for this file,
  44
+        # because the main Metadata plugin has already added one.
  45
+        # then we immediately set should_monitor to the proper value,
  46
+        # so that XInclude'd files get properly watched
45 47
         fpath = os.path.join(metadata.data, basefile)
46 48
         Bcfg2.Server.Plugin.XMLFileBacked.__init__(self, fpath,
47 49
                                                    fam=metadata.core.fam,
@@ -210,7 +212,8 @@ def