~free.ekanayaka/landscape-client/config-failure

« back to all changes in this revision

Viewing changes to landscape/configuration.py

  • Committer: Free Ekanayaka
  • Date: 2010-04-28 13:38:56 UTC
  • mfrom: (182.1.30 amp-trunk)
  • Revision ID: free.ekanayaka@canonical.com-20100428133856-wo697ypl3e96cd6i
Merge amp-trunk [trivial]

Migrate our inter-process communication system from D-Bus to a Twisted AMP-based
protocol.

The change has been splitted into serveral branches that have been reviewed
separately in the LP bugs 570763, 570192, 568983, 568499, 567152, 564587,
564620, 562330, 561471, 559312, 558455, 556603, 552414, 546825, 544070, 539658,
537538, 535227, 499176, 531480, 499121, 514250, 499225 and 499018.

For sake of convenience here follows a summary of the changes introduced by
each branch:

  - Cleanup for having the base classes for monitor and manager plugins
    in landscape.monitor.plugin and landscape.manager.plugin.
  
  - Install the dbus bpickle extensions when starting a LandscapeService, letting
    plugins like the HAL/hardware one send dbus data types over AMP.
  
  - Make the monitor and manager test cases inherit from LandscapeTest instead of
    LandscapeIsolatedTest, as they don't need DBus anymore.
  
  - Make the landscape-config script use the AMP-based protocol to communicate
    with the broker. 

 -  Change the MethodCall protocol to split arguments in chunks of
    64k and transparently send them over several AMP commands.

 -  Make all task handlers derived from PackageTaskHandlers use the AMP-based
    protocol instead of D-Bus to communicate with the broker.
  
  - Port the behavior introduced in Bug #542215 to the AMP-based
    BrokerServer, which now broadcasts package-data-changed events.
  
  - Make the BrokerServiceHelper provide a 'live' RemoteBroker instead
    of a FakeRemoteBroker. The former BrokerServiceHelper has been renamed
    to FakeBrokerServiceHelper.
  
  - Migrate the watchdog to AMP, replacing the existing DBus-based
    communication mechanism with the AMP-based one.

  - Change is in the shutdown logic in the watchdog to be fully asynchronous,
    while before was relying on DBus being synchronous.
  
  - Add a 'factor' parameter to the RemoteComponentConnector.connect
    mehtod, for setting the pace at which service will try to reconnect.
  
  - Make services pass "wantPID" to the reactor.listenUNIX method, which
    cleans up left-over unix sockets on the filesystem (e.g. the former
    process died).
  
  - Change the landscape-{broker,monitor,manager} scripts to use the AMP-based versions
    of the associated services.
  
  - Move the BrokerConfiguration from landscape.broker.deployment to landscape.broker.config,
    for consistency with the monitor and the manager equivalent classes.

  - Replace the DBus-based MonitorHelper and ManagerHelper with their
    AMP-based equivalent.
  
  - Make the BrokerServer subscribe to the exchanger events, like "message",
   "impending-exchange", "exchange-failed", "registration-done", etc. and
    notifiy all connected clients about them.
        
  - Convert the usermanager and usermonitor communication mechanism from
    D-Bus to our AMP-based protocol.
  
  - Add a ManagerService class that takes care of starting the manager
    and make it listen to the correct Unix socket for incoming AMP
    connections.
 
  - Add a MonitorService class that takes care of starting the monitor
    and make it listen to the correct Unix socket for incoming AMP
    connections.

  - Add a BrokerService class that starts listening for incoming AMP connections
    on a Unix socket.
  
  - Add a BrokerClientProtocol for exposing the methods of a broker client
    to the broker server.

  - Decouple the broker client API logic from DBus, adding a new BrokerClient
    class implementing the behavior the broker expects for its clients.

  - Add a BrokerServerProtocol class that can be used by the broker
    clients (monitor and manager) to perform remote method calls
    on the BrokerServer.

  - Add a MethodCall-based protocol for basic communication between the
    various Landscape components.
  
  - Add a BrokerServer class implementing the same methods and
    interfaces of BrokerDBusObject, but not publishing them on
    DBus. The methods of this class will be exposed to the broker
    clients (the monitor and the manager) by an AMP-based protocol.

  - Add a reconnection mechanism to the MethodCall protocol makes it:
  
    * Keep trying to connect if the very fist attempt failed (for instance the
      remote process we're trying to contact is not yet ready to accept
      connections).
  
    * Try to reconnect if the connection drops (for instance the remote
      process died and the watchdog started it again).
  
    * Try to re-perform a failed MethodCall request as soon as the connection
      is up again, in case it failed because it was issued during a connection 
      blackout (for instance a monitor plugin tries to call a 
      RemoteBroker.send_message while the broker is restarting).
  
  - Make the MethodCall protocol able to cope with remote methods returning
    deferreds.
  
  - Add an AMP-based protocol to easily expose objects over AMP and perform
    remote method calls on them.

