~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/scripts/runlaunchpad.py

[r=sinzui][bug=855670] Add additional checks to the private team
        launchpad.LimitedView security adaptor so more users in defined
        roles can see the team.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 
2
# GNU Affero General Public License version 3 (see the file LICENSE).
 
3
 
 
4
# pylint: disable-msg=W0603
 
5
 
 
6
__metaclass__ = type
 
7
__all__ = ['start_launchpad']
 
8
 
 
9
 
 
10
from contextlib import nested
 
11
import os
 
12
import signal
 
13
import subprocess
 
14
import sys
 
15
 
 
16
import fixtures
 
17
from lazr.config import as_host_port
 
18
from rabbitfixture.server import RabbitServerResources
 
19
from testtools.testresult.real import _details_to_str
 
20
from zope.app.server.main import main
 
21
 
 
22
from canonical.config import config
 
23
from canonical.launchpad.daemons import tachandler
 
24
from canonical.lazr.pidfile import (
 
25
    make_pidfile,
 
26
    pidfile_path,
 
27
    )
 
28
from lp.services.googlesearch import googletestservice
 
29
from lp.services.mailman import runmailman
 
30
from lp.services.osutils import ensure_directory_exists
 
31
from lp.services.rabbit.server import RabbitServer
 
32
from lp.services.txlongpoll.server import TxLongPollServer
 
33
 
 
34
 
 
35
def make_abspath(path):
 
36
    return os.path.abspath(os.path.join(config.root, *path.split('/')))
 
37
 
 
38
 
 
39
class Service(fixtures.Fixture):
 
40
 
 
41
    @property
 
42
    def should_launch(self):
 
43
        """Return true if this service should be launched by default."""
 
44
        return False
 
45
 
 
46
    def launch(self):
 
47
        """Run the service in a thread or external process.
 
48
 
 
49
        May block long enough to kick it off, but must return control to
 
50
        the caller without waiting for it to shutdown.
 
51
        """
 
52
        raise NotImplementedError
 
53
 
 
54
    def setUp(self):
 
55
        super(Service, self).setUp()
 
56
        self.launch()
 
57
 
 
58
 
 
59
class TacFile(Service):
 
60
 
 
61
    def __init__(self, name, tac_filename, section_name, pre_launch=None):
 
62
        """Create a TacFile object.
 
63
 
 
64
        :param name: A short name for the service. Used to name the pid file.
 
65
        :param tac_filename: The location of the TAC file, relative to this
 
66
            script.
 
67
        :param section_name: The config section name that provides the
 
68
            launch, logfile and spew options.
 
69
        :param pre_launch: A callable that is called before the launch
 
70
            process.
 
71
        """
 
72
        super(TacFile, self).__init__()
 
73
        self.name = name
 
74
        self.tac_filename = tac_filename
 
75
        self.section_name = section_name
 
76
        if pre_launch is None:
 
77
            self.pre_launch = lambda: None
 
78
        else:
 
79
            self.pre_launch = pre_launch
 
80
 
 
81
    @property
 
82
    def should_launch(self):
 
83
        return (self.section_name is not None
 
84
                and config[self.section_name].launch)
 
85
 
 
86
    @property
 
87
    def logfile(self):
 
88
        """Return the log file to use.
 
89
 
 
90
        Default to the value of the configuration key logfile.
 
91
        """
 
92
        return config[self.section_name].logfile
 
93
 
 
94
    def launch(self):
 
95
        self.pre_launch()
 
96
 
 
97
        pidfile = pidfile_path(self.name)
 
98
        logfile = config[self.section_name].logfile
 
99
        tacfile = make_abspath(self.tac_filename)
 
100
 
 
101
        args = [
 
102
            tachandler.twistd_script,
 
103
            "--no_save",
 
104
            "--nodaemon",
 
105
            "--python", tacfile,
 
106
            "--pidfile", pidfile,
 
107
            "--prefix", self.name.capitalize(),
 
108
            "--logfile", logfile,
 
109
            ]
 
