~ahasenack/landscape-client/landscape-client-1.5.5-0ubuntu0.9.04.0

« back to all changes in this revision

Viewing changes to landscape/deployment.py

  • Committer: Bazaar Package Importer
  • Author(s): Rick Clark
  • Date: 2008-09-08 16:35:57 UTC
  • mto: This revision was merged to the branch mainline in revision 2.
  • Revision ID: james.westby@ubuntu.com-20080908163557-fl0d2oc35hur473w
Tags: upstream-1.0.18
ImportĀ upstreamĀ versionĀ 1.0.18

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import signal
 
2
import os
 
3
import sys
 
4
 
 
5
from logging import (getLevelName, getLogger,
 
6
                     FileHandler, StreamHandler, Formatter, info)
 
7
 
 
8
from optparse import OptionParser
 
9
from ConfigParser import ConfigParser
 
10
 
 
11
import dbus.glib # Side-effects rule!
 
12
 
 
13
from twisted.application.service import Application, Service
 
14
from twisted.application.app import startApplication
 
15
 
 
16
from landscape import VERSION
 
17
from landscape.lib.persist import Persist
 
18
from landscape.lib.dbus_util import get_bus
 
19
from landscape.lib import bpickle_dbus
 
20
from landscape.log import rotate_logs
 
21
from landscape.reactor import TwistedReactor
 
22
 
 
23
from landscape.upgraders import UPGRADE_MANAGERS
 
24
 
 
25
 
 
26
def init_logging(configuration, program_name):
 
27
    """Given a basic configuration, set up logging."""
 
28
    handlers = []
 
29
    if not os.path.exists(configuration.log_dir):
 
30
        os.makedirs(configuration.log_dir)
 
31
    log_filename = os.path.join(configuration.log_dir, program_name+".log")
 
32
    handlers.append(FileHandler(log_filename))
 
33
    if not configuration.quiet:
 
34
        handlers.append(StreamHandler(sys.stdout))
 
35
    getLogger().setLevel(getLevelName(configuration.log_level.upper()))
 
36
    for handler in handlers:
 
37
        getLogger().addHandler(handler)
 
38
        format = ("%(asctime)s %(levelname)-8s [%(threadName)-10s] "
 
39
                  "%(message)s")
 
40
        handler.setFormatter(Formatter(format))
 
41
 
 
42
 
 
43
class BaseConfiguration(object):
 
44
    """Base class for configuration implementations.
 
45
 
 
46
    @var required_options: Optionally, a sequence of key names to require when
 
47
        reading or writing a configuration.
 
48
    @var unsaved_options: Optionally, a sequence of key names to never write
 
49
        to the configuration file.  This is useful when you want to provide
 
50
        command-line options that should never end up in a configuration file.
 
51
    @var default_config_filenames: A sequence of filenames to check when
 
52
        reading or writing a configuration.
 
53
    """
 
54
 
 
55
    required_options = ()
 
56
    unsaved_options = ()
 
57
    default_config_filenames = ["/etc/landscape/client.conf"]
 
58
    if (os.path.dirname(os.path.abspath(sys.argv[0]))
 
59
        == os.path.abspath("scripts")):
 
60
        default_config_filenames.insert(0, "landscape-client.conf")
 
61
    default_config_filenames = tuple(default_config_filenames)
 
62
    config_section = "client"
 
63
 
 
64
    def __init__(self):
 
65
        self._set_options = {}
 
66
        self._command_line_args = []
 
67
        self._command_line_options = {}
 
68
        self._config_filename = None
 
69
        self._config_file_options = {}
 
70
        self._parser = self.make_parser()
 
71
        self._command_line_defaults = self._parser.defaults.copy()
 
72
        # We don't want them mixed with explicitly given options,
 
73
        # otherwise we can't define the precedence properly.
 
74
        self._parser.defaults.clear()
 
75
 
 
76
    def __getattr__(self, name):
 
77
        """Find and return the value of the given configuration parameter.
 
78
 
 
79
        The following sources will be searched:
 
80
          * The attributes that were explicitly set on this object,
 
81
          * The parameters specified on the command line,
 
82
          * The parameters specified in the configuration file, and
 
83
          * The defaults.
 
84
 
 
85
        If no values are found and the parameter does exist as a possible
 
86
        parameter, C{None} is returned.
 
87
 
 
88
        Otherwise C{AttributeError} is raised.
 
89
        """
 
90
        for options in [self._set_options,
 
91
                        self._command_line_options,
 
92
                        self._config_file_options,
 
93
                        self._command_line_defaults]:
 
94
            if name in options:
 
95
                value = options[name]
 
96
                break
 
97
        else:
 
98
            if self._parser.has_option("--" + name.replace("_", "-")):
 
99
                value = None
 
100
            else:
 
101
                raise AttributeError(name)
 
102
        if isinstance(value, basestring):
 
103
            option = self._parser.get_option("--" + name.replace("_", "-"))
 
104
            if option is not None:
 
105
                value = option.convert_value(None, value)
 
106
        return value
 