Show diffs side-by-side

added added

removed removed

Lines of Context:
13
13
from ConfigParser import ConfigParser, Error as ConfigParserError
14
14
from StringIO import StringIO
15
15
 
16
 
from dbus.exceptions import DBusException
17
 
 
18
16
from landscape.lib.tag import is_valid_tag
19
17
 
20
18
from landscape.sysvconfig import SysVConfig, ProcessError
21
 
from landscape.lib.dbus_util import (
22
 
    get_bus, NoReplyError, ServiceUnknownError, SecurityError)
 
19
from landscape.lib.amp import MethodCallError
23
20
from landscape.lib.twisted_util import gather_results
24
21
from landscape.lib.fetch import fetch, FetchError
25
 
 
 
22
from landscape.reactor import TwistedReactor
26
23
from landscape.broker.registration import InvalidCredentialsError
27
 
from landscape.broker.deployment import BrokerConfiguration
28
 
from landscape.broker.remote import RemoteBroker
 
24
from landscape.broker.config import BrokerConfiguration
 
25
from landscape.broker.amp import RemoteBrokerConnector
29
26
 
30
27
 
31
28
class ConfigurationError(Exception):
32
29
    """Raised when required configuration values are missing."""
33
30
 
 
31
 
34
32
class ImportOptionError(ConfigurationError):
35
33
    """Raised when there are issues with handling the --import option."""
36
34
 
40
38
        stream = sys.stderr
41
39
    else:
42
40
        stream = sys.stdout
43
 
    stream.write(text+end)
 
41
    stream.write(text + end)
44
42
    stream.flush()
45
43
 
46
44
 
120
118
        parser.add_option("--import", dest="import_from",
121
119
                          metavar="FILENAME_OR_URL",
122
120
                          help="Filename or URL to import configuration from. "
123
 
                               "Imported options behave as if they were passed "
124
 
                               "in the command line, with precedence being "
125
 
                               "given to real command line options.")
 
121
                               "Imported options behave as if they were "
 
122
                               "passed in the command line, with precedence "
 
123
                               "being given to real command line options.")
