Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
@stpierre stpierre authored
Showing with 1,737 additions and 605 deletions.
  1. +1 −1  doc/appendix/guides/authentication.txt
  2. +1 −1  doc/appendix/guides/nat_howto.txt
  3. +3 −3 doc/server/backends.txt
  4. +45 −0 doc/server/database.txt
  5. +1 −0  doc/server/index.txt
  6. +1 −1  doc/server/plugins/generators/tgenshi/clientsxml.txt
  7. +39 −0 doc/server/plugins/grouping/dbmetadata.txt
  8. +138 −48 doc/server/plugins/grouping/metadata.txt
  9. +2 −1  schemas/clients.xsd
  10. +33 −20 schemas/metadata.xsd
  11. +32 −2 src/lib/Bcfg2/Options.py
  12. +2 −15 src/lib/Bcfg2/Server/Admin/Bundle.py
  13. +10 −35 src/lib/Bcfg2/Server/Admin/Client.py
  14. +0 −63 src/lib/Bcfg2/Server/Admin/Group.py
  15. +2 −3 src/lib/Bcfg2/Server/Admin/Init.py
  16. +33 −0 src/lib/Bcfg2/Server/Admin/Syncdb.py
  17. +1 −0  src/lib/Bcfg2/Server/Admin/__init__.py
  18. +11 −4 src/lib/Bcfg2/Server/Core.py
  19. +21 −18 src/lib/Bcfg2/Server/Plugin.py
  20. +128 −0 src/lib/Bcfg2/Server/Plugins/DBMetadata.py
  21. +422 −216 src/lib/Bcfg2/Server/Plugins/Metadata.py
  22. +1 −10 src/lib/Bcfg2/Server/Plugins/Packages/Collection.py
  23. +1 −1  src/lib/Bcfg2/Server/Reports/settings.py
  24. +62 −0 src/lib/Bcfg2/Server/models.py
  25. +14 −0 src/lib/Bcfg2/manage.py
  26. +71 −0 src/lib/Bcfg2/settings.py
  27. +7 −6 src/sbin/bcfg2-info
  28. +407 −0 testsuite/Testlib/TestServer/TestPlugins/TestDBMetadata.py
  29. +248 −157 testsuite/Testlib/TestServer/TestPlugins/TestMetadata.py