107
 
 
108
    def get(self, name, default=None):
 
109
        try:
 
110
            return self.__getattr__(name)
 
111
        except AttributeError:
 
112
            return default
 
113
 
 
114
    def __setattr__(self, name, value):
 
115
        """Set a configuration parameter.
 
116
 
 
117
        If the name begins with C{_}, it will only be set on this object and
 
118
        not stored in the configuration file.
 
119
        """
 
120
        if name.startswith("_"):
 
121
            super(BaseConfiguration, self).__setattr__(name, value)
 
122
        else:
 
123
            self._set_options[name] = value
 
124
 
 
125
    def reload(self):
 
126
        self.load(self._command_line_args)
 
127
 
 
128
    def load(self, args, accept_unexistent_config=False):
 
129
        """
 
130
        Load configuration data from command line arguments and a config file.
 
131
 
 
132
        @raise: A SystemExit if the arguments are bad.
 
133
        """
 
134
        self.load_command_line(args)
 
135
 
 
136
        # Parse configuration file, if found.
 
137
        if self.config:
 
138
            if os.path.isfile(self.config):
 
139
                self.load_configuration_file(self.config)
 
140
            elif not accept_unexistent_config:
 
141
                sys.exit("error: file not found: %s" % self.config)
 
142
        else:
 
143
            for potential_config_file in self.default_config_filenames:
 
144
                if os.access(potential_config_file, os.R_OK):
 
145
                    self.load_configuration_file(potential_config_file)
 
146
                    break
 
147
 
 
148
        # Check that all needed options were given.
 
149
        for option in self.required_options:
 
150
            if not getattr(self, option):
 
151
                sys.exit("error: must specify --%s "
 
152
                         "or the '%s' directive in the config file."
 
153
                         % (option.replace('_','-'), option))
 
154
 
 
155
        if self.bus not in ("session", "system"):
 
156
            sys.exit("error: bus must be one of 'session' or 'system'")
 
157
 
 
158
    def load_command_line(self, args):
 
159
        """Load configuration data from the given command line."""
 
160
        self._command_line_args = args
 
161
        values = self._parser.parse_args(args)[0]
 
162
        self._command_line_options = vars(values)
 
163
 
 
164
    def load_configuration_file(self, filename):
 
165
        """Load configuration data from the given file name.
 
166
 
 
167
        If any data has already been set on this configuration object,
 
168
        then the old data will take precedence.
 
169
        """
 
170
        self._config_filename = filename
 
171
        config_parser = ConfigParser()
 
172
        config_parser.read(filename)
 
173
        self._config_file_options = dict(
 
174
            config_parser.items(self.config_section))
 
175
 
 
176
    def write(self):
 
177
        """Write back configuration to the configuration file.
 
178
 
 
179
        Values which match the default option in the parser won't be saved.
 
180
 
 
181
        Options are considered in the following precedence:
 
182
 
 
183
        1. Manually set options (config.option = value)
 
184
        2. Options passed in the command line
 
185
        3. Previously existent options in the configuration file
 
186
 
 
187
        The filename picked for saving configuration options is:
 
188
 
 
189
        1. self.config, if defined
 
190
        2. The last loaded configuration file, if any
 
191
        3. The first filename in self.default_config_filenames
 
192
        """
 
193
        config_parser = ConfigParser()
 
194
        config_parser.add_section("client")
 
195
        all_options = self._config_file_options.copy()
 
196
        all_options.update(self._command_line_options)
 
197
        all_options.update(self._set_options)
 
198
        for name, value in all_options.items():
 
199
            if (name != "config" and
 
200
                name not in self.unsaved_options and
 
201
                value != self._command_line_defaults.get(name)):
 
202
                config_parser.set("client", name, value)
 
203
        filename = (self.config or self._config_filename or
 
204
                    self.default_config_filenames[0])
 
205
        config_file = open(filename, "w")
 
206
        config_parser.write(config_file)
 
207
        config_file.close()
 
208
 
 
209
    def make_parser(self):
 
210
        """
 
211
        Return an L{OptionParser} preset with options that all
 
212
        landscape-related programs accept.
 
213
        """
 
214
        parser = OptionParser(version=VERSION)
 
215
        parser.add_option("-c", "--config", metavar="FILE",
 
216
                          help="Use config from this file (any command line "
 
217
                               "options override settings from the file).")
 
218
        parser.add_option("--bus", default="system",
 
219
                          help="Which DBUS bus to use. One of 'session' "
 
220
                               "or 'system'.")
 
221
        return parser
 
222
 
 
223
    def get_config_filename(self):
 
224
        return self._config_filename
 
225
 
 
226
    def get_command_line_options(self):
 
227
        return self._command_line_options
 
228
 
 
229
 
 
230
class Configuration(BaseConfiguration):
 
231
    """Configuration data for Landscape client.
 
232
 
 
233
    This contains all simple data, some of it calculated.
 
234
    """
 
235
 
 
236
    @property
 
237
    def hashdb_filename(self):
 
238
        return os.path.join(self.data_path, "hash.db")
 