110
 
 
111
        if config[self.section_name].spew:
 
112
            args.append("--spew")
 
113
 
 
114
        # Note that startup tracebacks and evil programmers using 'print' will
 
115
        # cause output to our stdout. However, we don't want to have twisted
 
116
        # log to stdout and redirect it ourselves because we then lose the
 
117
        # ability to cycle the log files by sending a signal to the twisted
 
118
        # process.
 
119
        process = subprocess.Popen(args, stdin=subprocess.PIPE)
 
120
        self.addCleanup(stop_process, process)
 
121
        process.stdin.close()
 
122
 
 
123
 
 
124
class MailmanService(Service):
 
125
 
 
126
    @property
 
127
    def should_launch(self):
 
128
        return config.mailman.launch
 
129
 
 
130
    def launch(self):
 
131
        runmailman.start_mailman()
 
132
        self.addCleanup(runmailman.stop_mailman)
 
133
 
 
134
 
 
135
class CodebrowseService(Service):
 
136
 
 
137
    @property
 
138
    def should_launch(self):
 
139
        return False
 
140
 
 
141
    def launch(self):
 
142
        process = subprocess.Popen(
 
143
            ['make', 'run_codebrowse'],
 
144
            stdin=subprocess.PIPE)
 
145
        self.addCleanup(stop_process, process)
 
146
        process.stdin.close()
 
147
 
 
148
 
 
149
class GoogleWebService(Service):
 
150
 
 
151
    @property
 
152
    def should_launch(self):
 
153
        return config.google_test_service.launch
 
154
 
 
155
    def launch(self):
 
156
        self.addCleanup(stop_process, googletestservice.start_as_process())
 
157
 
 
158
 
 
159
class MemcachedService(Service):
 
160
    """A local memcached service for developer environments."""
 
161
 
 
162
    @property
 
163
    def should_launch(self):
 
164
        return config.memcached.launch
 
165
 
 
166
    def launch(self):
 
167
        cmd = [
 
168
            'memcached',
 
169
            '-m', str(config.memcached.memory_size),
 
170
            '-l', str(config.memcached.address),
 
171
            '-p', str(config.memcached.port),
 
172
            '-U', str(config.memcached.port),
 
173
            ]
 
174
        if config.memcached.verbose:
 
175
            cmd.append('-vv')
 
176
        else:
 
177
            cmd.append('-v')
 
178
        process = subprocess.Popen(cmd, stdin=subprocess.PIPE)
 
179
        self.addCleanup(stop_process, process)
 
180
        process.stdin.close()
 
181
 
 
182
 
 
183
class ForkingSessionService(Service):
 
184
    """A lp-forking-service for handling codehosting access."""
 
185
 
 
186
    # TODO: The "sftp" (aka codehosting) server depends fairly heavily on this
 
187
    #       service. It would seem reasonable to make one always start if the
 
188
    #       other one is started. Though this might be a way to "FeatureFlag"
 
189
    #       whether this is active or not.
 
190
    @property
 
191
    def should_launch(self):
 
192
        return (config.codehosting.launch and
 
193
                config.codehosting.use_forking_daemon)
 
194
 
 
195
    @property
 
196
    def logfile(self):
 
197
        """Return the log file to use.
 
198
 
 
199
        Default to the value of the configuration key logfile.
 
200
        """
 
201
        return config.codehosting.forker_logfile
 
202
 
 
203
    def launch(self):
 
204
        # Following the logic in TacFile. Specifically, if you configure sftp
 
205
        # to not run (and thus bzr+ssh) then we don't want to run the forking
 
206
        # service.
 
207
        if not self.should_launch:
 
208
            return
 
209
        from lp.codehosting import get_bzr_path
 
210
        command = [config.root + '/bin/py', get_bzr_path(),
 
211
                   'launchpad-forking-service',
 
212
                   '--path', config.codehosting.forking_daemon_socket,
 
213
                  ]
 