126
124
        parser.add_option("--script-users", metavar="USERS",
127
125
                          help="A comma-separated list of users to allow "
128
126
                               "scripts to run.  To allow scripts to be run "
161
159
 
162
160
    def show_help(self, text):
163
161
        lines = text.strip().splitlines()
164
 
        print_text("\n"+"".join([line.strip()+"\n" for line in lines]))
 
162
        print_text("\n" + "".join([line.strip() + "\n" for line in lines]))
165
163
 
166
164
    def prompt_get_input(self, msg, required):
167
165
        """Prompt the user on the terminal for a value
215
213
                value2 = getpass.getpass("Please confirm: ")
216
214
            if value:
217
215
                if value != value2:
218
 
                   self.show_help("Passwords must match.")
 
216
                    self.show_help("Passwords must match.")
219
217
                else:
220
218
                    setattr(self.config, option, value)
221
219
                    break
359
357
        """
360
358
        invalid_tags = []
361
359
        if tagnames:
362
 
           tags  = [tag.strip() for tag in tagnames.split(",")]
363
 
           invalid_tags = [tag for tag in tags if not is_valid_tag(tag)]
 
360
            tags = [tag.strip() for tag in tagnames.split(",")]
 
361
            invalid_tags = [tag for tag in tags if not is_valid_tag(tag)]
364
362
        return invalid_tags
365
363
 
366
364
    def query_tags(self):
378
376
        while True:
379
377
            self.prompt("tags", "Tags", False)
380
378
            if self._get_invalid_tags(self.config.tags):
381
 
               self.show_help("Tag names may only contain alphanumeric "
 
379
                self.show_help("Tag names may only contain alphanumeric "
382
380
                              "characters.")
383
 
               self.config.tags = None # Reset for the next prompt
 
381
                self.config.tags = None # Reset for the next prompt
384
382
            else:
385
383
                break
386
384
 
490
488
        have totally mangled everything with mocker.  Otherwise bad things
491
489
        will happen.
492
490
    """
493
 
    from landscape.reactor import install
494
 
    install()
495
 
    if reactor is None:
496
 
        from twisted.internet import reactor
 
491
    reactor = TwistedReactor()
497
492
 
498
493
    # XXX: many of these reactor.stop() calls should also specify a non-0 exit
499
494
    # code, unless ok-no-register is passed.
500
495
 
 
496
    def stop():
 
497
        connector.disconnect()
 
498
        # For some obscure reason our TwistedReactor.stop method calls
 
499
        # reactor.crash() instead of reactor.stop(), which doesn't work
 
500
        # here. Maybe TwistedReactor.stop should simply use reactor.stop().
 
501
        reactor.call_later(0, reactor._reactor.stop)
 
502
 
501
503
    def failure():
502
504
        print_text("Invalid account name or "
503
505
                   "registration password.", error=True)
504
 
        reactor.stop()
 
506
        stop()
505
507
 
506
508
    def success():
507
509
        print_text("System successfully registered.")
508
 
        reactor.stop()
 
510
        stop()
509
511
 
510
512
    def exchange_failure():
511
513
        print_text("We were unable to contact the server. "
513
515
                   "The landscape client will continue to try and contact "
514
516
                   "the server periodically.",
515
517
                   error=True)
516
 
        reactor.stop()
 
518
        stop()
517
519
 
518
520
    def handle_registration_errors(failure):
519
521
        # We'll get invalid credentials through the signal.
520
 
        error = failure.trap(InvalidCredentialsError, NoReplyError)
521
 
        # This event is fired here so we can catch this case where
522
 
        # there is no reply in a test.  In the normal case when
523
 
        # running the client there is no trigger added for this event
524
 
        # and it is essentially a noop.
525
 
        reactor.fireSystemEvent("landscape-registration-error")
 
522
        failure.trap(InvalidCredentialsError, MethodCallError)
 
523
        connector.disconnect()
526
524
 
527
525
    def catch_all(failure):
528
526
        # We catch SecurityError here too, because on some DBUS configurations
529
527
        # if you try to connect to a dbus name that doesn't have a listener,
530
528
        # it'll try auto-starting the service, but then the StartServiceByName
531
529
        # call can raise a SecurityError.
532
 
        if failure.check(ServiceUnknownError, SecurityError):
533
 
            print_text("Error occurred contacting Landscape Client. "
534
 
                       "Is it running?", error=True)
535
 
        else:
536
 
            print_text(failure.getTraceback(), error=True)
537
 
            print_text("Unknown error occurred.", error=True)
538
 
        reactor.callLater(0, reactor.stop)
 
530
        print_text(failure.getTraceback(), error=True)
 
531
        print_text("Unknown error occurred.", error=True)
 
532
        stop()
539
533
 
540
534
    print_text("Please wait... ", "")
541
535
 
542
536
    time.sleep(2)
543
 
    try:
544
 
        remote = RemoteBroker(get_bus(config.bus), retry_timeout=0)
545
 
    except DBusException:
546
 
        print_text("There was an error communicating with the Landscape client "
547
 
                   "via DBus.", error=True)
 
537
 
 
538
    def got_connection(remote):
 
539
        handlers = {"registration-done": success,
 
540
                    "registration-failed": failure,
 
541
                    "exchange-failed": exchange_failure}
 
542
        deferreds = [
 
543
            remote.reload_configuration(),
 
544
            remote.call_on_event(handlers),
 
545
            remote.register().addErrback(handle_registration_errors)]
 
546
        # We consume errors here to ignore errors after the first one.
 
547
        # catch_all will be called for the very first deferred that fails.
 
548
 
 
549
        results = gather_results(deferreds, consume_errors=True)
 
550
        return results.addErrback(catch_all)
 
551
 
 
552
    def got_error(failure):
 
553
        print_text("There was an error communicating with the Landscape "
 
554
                   "client.", error=True)
548
555
        print_text("This machine will be registered with the provided "
549
556
                   "details when the client runs.", error=True)
550
557
        exit_code = 2
551
558
        if config.ok_no_register:
552
559
            exit_code = 0
553
560
        sys.exit(exit_code)
554
 
    # This is a bit unfortunate. Every method of remote returns a deferred,
555
 
    # even stuff like connect_to_signal, because the fetching of the DBus
556
 
    # object itself is asynchronous. We can *mostly* fire-and-forget these
557
 
    # things, except that if the object isn't found, *all* of the deferreds
558
 
    # will fail. To prevent unhandled errors, we need to collect them all up
559
 
    # and add an errback.
560
 
    deferreds = [
561
 
        remote.reload_configuration(),
562
 
        remote.connect_to_signal("registration_done", success),
563
 
        remote.connect_to_signal("registration_failed", failure),
564
 
        remote.connect_to_signal("exchange_failed", exchange_failure),
565
 
        remote.register().addErrback(handle_registration_errors)]
566
 
    # We consume errors here to ignore errors after the first one. catch_all
567
 
    # will be called for the very first deferred that fails.
568
 
    gather_results(deferreds, consume_errors=True).addErrback(catch_all)
 
561
 
 
562
    connector = RemoteBrokerConnector(reactor, config)
 
563
    result = connector.connect(max_retries=0, quiet=True)
 
564
    result.addCallback(got_connection)
 
565
    result.addErrback(got_error)
 
566
 
569
567
    reactor.run()
570
568
 
 
569
    return result
 
570
 
571
571
 
572
572
def fetch_import_url(url):
573
573
    """Handle fetching of URLs passed to --url.