5
from logging import (getLevelName, getLogger,
6
FileHandler, StreamHandler, Formatter, info)
8
from optparse import OptionParser
9
from ConfigParser import ConfigParser
11
import dbus.glib # Side-effects rule!
13
from twisted.application.service import Application, Service
14
from twisted.application.app import startApplication
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
23
from landscape.upgraders import UPGRADE_MANAGERS
26
def init_logging(configuration, program_name):
27
"""Given a basic configuration, set up logging."""
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] "
40
handler.setFormatter(Formatter(format))
43
class BaseConfiguration(object):
44
"""Base class for configuration implementations.
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.
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"
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()
76
def __getattr__(self, name):
77
"""Find and return the value of the given configuration parameter.
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
85
If no values are found and the parameter does exist as a possible
86
parameter, C{None} is returned.
88
Otherwise C{AttributeError} is raised.
90
for options in [self._set_options,
91
self._command_line_options,
92
self._config_file_options,
93
self._command_line_defaults]:
98
if self._parser.has_option("--" + name.replace("_", "-")):
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)
108
def get(self, name, default=None):
110
return self.__getattr__(name)
111
except AttributeError:
114
def __setattr__(self, name, value):
115
"""Set a configuration parameter.
117
If the name begins with C{_}, it will only be set on this object and
118
not stored in the configuration file.
120
if name.startswith("_"):
121
super(BaseConfiguration, self).__setattr__(name, value)
123
self._set_options[name] = value
126
self.load(self._command_line_args)
128
def load(self, args, accept_unexistent_config=False):
130
Load configuration data from command line arguments and a config file.
132
@raise: A SystemExit if the arguments are bad.
134
self.load_command_line(args)
136
# Parse configuration file, if found.
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)
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)
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))
155
if self.bus not in ("session", "system"):
156
sys.exit("error: bus must be one of 'session' or 'system'")
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)
164
def load_configuration_file(self, filename):
165
"""Load configuration data from the given file name.
167
If any data has already been set on this configuration object,
168
then the old data will take precedence.
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))
177
"""Write back configuration to the configuration file.
179
Values which match the default option in the parser won't be saved.
181
Options are considered in the following precedence:
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
187
The filename picked for saving configuration options is:
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
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)
209
def make_parser(self):
211
Return an L{OptionParser} preset with options that all
212
landscape-related programs accept.
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' "
223
def get_config_filename(self):
224
return self._config_filename
226
def get_command_line_options(self):
227
return self._command_line_options
230
class Configuration(BaseConfiguration):
231
"""Configuration data for Landscape client.
233
This contains all simple data, some of it calculated.
237
def hashdb_filename(self):
238
return os.path.join(self.data_path, "hash.db")
240
def make_parser(self):
242
Return an L{OptionParser} preset with options that all
243
landscape-related programs accept.
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 "
257
parser.add_option("--ignore-sigint", action="store_true", default=False,
258
help="Ignore interrupt signals. ")
263
def get_versioned_persist(service):
265
Load a Persist database for the given service and upgrade or mark
266
as current, as necessary.
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)
273
upgrade_manager.initialize(persist)
274
persist.save(service.persist_filename)
278
class LandscapeService(Service, object):
280
A utility superclass for defining Landscape services.
282
This sets up the reactor, bpickle/dbus integration, a Persist object, and
283
connects to the bus when started.
285
@cvar service_name: The lower-case name of the service. This is used to
286
generate the bpickle filename.
288
reactor_factory = TwistedReactor
289
persist_filename = None
291
def __init__(self, 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())
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()))
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()))
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)
322
def run_landscape_service(configuration_class, service_class, args, bus_name):
323
"""Run a Landscape service.
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
332
from twisted.internet.glib2reactor import install
334
from twisted.internet import reactor
336
# Let's consider adding this:
337
# from twisted.python.log import startLoggingWithObserver, PythonLoggingObserver
338
# startLoggingWithObserver(PythonLoggingObserver().emit, setStdout=False)
340
configuration = configuration_class()
341
configuration.load(args)
342
init_logging(configuration, service_class.service_name)
344
assert_unowned_bus_name(get_bus(configuration.bus), bus_name)
346
application = Application("landscape-%s" % (service_class.service_name,))
347
service_class(configuration).setServiceParent(application)
349
startApplication(application, False)
351
if configuration.ignore_sigint:
352
signal.signal(signal.SIGINT, signal.SIG_IGN)