summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Plugins/Properties.py
blob: d9e62264533aa9194c6e0dc5343e63bffda8a1a5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
""" The properties plugin maps property files into client metadata
instances. """

import os
import re
import sys
import copy
import logging
import lxml.etree
import Bcfg2.Server.Plugin
from Bcfg2.Server.Plugin import PluginExecutionError
try:
    from Bcfg2.Encryption import ssl_decrypt, get_passphrases, \
        get_algorithm, bruteforce_decrypt, EVPError
    HAS_CRYPTO = True
except ImportError:
    HAS_CRYPTO = False

LOGGER = logging.getLogger(__name__)

SETUP = None


class PropertyFile(Bcfg2.Server.Plugin.StructFile):
    """ Class for properties files. """

    def write(self):
        """ Write the data in this data structure back to the property
        file """
        if not SETUP.cfp.getboolean("properties", "writes_enabled",
                                    default=True):
            msg = "Properties files write-back is disabled in the " + \
                "configuration"
            LOGGER.error(msg)
            raise PluginExecutionError(msg)
        try:
            self.validate_data()
        except PluginExecutionError:
            msg = "Cannot write %s: %s" % (self.name, sys.exc_info()[1])
            LOGGER.error(msg)
            raise PluginExecutionError(msg)

        try:
            open(self.name, "wb").write(
                lxml.etree.tostring(self.xdata,
                                    xml_declaration=False,
                                    pretty_print=True).decode('UTF-8'))
            return True
        except IOError:
            err = sys.exc_info()[1]
            msg = "Failed to write %s: %s" % (self.name, err)
            LOGGER.error(msg)
            raise PluginExecutionError(msg)

    def validate_data(self):
        """ ensure that the data in this object validates against the
        XML schema for this property file (if a schema exists) """
        schemafile = self.name.replace(".xml", ".xsd")
        if os.path.exists(schemafile):
            try:
                schema = lxml.etree.XMLSchema(file=schemafile)
            except lxml.etree.XMLSchemaParseError:
                err = sys.exc_info()[1]
                raise PluginExecutionError("Failed to process schema for %s: "
                                           "%s" % (self.name, err))
        else:
            # no schema exists
            return True

        if not schema.validate(self.xdata):
            raise PluginExecutionError("Data for %s fails to validate; run "
                                       "bcfg2-lint for more details" %
                                       self.name)
        else:
            return True

    def Index(self):
        Bcfg2.Server.Plugin.StructFile.Index(self)
        if self.xdata.get("encryption", "false").lower() != "false":
            if not HAS_CRYPTO:
                msg = "Properties: M2Crypto is not available: %s" % self.name
                LOGGER.error(msg)
                raise PluginExecutionError(msg)
            for el in self.xdata.xpath("//*[@encrypted]"):
                try:
                    el.text = self._decrypt(el)
                except EVPError:
                    msg = "Failed to decrypt %s element in %s" % (el.tag,
                                                                  self.name)
                    LOGGER.error(msg)
                    raise PluginExecutionError(msg)

    def _decrypt(self, element):
        """ Decrypt a single encrypted properties file element """
        if not element.text.strip():
            return
        passes = get_passphrases(SETUP)
        try:
            passphrase = passes[element.get("encrypted")]
            try:
                return ssl_decrypt(element.text, passphrase,
                                   algorithm=get_algorithm(SETUP))
            except EVPError:
                # error is raised below
                pass
        except KeyError:
            # bruteforce_decrypt raises an EVPError with a sensible
            # error message, so we just let it propagate up the stack
            return bruteforce_decrypt(element.text,
                                      passphrases=passes.values(),
                                      algorithm=get_algorithm(SETUP))
        raise EVPError("Failed to decrypt")


class PropDirectoryBacked(Bcfg2.Server.Plugin.DirectoryBacked):
    """ A collection of properties files """
    __child__ = PropertyFile
    patterns = re.compile(r'.*\.xml$')
    ignore = re.compile(r'.*\.xsd$')


class Properties(Bcfg2.Server.Plugin.Plugin,
                 Bcfg2.Server.Plugin.Connector):
    """ The properties plugin maps property files into client metadata
    instances. """
    name = 'Properties'

    def __init__(self, core, datastore):
        global SETUP  # pylint: disable=W0603
        Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
        Bcfg2.Server.Plugin.Connector.__init__(self)
        try:
            self.store = PropDirectoryBacked(self.data, core.fam)
        except OSError:
            err = sys.exc_info()[1]
            self.logger.error("Error while creating Properties store: %s" %
                              err)
            raise Bcfg2.Server.Plugin.PluginInitError

        SETUP = core.setup

    def get_additional_data(self, metadata):
        if self.core.setup.cfp.getboolean("properties", "automatch",
                                          default=False):
            default_automatch = "true"
        else:
            default_automatch = "false"
        rv = dict()
        for fname, pfile in self.store.entries.items():
            if pfile.xdata.get("automatch",
                               default_automatch).lower() == "true":
                rv[fname] = pfile.XMLMatch(metadata)
            else:
                rv[fname] = copy.copy(pfile)
        return rv