View
2  doc/appendix/guides/authentication.txt
@@ -62,7 +62,7 @@ How Authentication Works
#. Next, the ip address is verified against the client record. If the
address doesn't match, then the client must be set to
- location=floating
+ floating='true'
#. Finally, the password is verified. If the client is set to secure
mode, the only its per-client password is accepted. If it is not set
View
2  doc/appendix/guides/nat_howto.txt
@@ -44,7 +44,7 @@ the Client entry in clients.xml will look something like this:
.. code-block:: xml
<Client profile="desktop" name="test1"
- uuid='9001ec29-1531-4b16-8198-a71bea093d0a' location='floating'/>
+ uuid='9001ec29-1531-4b16-8198-a71bea093d0a' floating='true'/>
Alternatively, the Client entry can be setup like this:
View
6 doc/server/backends.txt
@@ -2,9 +2,9 @@
.. _server-backends:
-========
-Backends
-========
+===============
+Server Backends
+===============
.. versionadded:: 1.3.0
View
45 doc/server/database.txt
@@ -0,0 +1,45 @@
+.. -*- mode: rst -*-
+
+.. _server-database:
+
+========================
+Global Database Settings
+========================
+
+.. versionadded:: 1.3.0
+
+Several Bcfg2 plugins, including
+:ref:`server-plugins-grouping-dbmetadata` and
+:ref:`server-plugins-probes-index`, can connect use a relational
+database to store data. They use the global database settings in
+``bcfg2.conf``, described in this document, to connect.
+
+.. note::
+
+ The :ref:`server-plugins-statistics-dbstats` plugin and the
+ :ref:`reports-dynamic` do *not* currently use the global database
+ settings. They use their own separate database configuration.
+
+Configuration Options
+=====================
+
+All of the following options should go in the ``[database]`` section
+of ``/etc/bcfg2.conf``.
+
++-------------+------------------------------------------------------------+-------------------------------+
+| Option name | Description | Default |
++=============+============================================================+===============================+
+| engine | The full name of the Django database backend to use. See | "django.db.backends.sqlite3" |
+| | https://docs.djangoproject.com/en/dev/ref/settings/#engine | |
+| | for available options | |
++-------------+------------------------------------------------------------+-------------------------------+
+| name | The name of the database | "/var/lib/bcfg2/bcfg2.sqlite" |
++-------------+------------------------------------------------------------+-------------------------------+
+| user | The user to connect to the database as | None |
++-------------+------------------------------------------------------------+-------------------------------+
+| password | The password to connect to the database with | None |
++-------------+------------------------------------------------------------+-------------------------------+
+| host | The host to connect to | "localhost" |
++-------------+------------------------------------------------------------+-------------------------------+
+| port | The port to connect to | None |
++-------------+------------------------------------------------------------+-------------------------------+
View
1  doc/server/index.txt
@@ -30,3 +30,4 @@ clients.
bcfg2-info
selinux
backends
+ database
View
2  doc/server/plugins/generators/tgenshi/clientsxml.txt
@@ -65,7 +65,7 @@ Possible improvements:
name="${name}"
uuid="${name}"
password="${metadata.Properties['passwords.xml'].xdata.find('password').find('bcfg2-client').find(name).text}"
- location="floating"
+ floating="true"
secure="true"
/>\
{% end %}\
View
39 doc/server/plugins/grouping/dbmetadata.txt
@@ -0,0 +1,39 @@
+.. -*- mode: rst -*-
+
+.. _server-plugins-grouping-dbmetadata:
+
+==========
+DBMetadata
+==========
+
+.. versionadded:: 1.3.0
+
+The DBMetadata plugin is an alternative to the
+:ref:`server-plugins-grouping-metadata` plugin that stores client
+records in a database rather than writing back to ``clients.xml``.
+This provides several advantages:
+
+* ``clients.xml`` will never be written by the server, removing an
+ area of contention between the user and server.
+* ``clients.xml`` can be removed entirely for many sites.
+* The Bcfg2 client list can be queried by other machines without
+ obtaining and parsing ``clients.xml``.
+* A single client list can be shared amongst multiple Bcfg2 servers.
+
+In general, DBMetadata works almost the same as Metadata.
+``groups.xml`` is parsed identically. If ``clients.xml`` is present,
+it is parsed, but ``<Client>`` tags in ``clients.xml`` *do not* assert
+client existence; they are only used to set client options *if* the
+client exists (in the database). That is, the two purposes of
+``clients.xml`` -- to track which clients exist, and to set client
+options -- have been separated.
+
+With the improvements in ``groups.xml`` parsing in 1.3, client groups
+can now be set directly in ``groups.xml`` with ``<Client>`` tags. (See
+:ref:`metadata-client-tag` for more details.) As a result,
+``clients.xml`` is only necessary with DBMetadata if you need to set
+options (e.g., aliases, floating clients, per-client passwords, etc.)
+on clients.
+
+DBMetadata uses the :ref:`Global Server Database Settings
+<server-database>` to connect to its database.
View
186 doc/server/plugins/grouping/metadata.txt
@@ -6,11 +6,11 @@
Metadata
========
-The metadata mechanism has two types of information, client metadata and
-group metadata. The client metadata describes which top level group a
-client is associated with.The group metadata describes groups in terms
-of what bundles and other groups they include. Each aspect grouping
-and clients' memberships are reflected in the ``Metadata/groups.xml`` and
+The metadata mechanism has two types of information, client metadata
+and group metadata. The client metadata describes which top level
+group a client is associated with.The group metadata describes groups
+in terms of what bundles and other groups they include. Group data and
+clients' memberships are reflected in the ``Metadata/groups.xml`` and
``Metadata/clients.xml`` files, respectively.
Usage of Groups in Metadata
@@ -85,9 +85,9 @@ Additionally, the following properties can be specified:
| address | Establishes an extra IP address that | ip address |
| | resolves to this client. | |
+----------+----------------------------------------+----------------+
-| location | Requires requests to come from an IP | fixed|floating |
-| | address that matches the client | |
-| | record. | |
+| floating | Allows requests to come from any IP, | true|false |
+| | rather than requiring requests to come | |
+| | from an IP associated with the client | |
+----------+----------------------------------------+----------------+
| password | Establishes a per-node password that | String |
| | can be used instead of the global | |
@@ -101,6 +101,9 @@ Additionally, the following properties can be specified:
| | resolution. | |
+----------+----------------------------------------+----------------+
+Floating can also be configured by setting ``location="floating"``,
+but that is deprecated.
+
For detailed information on client authentication see
:ref:`appendix-guides-authentication`
@@ -112,31 +115,88 @@ definitions. Here's a simple ``Metadata/groups.xml`` file:
.. code-block:: xml
- <Groups version='3.0'>
+ <Groups>
<Group name='mail-server' profile='true'
- public='false'
comment='Top level mail server group' >
<Bundle name='mail-server'/>
<Bundle name='mailman-server'/>
<Group name='apache-server'/>
- <Group name='rhel-as-5-x86'/>
<Group name='nfs-client'/>
<Group name='server'/>
+ <Group name='rhel5'>
+ <Group name='sendmail-server'/>
+ </Group>
+ <Group name='rhel6'>
+ <Group name='postfix-server'/>
+ </Group>
+ </Group>
+ <Group name='rhel'>
+ <Group name='selinux-enabled'/>
</Group>
- <Group name='rhel-as-5-x86'>
- <Group name='rhel'/>
+ <Group name='oracle-server'>
+ <Group name='selinux-enabled' negate='true'/>
</Group>
- <Group name='apache-server'/>
- <Group name='nfs-client'/>
- <Group name='server'/>
- <Group name='rhel'/>
+ <Client name='foo.eample.com'>
+ <Group name='oracle-server'/>
+ <Group name='apache-server'/>
+ </Client>
</Groups>
+A Group or Client tag that does not contain any child tags is a
+declaration of membership; a Group or Client tag that does contain
+children is a conditional. So the example above does not assign
+either the ``rhel5`` or ``rhel6`` groups to machines in the
+``mail-server`` group, but conditionally assigns the
+``sendmail-server`` or ``postfix-server`` groups depending on the OS
+of the client. (Presumably in this example the OS groups are set by a
+probe.)
+
+Consequently, a client that is RHEL 5 and a member of the
+``mail-server`` profile group would also be a member of the
+``apache-server``, ``nfs-client``, ``server``, and ``sendmail-server``
+groups; a RHEL 6 client that is a member of the ``mail-server``
+profile group would be a member of the ``apache-server``,
+``nfs-client``, ``server``, and ``postfix-server`` groups.
+
+Client tags in ``groups.xml`` allow you to supplement the profile
+group declarations in ``clients.xml`` and/or client group assignments
+with the :ref:`server-plugins-grouping-grouppatterns` plugin. They
+should be used sparingly. (They are more useful with the
+:ref:`server-plugins-grouping-dbmetadata` plugin.)
+
+You can also declare that a group should be negated; this allows you
+to set defaults and override them efficiently. Negation is applied
+after other group memberships are calculated, so it doesn't matter how
+many times a client is assigned to a group or how many times it is
+negated; a single group negation is sufficient to remove a client from
+that group. For instance, in the following example,
+``foo.example.com`` is **not** a member of ``selinux-enabled``, even
+though it is a member of the ``foo-server`` and ``every-server``
+groups:
+
+.. code-block:: xml
+
+ <Groups>
+ <Group name="foo-server">
+ <Group name="apache-server"/>
+ <Group name="selinux-enabled"/>
+ </Group>
+ <Group name="apache-server">
+ <Group name="selinux-enabled"/>
+ </Group>
+ <Group name="every-server">
+ <Group name="selinux-enabled"/>
+ </Group>
+ <Client name="foo.example.com">
+ <Group name="selinux-enabled" negate="true"/>
+ </Client>
+
+.. note::
-Nested/chained groups definitions are conjunctive (logical and). For
-instance, in the above example, a client associated with the Profile
-Group ``mail-server`` is also a member of the ``apache-server``,
-``rhel-as-5-x86``, ``nfs-client``, ``server``, and ``rhel`` groups.
+ Nested Group conditionals, Client tags, and negated Group tags are
+ all new in 1.3.0.
+
+Order of ``groups.xml`` does not matter.
Groups describe clients in terms for abstract, disjoint aspects. Groups
can be combined to form complex descriptions of clients that use
@@ -165,33 +225,63 @@ Metadata Group Tag
The Group Tag has the following possible attributes:
-+----------+------------------------------------------+--------------+
-| Name | Description | Values |
-+==========+==========================================+==============+
-| name | Name of the group | String |
-+----------+------------------------------------------+--------------+
-| profile | If a client can be directly associated | True|False |
-| | with this group | |
-+----------+------------------------------------------+--------------+
-| public | If a client can freely associate itself | True|False |
-| | with this group. For use with the | |
-| | *bcfg2 -p* option on the client. | |
-+----------+------------------------------------------+--------------+
-| category | A group can only contain one instance of | String |
-| | a group in any one category. This | |
-| | provides the basis for representing | |
-| | groups which are conjugates of one | |
-| | another in a rigorous way. It also | |
-| | provides the basis for negation. | |
-+----------+------------------------------------------+--------------+
-| default | Set as the profile to use for clients | True|False |
-| | that are not associated with a profile | |
-| | in ``clients.xml`` | |
-+----------+------------------------------------------+--------------+
-| comment | English text description of group | String |
-+----------+------------------------------------------+--------------+
-
-Groups can also contain other groups and bundles.
++----------+----------------------------------------------+--------------+
+| Name | Description | Values |
++==========+==============================================+==============+
+| name | Name of the group | String |
++----------+----------------------------------------------+--------------+
+| profile | If a client can be directly associated with | True|False |
+| | this group | |
++----------+----------------------------------------------+--------------+
+| public | If a client can freely associate itself with | True|False |
+| | this group. For use with the ``bcfg2 -p`` | |
+| | option on the client. | |
++----------+----------------------------------------------+--------------+
+| category | A group can only contain one instance of a | String |
+| | group in any one category. This provides the | |
+| | basis for representing groups which are | |
+| | conjugates of one another in a rigorous way. | |
+| | way. |
++----------+----------------------------------------------+--------------+
+| default | Set as the profile to use for clients that | True|False |
+| | are not associated with a profile in | |
+| | ``clients.xml`` | |
++----------+----------------------------------------------+--------------+
+| comment | English text description of group | String |
++----------+----------------------------------------------+--------------+
+| negate | When used as a conditional, only apply the | True|False |
+| | children if the named group does not match. | |
+| | When used as a declaration, do not apply | |
+| | the named group to matching clients. | |
++----------+----------------------------------------------+--------------+
+
+The ``profile``, ``public``, ``category``, ``default``, and
+``comment`` attributes are only parsed if a Group tag either a) is the
+direct child of a Groups tag (i.e., at the top level of an XML file);
+or b) has no children. This matches legacy behavior in Bcfg2 1.2 and
+earlier.
+
+Groups can also contain other groups, clients, and bundles.
+
+.. _metadata-client-tag:
+
+Metadata Client Tag
+-------------------
+
+The Client Tag has the following possible attributes:
+
++----------+-----------------------------------------------+--------------+
+| Name | Description | Values |
++==========+===============================================+==============+
+| name | Name of the client | String |
++----------+-----------------------------------------------+--------------+
+| negate | Only apply the child tags if the named client | True|False |
+| | does not match. | |
++----------+-----------------------------------------------+--------------+
+
+Clients can also contain groups, other clients (although that's likely
+nonsensical), and bundles.
+
Use of XInclude
===============
View
3  schemas/clients.xsd
@@ -26,7 +26,8 @@
<xsd:attribute type='xsd:string' name='uuid'/>
<xsd:attribute type='xsd:string' name='password'/>
<xsd:attribute type='xsd:string' name='location'/>
- <xsd:attribute type='xsd:string' name='secure'/>
+ <xsd:attribute type='xsd:boolean' name='floating'/>
+ <xsd:attribute type='xsd:boolean' name='secure'/>
<xsd:attribute type='xsd:string' name='pingtime' use='optional'/>
<xsd:attribute type='xsd:string' name='address'/>
<xsd:attribute type='xsd:string' name='version'/>
View
53 schemas/metadata.xsd
@@ -1,6 +1,6 @@
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xi="http://www.w3.org/2001/XInclude" xml:lang="en">
-
+
<xsd:annotation>
<xsd:documentation>
metadata schema for bcfg2
@@ -13,38 +13,51 @@
<xsd:import namespace="http://www.w3.org/2001/XInclude"
schemaLocation="xinclude.xsd"/>
+ <xsd:complexType name='bundleDeclaration'>
+ <xsd:attribute type='xsd:string' name='name' use='required'/>
+ </xsd:complexType>
+
<xsd:complexType name='groupType'>
<xsd:choice minOccurs='0' maxOccurs='unbounded'>
- <xsd:element name='Bundle'>
- <xsd:complexType>
- <xsd:attribute type='xsd:string' name='name' use='required'/>
- </xsd:complexType>
- </xsd:element>
- <xsd:element name='Group' >
- <xsd:complexType>
- <xsd:attribute name='name' use='required'/>
- </xsd:complexType>
- </xsd:element>
+ <xsd:element name='Bundle' type='bundleDeclaration'/>
+ <xsd:element name='Group' type='groupType'/>
+ <xsd:element name='Client' type='clientType'/>
+ <xsd:element name='Groups' type='groupsType'/>
+ <xsd:element name='Options' type='optionsType'/>
+ </xsd:choice>
+ <xsd:attribute type='xsd:string' name='name' use='required'/>
+ <xsd:attribute type='xsd:boolean' name='profile'/>
+ <xsd:attribute type='xsd:boolean' name='public'/>
+ <xsd:attribute type='xsd:boolean' name='default'/>
+ <xsd:attribute type='xsd:string' name='auth'/>
+ <xsd:attribute type='xsd:string' name='category'/>
+ <xsd:attribute type='xsd:string' name='comment'/>
+ <xsd:attribute type='xsd:string' name='negate'/>
+ </xsd:complexType>
+
+ <xsd:complexType name='clientType'>
+ <xsd:choice minOccurs='0' maxOccurs='unbounded'>
+ <xsd:element name='Bundle' type='bundleDeclaration'/>
+ <xsd:element name='Group' type='groupType'/>
+ <xsd:element name='Client' type='clientType'/>
+ <xsd:element name='Groups' type='groupsType'/>
+ <xsd:element name='Options' type='optionsType'/>
</xsd:choice>
- <xsd:attribute type='xsd:boolean' name='profile' use='optional'/>
- <xsd:attribute type='xsd:boolean' name='public' use='optional'/>
- <xsd:attribute type='xsd:boolean' name='default' use='optional'/>
<xsd:attribute type='xsd:string' name='name' use='required'/>
- <xsd:attribute type='xsd:string' name='auth' use='optional'/>
- <xsd:attribute type='xsd:string' name='category' use='optional'/>
- <xsd:attribute type='xsd:string' name='comment' use='optional'/>
+ <xsd:attribute type='xsd:string' name='negate'/>
</xsd:complexType>
<xsd:complexType name='groupsType'>
<xsd:choice minOccurs='0' maxOccurs='unbounded'>
<xsd:element name='Group' type='groupType'/>
+ <xsd:element name='Client' type='clientType'/>
<xsd:element name='Groups' type='groupsType'/>
<xsd:element ref="xi:include"/>
</xsd:choice>
<xsd:attribute name='version' type='xsd:string'/>
- <xsd:attribute name='origin' type='xsd:string'/>
- <xsd:attribute name='revision' type='xsd:string'/>
- <xsd:attribute ref='xml:base'/>
+ <xsd:attribute name='origin' type='xsd:string'/>
+ <xsd:attribute name='revision' type='xsd:string'/>
+ <xsd:attribute ref='xml:base'/>
</xsd:complexType>
<xsd:element name='Groups' type='groupsType'/>
View
34 src/lib/Bcfg2/Options.py
@@ -224,6 +224,7 @@ def get_bool(s):
return False
else:
raise ValueError
+
"""
Options:
@@ -424,6 +425,32 @@ def get_bool(s):
default='best',
cf=('server', 'backend'))
+# database options
+DB_ENGINE = \
+ Option('Database engine',
+ default='django.db.backends.sqlite3',
+ cf=('database', 'engine'))
+DB_NAME = \
+ Option('Database name',
+ default=os.path.join(SERVER_REPOSITORY.default, "bcfg2.sqlite"),
+ cf=('database', 'name'))
+DB_USER = \
+ Option('Database username',
+ default=None,
+ cf=('database', 'user'))
+DB_PASSWORD = \
+ Option('Database password',
+ default=None,
+ cf=('database', 'password'))
+DB_HOST = \
+ Option('Database host',
+ default='localhost',
+ cf=('database', 'host'))
+DB_PORT = \
+ Option('Database port',
+ default='',
+ cf=('database', 'port'),)
+
# Client options
CLIENT_KEY = \
Option('Path to SSL key',
@@ -898,12 +925,15 @@ class OptionParser(OptionSet):
OptionParser bootstraps option parsing,
getting the value of the config file
"""
- def __init__(self, args, argv=None):
+ def __init__(self, args, argv=None, quiet=False):
if argv is None:
argv = sys.argv[1:]
+ # the bootstrap is always quiet, since it's running with a
+ # default config file and so might produce warnings otherwise
self.Bootstrap = OptionSet([('configfile', CFILE)], quiet=True)
self.Bootstrap.parse(argv, do_getopt=False)
- OptionSet.__init__(self, args, configfile=self.Bootstrap['configfile'])
+ OptionSet.__init__(self, args, configfile=self.Bootstrap['configfile'],
+ quiet=quiet)
self.optinfo = copy.copy(args)
def HandleEvent(self, event):
View
17 src/lib/Bcfg2/Server/Admin/Bundle.py
@@ -8,12 +8,11 @@
class Bundle(Bcfg2.Server.Admin.MetadataCore):
- __shorthelp__ = "Create or delete bundle entries"
- # TODO: add/del functions
+ __shorthelp__ = "List and view bundle entries"
__longhelp__ = (__shorthelp__ + "\n\nbcfg2-admin bundle list-xml"
"\nbcfg2-admin bundle list-genshi"
"\nbcfg2-admin bundle show\n")
- __usage__ = ("bcfg2-admin bundle [options] [add|del] [group]")
+ __usage__ = ("bcfg2-admin bundle [options] [list-xml|list-genshi|show]")
def __call__(self, args):
Bcfg2.Server.Admin.MetadataCore.__call__(self, args)
@@ -28,18 +27,6 @@ def __call__(self, args):
if len(args) == 0:
self.errExit("No argument specified.\n"
"Please see bcfg2-admin bundle help for usage.")
-# if args[0] == 'add':
-# try:
-# self.metadata.add_bundle(args[1])
-# except MetadataConsistencyError:
-# print("Error in adding bundle.")
-# raise SystemExit(1)
-# elif args[0] in ['delete', 'remove', 'del', 'rm']:
-# try:
-# self.metadata.remove_bundle(args[1])
-# except MetadataConsistencyError:
-# print("Error in deleting bundle.")
-# raise SystemExit(1)
# Lists all available xml bundles
elif args[0] in ['list-xml', 'ls-xml']:
bundle_name = []
View
45 src/lib/Bcfg2/Server/Admin/Client.py
@@ -4,50 +4,23 @@
class Client(Bcfg2.Server.Admin.MetadataCore):
- __shorthelp__ = "Create, delete, or modify client entries"
+ __shorthelp__ = "Create, delete, or list client entries"
__longhelp__ = (__shorthelp__ + "\n\nbcfg2-admin client add <client> "
- "attr1=val1 attr2=val2"
- "\nbcfg2-admin client update <client> "
- "attr1=val1 attr2=val2"
"\nbcfg2-admin client list"
"\nbcfg2-admin client del <client>\n")
- __usage__ = ("bcfg2-admin client [options] [add|del|update|list] [attr=val]")
+ __usage__ = ("bcfg2-admin client [options] [add|del|list] [attr=val]")
def __call__(self, args):
Bcfg2.Server.Admin.MetadataCore.__call__(self, args)
if len(args) == 0:
self.errExit("No argument specified.\n"
- "Please see bcfg2-admin client help for usage.")
+ "Usage: %s" % self.usage)
if args[0] == 'add':
- attr_d = {}
- for i in args[2:]:
- attr, val = i.split('=', 1)
- if attr not in ['profile', 'uuid', 'password',
- 'location', 'secure', 'address',
- 'auth']:
- print("Attribute %s unknown" % attr)
- raise SystemExit(1)
- attr_d[attr] = val
try:
- self.metadata.add_client(args[1], attr_d)
+ self.metadata.add_client(args[1])
except MetadataConsistencyError:
print("Error in adding client")
raise SystemExit(1)
- elif args[0] in ['update', 'up']:
- attr_d = {}
- for i in args[2:]:
- attr, val = i.split('=', 1)
- if attr not in ['profile', 'uuid', 'password',
- 'location', 'secure', 'address',
- 'auth']:
- print("Attribute %s unknown" % attr)
- raise SystemExit(1)
- attr_d[attr] = val
- try:
- self.metadata.update_client(args[1], attr_d)
- except MetadataConsistencyError:
- print("Error in updating client")
- raise SystemExit(1)
elif args[0] in ['delete', 'remove', 'del', 'rm']:
try:
self.metadata.remove_client(args[1])
@@ -55,7 +28,9 @@ def __call__(self, args):
print("Error in deleting client")
raise SystemExit(1)
elif args[0] in ['list', 'ls']:
- tree = lxml.etree.parse(self.metadata.data + "/clients.xml")
- tree.xinclude()
- for node in tree.findall("//Client"):
- print(node.attrib["name"])
+ for client in self.metadata.list_clients():
+ print(client.hostname)
+ else:
+ print("No command specified")
+ raise SystemExit(1)
+
View
63 src/lib/Bcfg2/Server/Admin/Group.py
@@ -1,63 +0,0 @@
-import lxml.etree
-import Bcfg2.Server.Admin
-from Bcfg2.Server.Plugins.Metadata import MetadataConsistencyError
-
-
-class Group(Bcfg2.Server.Admin.MetadataCore):
- __shorthelp__ = "Create, delete, or modify group entries"
- __longhelp__ = (__shorthelp__ + "\n\nbcfg2-admin group add <group> "
- "attr1=val1 attr2=val2"
- "\nbcfg2-admin group update <group> "
- "attr1=val1 attr2=val2"
- "\nbcfg2-admin group list"
- "\nbcfg2-admin group del <group>\n")
- __usage__ = ("bcfg2-admin group [options] [add|del|update|list] [attr=val]")
-
- def __call__(self, args):
- Bcfg2.Server.Admin.MetadataCore.__call__(self, args)
- if len(args) == 0:
- self.errExit("No argument specified.\n"
- "Please see bcfg2-admin group help for usage.")
- if args[0] == 'add':
- attr_d = {}
- for i in args[2:]:
- attr, val = i.split('=', 1)
- if attr not in ['profile', 'public', 'default',
- 'name', 'auth', 'toolset', 'category',
- 'comment']:
- print("Attribute %s unknown" % attr)
- raise SystemExit(1)
- attr_d[attr] = val
- try:
- self.metadata.add_group(args[1], attr_d)
- except MetadataConsistencyError:
- print("Error in adding group")
- raise SystemExit(1)
- elif args[0] in ['update', 'up']:
- attr_d = {}
- for i in args[2:]:
- attr, val = i.split('=', 1)
- if attr not in ['profile', 'public', 'default',
- 'name', 'auth', 'toolset', 'category',
- 'comment']:
- print("Attribute %s unknown" % attr)
- raise SystemExit(1)
- attr_d[attr] = val
- try:
- self.metadata.update_group(args[1], attr_d)
- except MetadataConsistencyError:
- print("Error in updating group")
- raise SystemExit(1)
- elif args[0] in ['delete', 'remove', 'del', 'rm']:
- try:
- self.metadata.remove_group(args[1])
- except MetadataConsistencyError:
- print("Error in deleting group")
- raise SystemExit(1)
- elif args[0] in ['list', 'ls']:
- tree = lxml.etree.parse(self.metadata.data + "/groups.xml")
- for node in tree.findall("//Group"):
- print(node.attrib["name"])
- else:
- print("No command specified")
- raise SystemExit(1)
View
5 src/lib/Bcfg2/Server/Admin/Init.py
@@ -308,9 +308,8 @@ def _init_plugins(self):
for plugin in self.plugins:
if plugin == 'Metadata':
Bcfg2.Server.Plugins.Metadata.Metadata.init_repo(self.repopath,
- groups,
- self.os_sel,
- clients)
+ groups_xml=groups % self.os_sel,
+ clients_xml=clients)
else:
try:
module = __import__("Bcfg2.Server.Plugins.%s" % plugin, '',
View
33 src/lib/Bcfg2/Server/Admin/Syncdb.py
@@ -0,0 +1,33 @@
+import Bcfg2.settings
+import Bcfg2.Options
+import Bcfg2.Server.Admin
+from django.core.management import setup_environ
+
+class Syncdb(Bcfg2.Server.Admin.Mode):
+ __shorthelp__ = ("Sync the Django ORM with the configured database")
+ __longhelp__ = __shorthelp__ + "\n\nbcfg2-admin syncdb"
+ __usage__ = "bcfg2-admin syncdb"
+ options = {'configfile': Bcfg2.Options.CFILE,
+ 'repo': Bcfg2.Options.SERVER_REPOSITORY}
+
+ def __call__(self, args):
+ Bcfg2.Server.Admin.Mode.__call__(self, args)
+
+ # Parse options
+ self.opts = Bcfg2.Options.OptionParser(self.options)
+ self.opts.parse(args)
+
+ # we have to set up the django environment before we import
+ # the syncdb command, but we have to wait to set up the
+ # environment until we've read the config, which has to wait
+ # until we've parsed options. it's a windy, twisting road.
+ Bcfg2.settings.read_config(cfile=self.opts['configfile'],
+ repo=self.opts['repo'])
+ setup_environ(Bcfg2.settings)
+ import Bcfg2.Server.models
+ Bcfg2.Server.models.load_models(cfile=self.opts['configfile'])
+
+ from django.core.management.commands import syncdb
+
+ cmd = syncdb.Command()
+ cmd.handle_noargs(interactive=False)
View
1  src/lib/Bcfg2/Server/Admin/__init__.py
@@ -11,6 +11,7 @@
'Query',
'Reports',
'Snapshots',
+ 'Syncdb',
'Tidy',
'Viz',
'Xcmd'
View
15 src/lib/Bcfg2/Server/Core.py
@@ -1,5 +1,6 @@
"""Bcfg2.Server.Core provides the runtime support for Bcfg2 modules."""
+import os
import atexit
import logging
import select
@@ -9,6 +10,11 @@
import inspect
import lxml.etree
from traceback import format_exc
+
+# this must be set before we import the Metadata plugin
+os.environ['DJANGO_SETTINGS_MODULE'] = 'Bcfg2.settings'
+
+import Bcfg2.settings
import Bcfg2.Server
import Bcfg2.Logger
import Bcfg2.Server.FileMonitor
@@ -95,6 +101,10 @@ def __init__(self, setup, start_fam_thread=False):
# Create an event to signal worker threads to shutdown
self.terminate = threading.Event()
+ # generate Django ORM settings. this must be done _before_ we
+ # load plugins
+ Bcfg2.settings.read_config(cfile=self.cfile, repo=self.datastore)
+
if '' in setup['plugins']:
setup['plugins'].remove('')
@@ -195,8 +205,7 @@ def init_plugins(self, plugin):
try:
self.plugins[plugin] = plug(self, self.datastore)
except PluginInitError:
- self.logger.error("Failed to instantiate plugin %s" % plugin,
- exc_info=1)
+ logger.error("Failed to instantiate plugin %s" % plugin, exc_info=1)
except:
self.logger.error("Unexpected instantiation failure for plugin %s" %
plugin, exc_info=1)
@@ -526,8 +535,6 @@ def GetProbes(self, address):
def RecvProbeData(self, address, probedata):
"""Receive probe data from clients."""
client, metadata = self.resolve_client(address)
- # clear dynamic groups
- self.metadata.cgroups[metadata.hostname] = []
try:
xpdata = lxml.etree.XML(probedata.encode('utf-8'),
parser=Bcfg2.Server.XMLParser)
View
39 src/lib/Bcfg2/Server/Plugin.py
@@ -102,6 +102,16 @@ def debug_log(self, message, flag=None):
self.logger.error(message)
+class DatabaseBacked(object):
+ def __init__(self):
+ pass
+
+
+class PluginDatabaseModel(object):
+ class Meta:
+ app_label = "Server"
+
+
class Plugin(Debuggable):
"""This is the base class for all Bcfg2 Server plugins.
Several attributes must be defined in the subclass:
@@ -139,8 +149,7 @@ def __init__(self, core, datastore):
@classmethod
def init_repo(cls, repo):
- path = "%s/%s" % (repo, cls.name)
- os.makedirs(path)
+ os.makedirs(os.path.join(repo, cls.name))
def shutdown(self):
self.running = False
@@ -169,7 +178,7 @@ def BuildStructures(self, metadata):
class Metadata(object):
"""Signal metadata capabilities for this plugin"""
- def add_client(self, client_name, attribs):
+ def add_client(self, client_name):
"""Add client."""
pass
@@ -181,6 +190,9 @@ def viz(self, hosts, bundles, key, colors):
"""Create viz str for viz admin mode."""
pass
+ def _handle_default_event(self, event):
+ pass
+
def get_initial_metadata(self, client_name):
raise PluginExecutionError
@@ -650,7 +662,7 @@ def Index(self):
def add_monitor(self, fpath, fname):
self.extras.append(fname)
- if self.fam:
+ if self.fam and self.should_monitor:
self.fam.AddMonitor(fpath, self)
def __iter__(self):
@@ -666,22 +678,13 @@ class StructFile(XMLFileBacked):
def _include_element(self, item, metadata):
""" determine if an XML element matches the metadata """
+ negate = item.get('negate', 'false').lower() == 'true'
if item.tag == 'Group':
- if ((item.get('negate', 'false').lower() == 'true' and
- item.get('name') not in metadata.groups) or
- (item.get('negate', 'false').lower() == 'false' and
- item.get('name') in metadata.groups)):
- return True
- else:
- return False
+ return ((negate and item.get('name') not in metadata.groups) or
+ (not negate and item.get('name') in metadata.groups))
elif item.tag == 'Client':
- if ((item.get('negate', 'false').lower() == 'true' and
- item.get('name') != metadata.hostname) or
- (item.get('negate', 'false').lower() == 'false' and
- item.get('name') == metadata.hostname)):
- return True
- else:
- return False
+ return ((negate and item.get('name') != metadata.hostname) or
+ (not negate and item.get('name') == metadata.hostname))
elif isinstance(item, lxml.etree._Comment):
return False
else:
View
128 src/lib/Bcfg2/Server/Plugins/DBMetadata.py
@@ -0,0 +1,128 @@
+import os
+import sys
+from UserDict import DictMixin
+from django.db import models
+import Bcfg2.Server.Lint
+import Bcfg2.Server.Plugin
+from Bcfg2.Server.Plugins.Metadata import *
+
+class MetadataClientModel(models.Model,
+ Bcfg2.Server.Plugin.PluginDatabaseModel):
+ hostname = models.CharField(max_length=255, primary_key=True)
+ version = models.CharField(max_length=31, null=True)
+
+
+class ClientVersions(DictMixin):
+ def __getitem__(self, key):
+ try:
+ return MetadataClientModel.objects.get(hostname=key).version
+ except MetadataClientModel.DoesNotExist:
+ raise KeyError(key)
+
+ def __setitem__(self, key, value):
+ client = MetadataClientModel.objects.get_or_create(hostname=key)[0]
+ client.version = value
+ client.save()
+
+ def keys(self):
+ return [c.hostname for c in MetadataClientModel.objects.all()]
+
+ def __contains__(self, key):
+ try:
+ client = MetadataClientModel.objects.get(hostname=key)
+ return True
+ except MetadataClientModel.DoesNotExist:
+ return False
+
+
+class DBMetadata(Metadata, Bcfg2.Server.Plugin.DatabaseBacked):
+ __files__ = ["groups.xml"]
+ experimental = True
+ conflicts = ['Metadata']
+
+ def __init__(self, core, datastore, watch_clients=True):
+ Metadata.__init__(self, core, datastore, watch_clients=watch_clients)
+ Bcfg2.Server.Plugin.DatabaseBacked.__init__(self)
+ if os.path.exists(os.path.join(self.data, "clients.xml")):
+ self.logger.warning("DBMetadata: clients.xml found, parsing in "
+ "compatibility mode")
+ self._handle_file("clients.xml")
+ self.versions = ClientVersions()
+
+ def add_group(self, group_name, attribs):
+ msg = "DBMetadata does not support adding groups"
+ self.logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+
+ def add_bundle(self, bundle_name):
+ msg = "DBMetadata does not support adding bundles"
+ self.logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+
+ def add_client(self, client_name):
+ """Add client to clients database."""
+ client = MetadataClientModel(hostname=client_name)
+ client.save()
+ self.clients = self.list_clients()
+ return client
+
+ def update_group(self, group_name, attribs):
+ msg = "DBMetadata does not support updating groups"
+ self.logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+
+ def update_bundle(self, bundle_name):
+ msg = "DBMetadata does not support updating bundles"
+ self.logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+
+ def update_client(self, client_name, attribs):
+ msg = "DBMetadata does not support updating clients"
+ self.logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+
+ def list_clients(self):
+ """ List all clients in client database """
+ return set([c.hostname for c in MetadataClientModel.objects.all()])
+
+ def remove_group(self, group_name, attribs):
+ msg = "DBMetadata does not support removing groups"
+ self.logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+
+ def remove_bundle(self, bundle_name):
+ msg = "DBMetadata does not support removing bundles"
+ self.logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+
+ def remove_client(self, client_name):
+ """Remove a client"""
+ try:
+ client = MetadataClientModel.objects.get(hostname=client_name)
+ except MetadataClientModel.DoesNotExist:
+ msg = "Client %s does not exist" % client_name
+ self.logger.warning(msg)
+ raise MetadataConsistencyError(msg)
+ client.delete()
+ self.clients = self.list_clients()
+
+ def _set_profile(self, client, profile, addresspair):
+ if client not in self.clients:
+ # adding a new client
+ self.add_client(client)
+ if client not in self.clientgroups:
+ self.clientgroups[client] = [profile]
+ else:
+ msg = "DBMetadata does not support asserting client profiles"
+ self.logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+
+ def _handle_clients_xml_event(self, event):
+ # clients.xml is parsed and the options specified in it are
+ # understood, but it does _not_ assert client existence.
+ Metadata._handle_clients_xml_event(self, event)
+ self.clients = self.list_clients()
+
+
+class DBMetadataLint(MetadataLint):
+ pass
View
638 src/lib/Bcfg2/Server/Plugins/Metadata.py
@@ -2,6 +2,7 @@
This file stores persistent metadata for the Bcfg2 Configuration Repository.
"""
+import re
import copy
import fcntl
import lxml.etree
@@ -10,8 +11,9 @@
import sys
import time
import Bcfg2.Server
-import Bcfg2.Server.FileMonitor
+import Bcfg2.Server.Lint
import Bcfg2.Server.Plugin
+import Bcfg2.Server.FileMonitor
from Bcfg2.version import Bcfg2VersionInfo
def locked(fd):
@@ -38,10 +40,10 @@ class MetadataRuntimeError(Exception):
class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked):
"""Handles xml config files and all XInclude statements"""
def __init__(self, metadata, watch_clients, basefile):
- # we tell XMLFileBacked _not_ to add a monitor for this
- # file, because the main Metadata plugin has already added
- # one. then we immediately set should_monitor to the proper
- # value, so that XIinclude'd files get properly watched
+ # we tell XMLFileBacked _not_ to add a monitor for this file,
+ # because the main Metadata plugin has already added one.
+ # then we immediately set should_monitor to the proper value,
+ # so that XInclude'd files get properly watched
fpath = os.path.join(metadata.data, basefile)
Bcfg2.Server.Plugin.XMLFileBacked.__init__(self, fpath,
fam=metadata.core.fam,
@@ -210,7 +212,8 @@ def group_in_category(self, category):
class MetadataQuery(object):
- def __init__(self, by_name, get_clients, by_groups, by_profiles, all_groups, all_groups_in_category):
+ def __init__(self, by_name, get_clients, by_groups, by_profiles,
+ all_groups, all_groups_in_category):
# resolver is set later
self.by_name = by_name
self.names_by_groups = by_groups
@@ -229,6 +232,36 @@ def all(self):
return [self.by_name(name) for name in self.all_clients()]
+class MetadataGroup(tuple):
+ def __new__(cls, name, bundles=None, category=None,
+ is_profile=False, is_public=False, is_private=False):
+ if bundles is None:
+ bundles = set()
+ return tuple.__new__(cls, (bundles, category))
+
+ def __init__(self, name, bundles=None, category=None,
+ is_profile=False, is_public=False, is_private=False):
+ if bundles is None:
+ bundles = set()
+ tuple.__init__(self)
+ self.name = name
+ self.bundles = bundles
+ self.category = category
+ self.is_profile = is_profile
+ self.is_public = is_public
+ self.is_private = is_private
+
+ def __str__(self):
+ return repr(self)
+
+ def __repr__(self):
+ return "%s %s (bundles=%s, category=%s)" % \
+ (self.__class__.__name__, self.name, self.bundles,
+ self.category)
+
+ def __hash__(self):
+ return hash(self.name)
+
class Metadata(Bcfg2.Server.Plugin.Plugin,
Bcfg2.Server.Plugin.Metadata,
Bcfg2.Server.Plugin.Statistics):
@@ -236,69 +269,80 @@ class Metadata(Bcfg2.Server.Plugin.Plugin,
__author__ = 'bcfg-dev@mcs.anl.gov'
name = "Metadata"
sort_order = 500
+ __files__ = ["groups.xml", "clients.xml"]
def __init__(self, core, datastore, watch_clients=True):
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Metadata.__init__(self)
Bcfg2.Server.Plugin.Statistics.__init__(self)
+ self.watch_clients = watch_clients
self.states = dict()
- if watch_clients:
- for fname in ["groups.xml", "clients.xml"]:
- self.states[fname] = False
- try:
- core.fam.AddMonitor(os.path.join(self.data, fname), self)
- except:
- err = sys.exc_info()[1]
- msg = "Unable to add file monitor for %s: %s" % (fname, err)
- print(msg)
- raise Bcfg2.Server.Plugin.PluginInitError(msg)
-
- self.clients_xml = XMLMetadataConfig(self, watch_clients, 'clients.xml')
- self.groups_xml = XMLMetadataConfig(self, watch_clients, 'groups.xml')
- self.addresses = {}
+ self.extra = dict()
+ self.handlers = []
+ for fname in self.__files__:
+ self._handle_file(fname)
+
+ # mapping of clientname -> authtype
self.auth = dict()
- self.clients = {}
- self.aliases = {}
- self.groups = {}
- self.cgroups = {}
- self.versions = {}
- self.public = []
- self.private = []
- self.profiles = []
- self.categories = {}
- self.bad_clients = {}
- self.uuid = {}
+ # list of clients required to have non-global password
self.secure = []
+ # list of floating clients
self.floating = []
+ # mapping of clientname -> password
self.passwords = {}
+ self.addresses = {}
+ self.raddresses = {}
+ # mapping of clientname -> [groups]
+ self.clientgroups = {}
+ # list of clients
+ self.clients = []
+ self.aliases = {}
+ self.raliases = {}
+ # mapping of groupname -> MetadataGroup object
+ self.groups = {}
+ # mappings of predicate -> MetadataGroup object
+ self.group_membership = dict()
+ self.negated_groups = dict()
+ # mapping of hostname -> version string
+ self.versions = dict()
+ self.uuid = {}
self.session_cache = {}
self.default = None
self.pdirty = False
- self.extra = {'groups.xml': [],
- 'clients.xml': []}
self.password = core.password
self.query = MetadataQuery(core.build_metadata,
- lambda: list(self.clients.keys()),
+ lambda: list(self.clients),
self.get_client_names_by_groups,
self.get_client_names_by_profiles,
self.get_all_group_names,
self.get_all_groups_in_category)
@classmethod
- def init_repo(cls, repo, groups, os_selection, clients):
- path = os.path.join(repo, cls.name)
- os.makedirs(path)
- open(os.path.join(repo, "Metadata", "groups.xml"),
- "w").write(groups % os_selection)
- open(os.path.join(repo, "Metadata", "clients.xml"),
- "w").write(clients % socket.getfqdn())
-
- def get_groups(self):
- '''return groups xml tree'''
- groups_tree = lxml.etree.parse(os.path.join(self.data, "groups.xml"),
- parser=Bcfg2.Server.XMLParser)
- root = groups_tree.getroot()
- return root
+ def init_repo(cls, repo, **kwargs):
+ # must use super here; inheritance works funny with class methods
+ super(Metadata, cls).init_repo(repo)
+
+ for fname in cls.__files__:
+ aname = re.sub(r'[^A-z0-9_]', '_', fname)
+ if aname in kwargs:
+ open(os.path.join(repo, cls.name, fname),
+ "w").write(kwargs[aname])
+
+ def _handle_file(self, fname):
+ if self.watch_clients:
+ try:
+ self.core.fam.AddMonitor(os.path.join(self.data, fname), self)
+ except:
+ err = sys.exc_info()[1]
+ msg = "Unable to add file monitor for %s: %s" % (fname, err)
+ self.logger.error(msg)
+ raise Bcfg2.Server.Plugin.PluginInitError(msg)
+ self.states[fname] = False
+ aname = re.sub(r'[^A-z0-9_]', '_', fname)
+ xmlcfg = XMLMetadataConfig(self, self.watch_clients, fname)
+ setattr(self, aname, xmlcfg)
+ self.handlers.append(xmlcfg.HandleEvent)
+ self.extra[fname] = []
def _search_xdata(self, tag, name, tree, alias=False):
for node in tree.findall("//%s" % tag):
@@ -325,9 +369,8 @@ def search_client(self, client_name, tree):
def _add_xdata(self, config, tag, name, attribs=None, alias=False):
node = self._search_xdata(tag, name, config.xdata, alias=alias)
if node != None:
- msg = "%s \"%s\" already exists" % (tag, name)
- self.logger.error(msg)
- raise MetadataConsistencyError(msg)
+ self.logger.error("%s \"%s\" already exists" % (tag, name))
+ raise MetadataConsistencyError
element = lxml.etree.SubElement(config.base_xdata.getroot(),
tag, name=name)
if attribs:
@@ -352,15 +395,14 @@ def add_client(self, client_name, attribs):
def _update_xdata(self, config, tag, name, attribs, alias=False):
node = self._search_xdata(tag, name, config.xdata, alias=alias)
if node == None:
- msg = "%s \"%s\" does not exist" % (tag, name)
- self.logger.error(msg)
- raise MetadataConsistencyError(msg)
+ self.logger.error("%s \"%s\" does not exist" % (tag, name))
+ raise MetadataConsistencyError
xdict = config.find_xml_for_xpath('.//%s[@name="%s"]' %
(tag, node.get('name')))
if not xdict:
- msg = "Unexpected error finding %s \"%s\"" % (tag, name)
- self.logger.error(msg)
- raise MetadataConsistencyError(msg)
+ self.logger.error("Unexpected error finding %s \"%s\"" %
+ (tag, name))
+ raise MetadataConsistencyError
for key, val in list(attribs.items()):
xdict['xquery'][0].set(key, val)
config.write_xml(xdict['filename'], xdict['xmltree'])
@@ -377,17 +419,16 @@ def update_client(self, client_name, attribs):
def _remove_xdata(self, config, tag, name, alias=False):
node = self._search_xdata(tag, name, config.xdata)
if node == None:
- msg = "%s \"%s\" does not exist" % (tag, name)
- self.logger.error(msg)
- raise MetadataConsistencyError(msg)
+ self.logger.error("%s \"%s\" does not exist" % (tag, name))
+ raise MetadataConsistencyError
xdict = config.find_xml_for_xpath('.//%s[@name="%s"]' %
(tag, node.get('name')))
if not xdict:
- msg = "Unexpected error finding %s \"%s\"" % (tag, name)
- self.logger.error(msg)
- raise MetadataConsistencyError(msg)
+ self.logger.error("Unexpected error finding %s \"%s\"" %
+ (tag, name))
+ raise MetadataConsistencyError
xdict['xquery'][0].getparent().remove(xdict['xquery'][0])
- self.groups_xml.write_xml(xdict['filename'], xdict['xmltree'])
+ config.write_xml(xdict['filename'], xdict['xmltree'])
def remove_group(self, group_name):
"""Remove a group."""
@@ -397,12 +438,16 @@ def remove_bundle(self, bundle_name):
"""Remove a bundle."""
return self._remove_xdata(self.groups_xml, "Bundle", bundle_name)
+ def remove_client(self, client_name):
+ """Remove a bundle."""
+ return self._remove_xdata(self.clients_xml, "Client", client_name)
+
def _handle_clients_xml_event(self, event):
xdata = self.clients_xml.xdata
- self.clients = {}
+ self.clients = []
+ self.clientgroups = {}
self.aliases = {}
self.raliases = {}
- self.bad_clients = {}
self.secure = []
self.floating = []
self.addresses = {}
@@ -423,9 +468,10 @@ def _handle_clients_xml_event(self, event):
'cert+password')
if 'uuid' in client.attrib:
self.uuid[client.get('uuid')] = clname
- if client.get('secure', 'false') == 'true':
+ if client.get('secure', 'false').lower() == 'true':
self.secure.append(clname)
- if client.get('location', 'fixed') == 'floating':
+ if (client.get('location', 'fixed') == 'floating' or
+ client.get('floating', 'false').lower() == 'true'):
self.floating.append(clname)
if 'password' in client.attrib:
self.passwords[clname] = client.get('password')
@@ -445,106 +491,157 @@ def _handle_clients_xml_event(self, event):
if clname not in self.raddresses:
self.raddresses[clname] = set()
self.raddresses[clname].add(alias.get('address'))
- self.clients.update({clname: client.get('profile')})
+ self.clients.append(clname)
+ try:
+ self.clientgroups[clname].append(client.get('profile'))
+ except KeyError:
+ self.clientgroups[clname] = [client.get('profile')]
self.states['clients.xml'] = True
def _handle_groups_xml_event(self, event):
- xdata = self.groups_xml.xdata
- self.public = []
- self.private = []
- self.profiles = []
self.groups = {}
- grouptmp = {}
- self.categories = {}
- groupseen = list()
- for group in xdata.xpath('//Groups/Group'):
- if group.get('name') not in groupseen:
- groupseen.append(group.get('name'))
+
+ # get_condition and aggregate_conditions must be separate
+ # functions in order to ensure that the scope is right for the
+ # closures they return
+ def get_condition(element):
+ negate = element.get('negate', 'false').lower() == 'true'
+ pname = element.get("name")
+ if element.tag == 'Group':
+ return lambda c, g, _: negate != (pname in g)
+ elif element.tag == 'Client':
+ return lambda c, g, _: negate != (pname == c)
+
+ def aggregate_conditions(conditions):
+ return lambda client, groups, cats: \
+ all(cond(client, groups, cats) for cond in conditions)
+
+ # first, we get a list of all of the groups declared in the
+ # file. we do this in two stages because the old way of
+ # parsing groups.xml didn't support nested groups; in the old
+ # way, only Group tags under a Groups tag counted as
+ # declarative. so we parse those first, and then parse the
+ # other Group tags if they haven't already been declared.
+ # this lets you set options on a group (e.g., public="false")
+ # at the top level and then just use the name elsewhere, which
+ # is the original behavior
+ for grp in self.groups_xml.xdata.xpath("//Groups/Group") + \
+ self.groups_xml.xdata.xpath("//Groups/Group//Group"):
+ if grp.get("name") in self.groups:
+ continue
+ self.groups[grp.get("name")] = \
+ MetadataGroup(grp.get("name"),
+ bundles=[b.get("name")
+ for b in grp.findall("Bundle")],
+ category=grp.get("category"),
+ is_profile=grp.get("profile", "false") == "true",
+ is_public=grp.get("public", "false") == "true",
+ is_private=grp.get("public", "true") == "false")
+ if grp.get('default', 'false') == 'true':
+ self.default = grp.get('name')
+
+ self.group_membership = dict()
+ self.negated_groups = dict()
+ self.options = dict()
+ # confusing loop condition; the XPath query asks for all
+ # elements under a Group tag under a Groups tag; that is
+ # infinitely recursive, so "all" elements really means _all_
+ # elements. We then manually filter out non-Group elements
+ # since there doesn't seem to be a way to get Group elements
+ # of arbitrary depth with particular ultimate ancestors in
+ # XPath. We do the same thing for Client tags.
+ for el in self.groups_xml.xdata.xpath("//Groups/Group//*") + \
+ self.groups_xml.xdata.xpath("//Groups/Client//*"):
+ if ((el.tag != 'Group' and el.tag != 'Client') or
+ el.getchildren()):
+ continue
+
+ conditions = []
+ for parent in el.iterancestors():
+ cond = get_condition(parent)
+ if cond:
+ conditions.append(cond)
+
+ gname = el.get("name")
+ if el.get("negate", "false").lower() == "true":
+ self.negated_groups[aggregate_conditions(conditions)] = \
+ self.groups[gname]
else:
- self.logger.error("Metadata: Group %s defined multiply" %
- group.get('name'))
- grouptmp[group.get('name')] = \
- ([item.get('name') for item in group.findall('./Bundle')],
- [item.get('name') for item in group.findall('./Group')])
- grouptmp[group.get('name')][1].append(group.get('name'))
- if group.get('default', 'false') == 'true':
- self.default = group.get('name')
- if group.get('profile', 'false') == 'true':
- self.profiles.append(group.get('name'))
- if group.get('public', 'false') == 'true':
- self.public.append(group.get('name'))
- elif group.get('public', 'true') == 'false':
- self.private.append(group.get('name'))
- if 'category' in group.attrib:
- self.categories[group.get('name')] = group.get('category')
-
- for group in grouptmp:
- # self.groups[group] => (bundles, groups, categories)
- self.groups[group] = (set(), set(), {})
- tocheck = [group]
- group_cat = self.groups[group][2]
- while tocheck:
- now = tocheck.pop()
- self.groups[group][1].add(now)
- if now in grouptmp:
- (bundles, groups) = grouptmp[now]
- for ggg in groups:
- if ggg in self.groups[group][1]:
- continue
- if (ggg not in self.categories or \
- self.categories[ggg] not in self.groups[group][2]):
- self.groups[group][1].add(ggg)
- tocheck.append(ggg)
- if ggg in self.categories:
- group_cat[self.categories[ggg]] = ggg
- elif ggg in self.categories:
- self.logger.info("Group %s: %s cat-suppressed %s" % \
- (group,
- group_cat[self.categories[ggg]],
- ggg))
- [self.groups[group][0].add(bund) for bund in bundles]
+ if self.groups[gname].category and gname in self.groups:
+ category = self.groups[gname].category
+
+ def in_cat(client, groups, categories):
+ if category in categories:
+ self.logger.warning("%s: Group %s suppressed by "
+ "category %s; %s already a "
+ "member of %s" %
+ (self.name, gname, category,
+ client, categories[category]))
+ return False
+ return True
+ conditions.append(in_cat)
+
+ self.group_membership[aggregate_conditions(conditions)] = \
+ self.groups[gname]
self.states['groups.xml'] = True
def HandleEvent(self, event):
"""Handle update events for data files."""
- if self.clients_xml.HandleEvent(event):
- self._handle_clients_xml_event(event)
- elif self.groups_xml.HandleEvent(event):
- self._handle_groups_xml_event(event)
-
- if False not in list(self.states.values()):
- # check that all client groups are real and complete
- real = list(self.groups.keys())
- for client in list(self.clients.keys()):
- if self.clients[client] not in self.profiles:
- self.logger.error("Client %s set as nonexistent or "
- "incomplete group %s" %
- (client, self.clients[client]))
- self.logger.error("Removing client mapping for %s" % client)
- self.bad_clients[client] = self.clients[client]
- del self.clients[client]
- for bclient in list(self.bad_clients.keys()):
- if self.bad_clients[bclient] in self.profiles:
- self.logger.info("Restored profile mapping for client %s" %
- bclient)
- self.clients[bclient] = self.bad_clients[bclient]
- del self.bad_clients[bclient]
-
- def set_profile(self, client, profile, addresspair):
+ for hdlr in self.handlers:
+ aname = re.sub(r'[^A-z0-9_]', '_', os.path.basename(event.filename))
+ if hdlr(event):
+ try:
+ proc = getattr(self, "_handle_%s_event" % aname)
+ except AttributeError:
+ proc = self._handle_default_event
+ proc(event)
+
+ if False not in list(self.states.values()) and self.debug_flag:
+ # check that all groups are real and complete. this is
+ # just logged at a debug level because many groups might
+ # be probed, and we don't want to warn about them.
+ for client, groups in list(self.clientgroups.items()):
+ for group in groups:
+ if group not in self.groups:
+ self.debug_log("Client %s set as nonexistent group %s" %
+ (client, group))
+ for gname, ginfo in list(self.groups.items()):
+ for group in ginfo.groups:
+ if group not in self.groups:
+ self.debug_log("Group %s set as nonexistent group %s" %
+ (gname, group))
+
+
+ def set_profile(self, client, profile, addresspair, force=False):
"""Set group parameter for provided client."""
- self.logger.info("Asserting client %s profile to %s" % (client,
- profile))
+ self.logger.info("Asserting client %s profile to %s" %
+ (client, profile))
if False in list(self.states.values()):
- raise MetadataRuntimeError("Metadata has not been read yet")
- if profile not in self.public:
- msg = "Failed to set client %s to private group %s" % (client,
- profile)
+ raise MetadataRuntimeError
+ if not force and profile not in self.groups:
+ msg = "Profile group %s does not exist" % profile
+ self.logger.error(msg)
+ raise MetadataConsistencyError(msg)
+ group = self.groups[profile]
+ if not force and not group.is_public:
+ msg = "Cannot set client %s to private group %s" % (client, profile)
self.logger.error(msg)
raise MetadataConsistencyError(msg)
+ self._set_profile(client, profile, addresspair)
+
+ def _set_profile(self, client, profile, addresspair):
if client in self.clients:
- self.logger.info("Changing %s group from %s to %s" %
- (client, self.clients[client], profile))
+ profiles = [g for g in self.clientgroups[client]
+ if g in self.groups and self.groups[g].is_profile]
+ self.logger.info("Changing %s profile from %s to %s" %
+ (client, profiles, profile))
self.update_client(client, dict(profile=profile))
+ if client in self.clientgroups:
+ for p in profiles:
+ self.clientgroups[client].remove(p)
+ self.clientgroups[client].append(profile)
+ else:
+ self.clientgroups[client] = [profile]
else:
self.logger.info("Creating new client: %s, profile %s" %
(client, profile))
@@ -555,7 +652,8 @@ def set_profile(self, client, profile, addresspair):
address=addresspair[0]))
else:
self.add_client(client, dict(profile=profile))
- self.clients[client] = profile
+ self.clients.append(client)
+ self.clientgroups[client] = [profile]
self.clients_xml.write()
def set_version(self, client, version):
@@ -614,6 +712,31 @@ def resolve_client(self, addresspair, cleanup_cache=False):
self.logger.warning(warning)
raise MetadataConsistencyError(warning)
+ def _merge_groups(self, client, groups, categories=None):
+ """ set group membership based on the contents of groups.xml
+ and initial group membership of this client. Returns a tuple
+ of (allgroups, categories)"""
+ numgroups = -1 # force one initial pass
+ if categories is None:
+ categories = dict()
+ while numgroups != len(groups):
+ numgroups = len(groups)
+ for predicate, group in self.group_membership.items():
+ if group.name in groups:
+ continue
+ if predicate(client, groups, categories):
+ groups.add(group.name)
+ if group.category:
+ categories[group.category] = group.name
+ for predicate, group in self.negated_groups.items():
+ if group.name not in groups:
+ continue
+ if predicate(client, groups, categories):
+ groups.remove(group.name)
+ if group.category:
+ del categories[group.category]
+ return (groups, categories)
+
def get_initial_metadata(self, client):
"""Return the metadata for a given client."""
if False in list(self.states.values()):
@@ -621,25 +744,66 @@ def get_initial_metadata(self, client):
client = client.lower()
if client in self.aliases:
client = self.aliases[client]
- if client in self.clients:
- profile = self.clients[client]
- (bundles, groups, categories) = self.groups[profile]
- else:
- if self.default == None:
- msg = "Cannot set group for client %s; no default group set" % \
- client
+
+ groups = set()
+ categories = dict()
+ profile = None
+
+ if client not in self.clients:
+ pgroup = None
+ if client in self.clientgroups:
+ pgroup = self.clientgroups[client][0]
+ elif self.default:
+ pgroup = self.default
+
+ if pgroup:
+ self.set_profile(client, pgroup, (None, None), force=True)
+ groups.add(pgroup)
+ category = self.groups[pgroup].category
+ if category:
+ categories[category] = pgroup
+ if (pgroup in self.groups and self.groups[pgroup].is_profile):
+ profile = pgroup
+ else:
+ msg = "Cannot add new client %s; no default group set" % client
self.logger.error(msg)
raise MetadataConsistencyError(msg)
- self.set_profile(client, self.default, (None, None))
- profile = self.default
- [bundles, groups, categories] = self.groups[self.default]
+
+ if client in self.clientgroups:
+ for cgroup in self.clientgroups[client]:
+ if cgroup in groups:
+ continue
+ if cgroup not in self.groups:
+ self.groups[cgroup] = MetadataGroup(cgroup)
+ category = self.groups[cgroup].category
+ if category and category in categories:
+ self.logger.warning("%s: Group %s suppressed by "
+ "category %s; %s already a member "
+ "of %s" %
+ (self.name, cgroup, category,
+ client, categories[category]))
+ continue
+ if category:
+ categories[category] = cgroup
+ groups.add(cgroup)
+ # favor client groups for setting profile
+ if not profile and self.groups[cgroup].is_profile:
+ profile = cgroup
+
+ groups, categories = self._merge_groups(client, groups,
+ categories=categories)
+
+ bundles = set()
+ for group in groups:
+ try:
+ bundles.update(self.groups[group].bundles)
+ except KeyError:
+ self.logger.warning("%s: %s is a member of undefined group %s" %
+ (self.name, client, group))
+
aliases = self.raliases.get(client, set())
addresses = self.raddresses.get(client, set())
version = self.versions.get(client, None)
- newgroups = set(groups)
- newbundles = set(bundles)
- newcategories = {}
- newcategories.update(categories)
if client in self.passwords:
password = self.passwords[client]
else:
@@ -650,36 +814,41 @@ def get_initial_metadata(self, client):
uuid = uuids[0]
else:
uuid = None
- for group in self.cgroups.get(client, []):
- if group in self.groups:
- nbundles, ngroups, ncategories = self.groups[group]
- else:
- nbundles, ngroups, ncategories = ([], [group], {})
- [newbundles.add(b) for b in nbundles if b not in newbundles]
- [newgroups.add(g) for g in ngroups if g not in newgroups]
- newcategories.update(ncategories)
- return ClientMetadata(client, profile, newgroups, newbundles, aliases,
- addresses, newcategories, uuid, password, version,
+ if not profile:
+ # one last ditch attempt at setting the profile
+ profiles = [g for g in groups
+ if g in self.groups and self.groups[g].is_profile]
+ if len(profiles) >= 1:
+ profile = profiles[0]
+
+ return ClientMetadata(client, profile, groups, bundles, aliases,
+ addresses, categories, uuid, password, version,
self.query)
def get_all_group_names(self):
all_groups = set()
- [all_groups.update(g[1]) for g in list(self.groups.values())]
+ all_groups.update(self.groups.keys())
+ all_groups.update([g.name for g in self.group_membership.values()])
+ all_groups.update([g.name for g in self.negated_groups.values()])
+ for grp in self.clientgroups.values():
+ all_groups.update(grp)
return all_groups
def get_all_groups_in_category(self, category):
- all_groups = set()
- [all_groups.add(g) for g in self.categories \
- if self.categories[g] == category]
- return all_groups
+ return set([g.name for g in self.groups.values()
+ if g.category == category])
def get_client_names_by_profiles(self, profiles):
- return [client for client, profile in list(self.clients.items()) \
- if profile in profiles]
+ rv = []
+ for client in list(self.clients):
+ mdata = self.get_initial_metadata(client)
+ if mdata.profile in profiles:
+ rv.append(client)
+ return rv
def get_client_names_by_groups(self, groups):
mdata = [self.core.build_metadata(client)
- for client in list(self.clients.keys())]
+ for client in list(self.clients)]
return [md.hostname for md in mdata if md.groups.issuperset(groups)]
def get_client_names_by_bundles(self, bundles):
@@ -689,27 +858,26 @@ def get_client_names_by_bundles(self, bundles):
def merge_additional_groups(self, imd, groups):
for group in groups:
- if (group in self.categories and
- self.categories[group] in imd.categories):
+ if group in imd.groups or group not in self.groups:
continue
- newbundles, newgroups, _ = self.groups.get(group,
- (list(),
- [group],
- dict()))
- for newbundle in newbundles:
- if newbundle not in imd.bundles:
- imd.bundles.add(newbundle)
- for newgroup in newgroups:
- if newgroup not in imd.groups:
- if (newgroup in self.categories and
- self.categories[newgroup] in imd.categories):
- continue
- if newgroup in self.private:
- self.logger.error("Refusing to add dynamic membership "
- "in private group %s for client %s" %
- (newgroup, imd.hostname))
- continue
- imd.groups.add(newgroup)
+ category = self.groups[group].category
+ if category:
+ if self.groups[group].category in imd.categories:
+ self.logger.warning("%s: Group %s suppressed by category "
+ "%s; %s already a member of %s" %
+ (self.name, group, category,
+ imd.hostname,
+ imd.categories[category]))
+ continue
+ imd.categories[group] = category
+ imd.groups.add(group)
+
+ self._merge_groups(imd.hostname, imd.groups,
+ categories=imd.categories)
+
+ for group in imd.groups:
+ if group in self.groups:
+ imd.bundles.update(self.groups[group].bundles)
def merge_additional_data(self, imd, source, data):
if not hasattr(imd, source):
@@ -728,8 +896,8 @@ def validate_client_address(self, client, addresspair):
(client, address))
return True
else:
- self.logger.error("Got request for non-float client %s from %s" %
- (client, address))
+ self.logger.error("Got request for non-float client %s from %s"
+ % (client, address))
return False
resolved = self.resolve_client(addresspair)
if resolved.lower() == client.lower():
@@ -853,20 +1021,26 @@ def include_group(group):
del categories[None]
if hosts:
instances = {}
- clients = self.clients
- for client, profile in list(clients.items()):
+ for client in list(self.clients):
if include_client(client):
continue
- if profile in instances:
- instances[profile].append(client)
+ if client in self.clientgroups:
+ groups = self.clientgroups[client]
+ elif self.default:
+ groups = [self.default]
else:
- instances[profile] = [client]
- for profile, clist in list(instances.items()):
+ continue
+ for group in groups:
+ try:
+ instances[group].append(client)
+ except KeyError:
+ instances[group] = [client]
+ for group, clist in list(instances.items()):
clist.sort()
viz_str.append('"%s-instances" [ label="%s", shape="record" ];' %
- (profile, '|'.join(clist)))
+ (group, '|'.join(clist)))
viz_str.append('"%s-instances" -> "group-%s";' %
- (profile, profile))
+ (group, group))
if bundles:
bundles = []
[bundles.append(bund.get('name')) \
@@ -907,3 +1081,35 @@ def include_group(group):
viz_str.append('"%s" [label="%s", shape="record", style="filled", fillcolor="%s"];' %
(category, category, categories[category]))
return "\n".join("\t" + s for s in viz_str)
+
+
+class MetadataLint(Bcfg2.Server.Lint.ServerPlugin):
+ def Run(self):
+ self.nested_clients()
+ self.deprecated_options()
+
+ @classmethod
+ def Errors(cls):
+ return {"nested-client-tags": "warning",
+ "deprecated-clients-options": "warning"}
+
+ def deprecated_options(self):
+ groupdata = self.metadata.clients_xml.xdata
+ for el in groupdata.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: %s" %
+ (loc, floating, self.RenderXML(el)))
+
+ def nested_clients(self):
+ 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)))
View
11 src/lib/Bcfg2/Server/Plugins/Packages/Collection.py
@@ -375,15 +375,7 @@ def factory(metadata, sources, basepath, debug=False):
",".join([s.__name__ for s in sclasses]))
cclass = Collection
elif len(sclasses) == 0:
- # you'd think this should be a warning, but it happens all the
- # freaking time if you have a) machines in your clients.xml
- # that do not have the proper groups set up yet (e.g., if you
- # have multiple Bcfg2 servers and Packages-relevant groups set
- # by probes); and b) templates that query all or multiple
- # machines (e.g., with metadata.query.all_clients())
- if debug:
- logger.error("Packages: No sources found for %s" %
- metadata.hostname)
+ logger.error("Packages: No sources found for %s" % metadata.hostname)
cclass = Collection
else:
cclass = get_collection_class(sclasses.pop().__name__.replace("Source",
@@ -398,4 +390,3 @@ def factory(metadata, sources, basepath, debug=False):
clients[metadata.hostname] = ckey
collections[ckey] = collection
return collection
-
View
2  src/lib/Bcfg2/Server/Reports/settings.py
@@ -43,7 +43,7 @@
db_engine = c.get('statistics', 'database_engine')
except ConfigParser.NoSectionError:
e = sys.exc_info()[1]
- raise ImportError("Failed to determine database engine: %s" % e)
+ raise ImportError("Failed to determine database engine for reports: %s" % e)
db_name = ''
if c.has_option('statistics', 'database_name'):
db_name = c.get('statistics', 'database_name')
View
62 src/lib/Bcfg2/Server/models.py
@@ -0,0 +1,62 @@
+import sys
+import logging
+import Bcfg2.Options
+import Bcfg2.Server.Plugins
+from django.db import models
+from Bcfg2.Bcfg2Py3k import ConfigParser
+
+logger = logging.getLogger('Bcfg2.Server.models')
+
+MODELS = []
+
+def load_models(plugins=None, cfile='/etc/bcfg2.conf', quiet=True):
+ global MODELS
+
+ if plugins is None:
+ # we want to provide a different default plugin list --
+ # namely, _all_ plugins, so that the database is guaranteed to
+ # work, even if /etc/bcfg2.conf isn't set up properly
+ plugin_opt = Bcfg2.Options.SERVER_PLUGINS
+ plugin_opt.default = Bcfg2.Server.Plugins.__all__
+
+ setup = Bcfg2.Options.OptionParser(dict(plugins=plugin_opt,
+ configfile=Bcfg2.Options.CFILE),
+ quiet=quiet)
+ setup.parse([Bcfg2.Options.CFILE.cmd, cfile])
+ plugins = setup['plugins']
+
+ if MODELS:
+ # load_models() has been called once, so first unload all of
+ # the models; otherwise we might call load_models() with no
+ # arguments, end up with _all_ models loaded, and then in a
+ # subsequent call only load a subset of models
+ for model in MODELS:
+ delattr(sys.modules[__name__], model)
+ MODELS = []
+
+ for plugin in plugins:
+ try:
+ mod = getattr(__import__("Bcfg2.Server.Plugins.%s" %
+ plugin).Server.Plugins, plugin)
+ except ImportError:
+ try:
+ mod = __import__(plugin)
+ except:
+ if plugins != Bcfg2.Server.Plugins.__all__:
+ # only produce errors if the default plugin list
+ # was not used -- i.e., if the config file was set
+ # up. don't produce errors when trying to load
+ # all plugins, IOW
+ err = sys.exc_info()[1]
+ logger.error("Failed to load plugin %s: %s" % (plugin, err))
+ continue
+ for sym in dir(mod):
+ obj = getattr(mod, sym)
+ if hasattr(obj, "__bases__") and models.Model in obj.__bases__:
+ print("Adding %s to models" % sym)
+ setattr(sys.modules[__name__], sym, obj)
+ MODELS.append(sym)
+
+# basic invocation to ensure that a default set of models is loaded,
+# and thus that this module will always work.
+load_models(quiet=True)
View
14 src/lib/Bcfg2/manage.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+from django.core.management import execute_manager
+import imp
+try:
+ imp.find_module('settings') # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__)
+ sys.exit(1)
+
+import settings
+
+if __name__ == "__main__":
+ execute_manager(settings)
View
71 src/lib/Bcfg2/settings.py
@@ -0,0 +1,71 @@
+import sys
+import django
+import Bcfg2.Options
+
+DATABASES = dict()
+
+# Django < 1.2 compat
+DATABASE_ENGINE = None
+DATABASE_NAME = None
+DATABASE_USER = None
+DATABASE_PASSWORD = None
+DATABASE_HOST = None
+DATABASE_PORT = None
+
+def read_config(cfile='/etc/bcfg2.conf', repo=None, quiet=False):
+ global DATABASE_ENGINE, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, \
+ DATABASE_HOST, DATABASE_PORT
+
+ setup = \
+ Bcfg2.Options.OptionParser(dict(repo=Bcfg2.Options.SERVER_REPOSITORY,
+ configfile=Bcfg2.Options.CFILE,
+ db_engine=Bcfg2.Options.DB_ENGINE,
+ db_name=Bcfg2.Options.DB_NAME,
+ db_user=Bcfg2.Options.DB_USER,
+ db_password=Bcfg2.Options.DB_PASSWORD,
+ db_host=Bcfg2.Options.DB_HOST,
+ db_port=Bcfg2.Options.DB_PORT),
+ quiet=quiet)
+ setup.parse([Bcfg2.Options.CFILE.cmd, cfile])
+
+ if repo is None:
+ repo = setup['repo']
+
+ DATABASES['default'] = \
+ dict(ENGINE=setup['db_engine'],
+ NAME=setup['db_name'],
+ USER=setup['db_user'],
+ PASSWORD=setup['db_password'],
+ HOST=setup['db_host'],
+ PORT=setup['db_port'])
+
+ if django.VERSION[0] == 1 and django.VERSION[1] < 2:
+ DATABASE_ENGINE = setup['db_engine']
+ DATABASE_NAME = DATABASES['default']['NAME']
+ DATABASE_USER = DATABASES['default']['USER']
+ DATABASE_PASSWORD = DATABASES['default']['PASSWORD']
+ DATABASE_HOST = DATABASES['default']['HOST']
+ DATABASE_PORT = DATABASES['default']['PORT']<