214
        env = dict(os.environ)
 
215
        env['BZR_PLUGIN_PATH'] = config.root + '/bzrplugins'
 
216
        logfile = self.logfile
 
217
        if logfile == '-':
 
218
            # This process uses a different logging infrastructure from the
 
219
            # rest of the Launchpad code. As such, it cannot trivially use '-'
 
220
            # as the logfile. So we just ignore this setting.
 
221
            pass
 
222
        else:
 
223
            env['BZR_LOG'] = logfile
 
224
        process = subprocess.Popen(command, env=env, stdin=subprocess.PIPE)
 
225
        self.addCleanup(stop_process, process)
 
226
        process.stdin.close()
 
227
 
 
228
 
 
229
class RabbitService(Service):
 
230
    """A RabbitMQ service."""
 
231
 
 
232
    @property
 
233
    def should_launch(self):
 
234
        return config.rabbitmq.launch
 
235
 
 
236
    def launch(self):
 
237
        hostname, port = as_host_port(config.rabbitmq.host, None, None)
 
238
        self.server = RabbitServer(
 
239
            RabbitServerResources(hostname=hostname, port=port))
 
240
        self.useFixture(self.server)
 
241
 
 
242
 
 
243
class TxLongPollService(Service):
 
244
    """A TxLongPoll service."""
 
245
 
 
246
    @property
 
247
    def should_launch(self):
 
248
        return config.txlongpoll.launch
 
249
 
 
250
    def launch(self):
 
251
        twistd_bin = os.path.join(
 
252
            config.root, 'bin', 'twistd-for-txlongpoll')
 
253
        broker_hostname, broker_port = as_host_port(
 
254
            config.rabbitmq.host, None, None)
 
255
        self.server = TxLongPollServer(
 
256
            twistd_bin=twistd_bin,
 
257
            frontend_port=config.txlongpoll.frontend_port,
 
258
            broker_user=config.rabbitmq.userid,
 
259
            broker_password=config.rabbitmq.password,
 
260
            broker_vhost=config.rabbitmq.virtual_host,
 
261
            broker_host=broker_hostname,
 
262
            broker_port=broker_port)
 
263
        self.useFixture(self.server)
 
264
 
 
265
 
 
266
def stop_process(process):
 
267
    """kill process and BLOCK until process dies.
 
268
 
 
269
    :param process: An instance of subprocess.Popen.
 
270
    """
 
271
    if process.poll() is None:
 
272
        os.kill(process.pid, signal.SIGTERM)
 
273
        process.wait()
 
274
 
 
275
 
 
276
def prepare_for_librarian():
 
277
    if not os.path.isdir(config.librarian_server.root):
 
278
        os.makedirs(config.librarian_server.root, 0700)
 
279
 
 
280
 
 
281
SERVICES = {
 
282
    'librarian': TacFile('librarian', 'daemons/librarian.tac',
 
283
                         'librarian_server', prepare_for_librarian),
 
284
    'sftp': TacFile('sftp', 'daemons/sftp.tac', 'codehosting'),
 
285
    'forker': ForkingSessionService(),
 
286
    'mailman': MailmanService(),
 
287
    'codebrowse': CodebrowseService(),
 
288
    'google-webservice': GoogleWebService(),
 
289
    'memcached': MemcachedService(),
 
290
    'rabbitmq': RabbitService(),
 
291
    'txlongpoll': TxLongPollService(),
 
292
    }
 
293
 
 
294
 
 
295
def get_services_to_run(requested_services):
 
296
    """Return a list of services (TacFiles) given a list of service names.
 
297
 
 
298
    If no names are given, then the list of services to run comes from the
 
299
    launchpad configuration.
 
300
 
 
301
    If names are given, then only run the services matching those names.
 
302
    """
 
303
    if len(requested_services) == 0:
 
304
        return [svc for svc in SERVICES.values() if svc.should_launch]
 
305
    return [SERVICES[name] for name in requested_services]
 