239
 
 
240
    def make_parser(self):
 
241
        """
 
242
        Return an L{OptionParser} preset with options that all
 
243
        landscape-related programs accept.
 
244
        """
 
245
        parser = super(Configuration, self).make_parser()
 
246
        parser.add_option("-d", "--data-path", metavar="PATH",
 
247
                          default="/var/lib/landscape/client/",
 
248
                          help="The directory to store data files in.")
 
249
        parser.add_option("-q", "--quiet", default=False, action="store_true",
 
250
                          help="Do not log to the standard output.")
 
251
        parser.add_option("-l", "--log-dir", metavar="FILE",
 
252
                          help="The directory to write log files to.",
 
253
                          default="/var/log/landscape")
 
254
        parser.add_option("--log-level", default="info",
 
255
                          help="One of debug, info, warning, error or "
 
256
                               "critical.")
 
257
        parser.add_option("--ignore-sigint", action="store_true", default=False,
 
258
                          help="Ignore interrupt signals. ")
 
259
 
 
260
        return parser
 
261
 
 
262
 
 
263
def get_versioned_persist(service):
 
264
    """
 
265
    Load a Persist database for the given service and upgrade or mark
 
266
    as current, as necessary.
 
267
    """
 
268
    persist = Persist(filename=service.persist_filename)
 
269
    upgrade_manager = UPGRADE_MANAGERS[service.service_name]
 
270
    if os.path.exists(service.persist_filename):
 
271
        upgrade_manager.apply(persist)
 
272
    else:
 
273
        upgrade_manager.initialize(persist)
 
274
    persist.save(service.persist_filename)
 
275
    return persist
 
276
 
 
277
 
 
278
class LandscapeService(Service, object):
 
279
    """
 
280
    A utility superclass for defining Landscape services.
 
281
 
 
282
    This sets up the reactor, bpickle/dbus integration, a Persist object, and
 
283
    connects to the bus when started.
 
284
 
 
285
    @cvar service_name: The lower-case name of the service. This is used to
 
286
        generate the bpickle filename.
 
287
    """
 
288
    reactor_factory = TwistedReactor
 
289
    persist_filename = None
 
290
 
 
291
    def __init__(self, config):
 
292
        self.config = config
 
293
        bpickle_dbus.install()
 
294
        self.reactor = self.reactor_factory()
 
295
        if self.persist_filename:
 
296
            self.persist = get_versioned_persist(self)
 
297
        signal.signal(signal.SIGUSR1, lambda signal, frame: rotate_logs())
 
298
 
 
299
    def startService(self):
 
300
        Service.startService(self)
 
301
        self.bus = get_bus(self.config.bus)
 
302
        info("%s started on '%s' bus with config %s" % (
 
303
                self.service_name.capitalize(), self.config.bus,
 
304
                self.config.get_config_filename()))
 
305
 
 
306
    def stopService(self):
 
307
        Service.stopService(self)
 
308
        info("%s stopped on '%s' bus with config %s" % (
 
309
                self.service_name.capitalize(), self.config.bus,
 
310
                self.config.get_config_filename()))
 
311
 
 
312
 
 
313
def assert_unowned_bus_name(bus, bus_name):
 
314
    dbus_object = bus.get_object("org.freedesktop.DBus",
 
315
                                 "/org/freedesktop/DBus")
 
316
    if dbus_object.NameHasOwner(bus_name,
 
317
                                dbus_interface="org.freedesktop.DBus"):
 
318
        sys.exit("error: DBus name %s is owned. "
 
319
                 "Is the process already running?" % bus_name)
 
320
 
 
321
 
 
322
def run_landscape_service(configuration_class, service_class, args, bus_name):
 
323
    """Run a Landscape service.
 
324
 
 
325
    @param configuration_class: A subclass of L{Configuration} for processing
 
326
        the C{args} and config file.
 
327
    @param service_class: A subclass of L{LandscapeService} to create and start.
 
328
    @param args: Command line arguments.
 
329
    @param bus_name: A bus name used to verify if the service is already
 
330
        running.
 
331
    """
 
332
    from twisted.internet.glib2reactor import install
 
333
    install()
 
334
    from twisted.internet import reactor
 
335
 
 
336
    # Let's consider adding this:
 
337
#     from twisted.python.log import startLoggingWithObserver, PythonLoggingObserver
 
338
#     startLoggingWithObserver(PythonLoggingObserver().emit, setStdout=False)
 
339
 
 
340
    configuration = configuration_class()
 
341
    configuration.load(args)
 
342
    init_logging(configuration, service_class.service_name)
 
343
 
 
344
    assert_unowned_bus_name(get_bus(configuration.bus), bus_name)
 
345
 
 
346
    application = Application("landscape-%s" % (service_class.service_name,))
 
347
    service_class(configuration).setServiceParent(application)
 
348
 
 
349
    startApplication(application, False)
 
350
 
 
351
    if configuration.ignore_sigint:
 
352
        signal.signal(signal.SIGINT, signal.SIG_IGN)
 
353
 
 
354
    reactor.run()