306
 
 
307
 
 
308
def split_out_runlaunchpad_arguments(args):
 
309
    """Split the given command-line arguments into services to start and Zope
 
310
    arguments.
 
311
 
 
312
    The runlaunchpad script can take an optional '-r services,...' argument.
 
313
    If this argument is present, then the value is returned as the first
 
314
    element of the return tuple. The rest of the arguments are returned as the
 
315
    second element of the return tuple.
 
316
 
 
317
    Returns a tuple of the form ([service_name, ...], remaining_argv).
 
318
    """
 
319
    if len(args) > 1 and args[0] == '-r':
 
320
        return args[1].split(','), args[2:]
 
321
    return [], args
 
322
 
 
323
 
 
324
def process_config_arguments(args):
 
325
    """Process the arguments related to the config.
 
326
 
 
327
    -i  Will set the instance name aka LPCONFIG env.
 
328
 
 
329
    If there is no ZConfig file passed, one will add to the argument
 
330
    based on the selected instance.
 
331
    """
 
332
    if '-i' in args:
 
333
        index = args.index('-i')
 
334
        config.setInstance(args[index + 1])
 
335
        del args[index:index + 2]
 
336
 
 
337
    if '-C' not in args:
 
338
        zope_config_file = config.zope_config_file
 
339
        if not os.path.isfile(zope_config_file):
 
340
            raise ValueError(
 
341
                "Cannot find ZConfig file for instance %s: %s" % (
 
342
                    config.instance_name, zope_config_file))
 
343
        args.extend(['-C', zope_config_file])
 
344
    return args
 
345
 
 
346
 
 
347
def start_testapp(argv=list(sys.argv)):
 
348
    from canonical.config.fixture import ConfigUseFixture
 
349
    from canonical.testing.layers import (
 
350
        BaseLayer,
 
351
        DatabaseLayer,
 
352
        LayerProcessController,
 
353
        LibrarianLayer,
 
354
        RabbitMQLayer,
 
355
        )
 
356
    from lp.testing.pgsql import (
 
357
        installFakeConnect,
 
358
        uninstallFakeConnect,
 
359
        )
 
360
    assert config.instance_name.startswith('testrunner-appserver'), (
 
361
        '%r does not start with "testrunner-appserver"' %
 
362
        config.instance_name)
 
363
    interactive_tests = 'INTERACTIVE_TESTS' in os.environ
 
364
    teardowns = []
 
365
 
 
366
    def setup():
 
367
        # This code needs to be run after other zcml setup happens in
 
368
        # runlaunchpad, so it is passed in as a callable.  We set up layers
 
369
        # here because we need to control fixtures within this process, and
 
370
        # because we want interactive tests to be as similar as possible to
 
371
        # tests run in the testrunner.
 
372
        # Note that this changes the config instance-name, with the result
 
373
        # that the configuration of utilities may become invalidated.
 
374
        # XXX Robert Collins, bug=883980: In short, we should derive the
 
375
        # other services from the test runner, rather than duplicating
 
376
        # the work of test setup within the slave appserver. That will
 
377
        # permit reuse of the librarian, DB, rabbit etc, and
 
378
        # correspondingly easier assertions and inspection of interactions
 
379
        # with other services. That would mean we do not need to set up rabbit
 
380
        # or the librarian here: the test runner would control and take care
 
381
        # of that.
 
382
        BaseLayer.setUp()
 
383
        teardowns.append(BaseLayer.tearDown)
 
384
        RabbitMQLayer.setUp()
 
385
        teardowns.append(RabbitMQLayer.tearDown)
 
386
        # We set up the database here even for the test suite because we want
 
387
        # to be able to control the database here in the subprocess.  It is
 
388
        # possible to do that when setting the database up in the parent
 
389
        # process, but it is messier.  This is simple.
 
390
        installFakeConnect()
 
391
        teardowns.append(uninstallFakeConnect)
 
392
        DatabaseLayer.setUp()
 
393
        teardowns.append(DatabaseLayer.tearDown)
 
394
        # The Librarian needs access to the database, so setting it up here
 
395
        # where we are setting up the database makes the most sense.
 
396
        LibrarianLayer.setUp()
 
397
        teardowns.append(LibrarianLayer.tearDown)
 
398
        # Switch to the appserver config.
 
399
        fixture = ConfigUseFixture(BaseLayer.appserver_config_name)
 
400
        fixture.setUp()
 
401
        teardowns.append(fixture.cleanUp)
 
402
        # Interactive tests always need this.  We let functional tests use
 
403
        # a local one too because of simplicity.
 
404
        LayerProcessController.startSMTPServer()
 
405
        teardowns.append(LayerProcessController.stopSMTPServer)
 
406
        if interactive_tests:
 
407
            root_url = config.appserver_root_url()
 
408
            print '*' * 70
 
409
            print 'In a few seconds, go to ' + root_url + '/+yuitest'
 
410
            print '*' * 70
 
411
    try:
 
412
        start_launchpad(argv, setup)
 
413
    finally:
 
414
        teardowns.reverse()
 
415
        for teardown in teardowns:
 
416
            try:
 
417
                teardown()
 
418
            except NotImplementedError:
 
419
                # We are in a separate process anyway.  Bah.
 
420
                pass
 
421
 
 
422
 
 
423
def start_launchpad(argv=list(sys.argv), setup=None):
 
424
    # We really want to replace this with a generic startup harness.
 
425
    # However, this should last us until this is developed
 
426
    services, argv = split_out_runlaunchpad_arguments(argv[1:])
 
427
    argv = process_config_arguments(argv)
 
428
    services = get_services_to_run(services)
 
429
    # Create the ZCML override file based on the instance.
 
430
    config.generate_overrides()
 
431
    # Many things rely on a directory called 'logs' existing in the current
 
432
    # working directory.
 
433
    ensure_directory_exists('logs')
 
434
    if setup is not None:
 
435
        # This is the setup from start_testapp, above.
 
436
        setup()
 
437
    try:
 
438
        with nested(*services):
 
439
            # Store our process id somewhere
 
440
            make_pidfile('launchpad')
 
441
            if config.launchpad.launch:
 
442
                main(argv)
 
443
            else:
 
444
                # We just need the foreground process to sit around forever
 
445
                # waiting for the signal to shut everything down.  Normally,
 
446
                # Zope itself would be this master process, but we're not
 
447
                # starting that up, so we need to do something else.
 
448
                try:
 
449
                    signal.pause()
 
450
                except KeyboardInterrupt:
 
451
                    pass
 
452
    except Exception, e:
 
453
        print >> sys.stderr, "stopping services on exception %r" % e
 
454
        for service in services:
 
455
            print >> sys.stderr, service, "fixture details:"
 
456
            # There may be no details on some services if they haven't been
 
457
            # initialized yet.
 
458
            if getattr(service, '_details', None) is None:
 
459
                print >> sys.stderr, "(not ready yet?)"
 
460
                continue
 
461
            details_str = _details_to_str(service.getDetails())
 
462
            if details_str:
 
463
                print >> sys.stderr, details_str
 
464
            else:
 
465
                print >> sys.stderr, "(no details present)"
 
466
        raise
 
467
 
 
468
 
 
469
def start_librarian():
 
470
    """Start the Librarian in the background."""
 
471
    # Create the ZCML override file based on the instance.
 
472
    config.generate_overrides()
 
473
    # Create the Librarian storage directory if it doesn't already exist.
 
474
    prepare_for_librarian()
 
475
    pidfile = pidfile_path('librarian')
 
476
    cmd = [
 
477
        tachandler.twistd_script,
 
478
        "--python", 'daemons/librarian.tac',
 
479
        "--pidfile", pidfile,
 
480
        "--prefix", 'Librarian',
 
481
        "--logfile", config.librarian_server.logfile,
 
482
        ]
 
483
    return subprocess.call(cmd)