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

« back to all changes in this revision

Viewing changes to landscape/configuration.py

  • Committer: Bazaar Package Importer
  • Author(s): Rick Clark
  • Date: 2008-09-08 16:35:57 UTC
  • mfrom: (1.1.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080908163557-l3ixzj5dxz37wnw2
Tags: 1.0.18-0ubuntu1
New upstream release 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""Interactive configuration support for Landscape.
 
2
 
 
3
This module, and specifically L{LandscapeSetupScript}, implements the support
 
4
for the C{landscape-config} script.
 
5
"""
 
6
 
 
7
import time
 
8
import sys
 
9
import os
 
10
import getpass
 
11
 
 
12
from landscape.sysvconfig import SysVConfig, ProcessError
 
13
from landscape.lib.dbus_util import (
 
14
    get_bus, NoReplyError, ServiceUnknownError, SecurityError)
 
15
from landscape.lib.twisted_util import gather_results
 
16
 
 
17
from landscape.broker.registration import InvalidCredentialsError
 
18
from landscape.broker.deployment import BrokerConfiguration
 
19
from landscape.broker.remote import RemoteBroker
 
20
 
 
21
 
 
22
class ConfigurationError(Exception):
 
23
    """Raised when required configuration values are missing."""
 
24
 
 
25
 
 
26
def print_text(text, end="\n", error=False):
 
27
    if error:
 
28
        stream = sys.stderr
 
29
    else:
 
30
        stream = sys.stdout
 
31
    stream.write(text+end)
 
32
    stream.flush()
 
33
 
 
34
 
 
35
class LandscapeSetupConfiguration(BrokerConfiguration):
 
36
 
 
37
    unsaved_options = ("no_start", "disable", "silent")
 
38
 
 
39
    def make_parser(self):
 
40
        """
 
41
        Specialize L{Configuration.make_parser}, adding many
 
42
        broker-specific options.
 
43
        """
 
44
        parser = super(LandscapeSetupConfiguration, self).make_parser()
 
45
 
 
46
        parser.add_option("--script-users", metavar="USERS",
 
47
                          help="A comma-separated list of users to allow "
 
48
                               "scripts to run.  To allow scripts to be run "
 
49
                               "by any user, enter: ALL")
 
50
        parser.add_option("--include-manager-plugins", metavar="PLUGINS",
 
51
                          default="",
 
52
                          help="A comma-separated list of manager plugins to "
 
53
                               "load.")
 
54
        parser.add_option("-n", "--no-start", action="store_true",
 
55
                          help="Don't start the client automatically.")
 
56
        parser.add_option("--silent", action="store_true", default=False,
 
57
                          help="Run without manual interaction.")
 
58
        parser.add_option("--disable", action="store_true", default=False,
 
59
                          help="Stop running clients and disable start at "
 
60
                               "boot.")
 
61
        return parser
 
62
 
 
63
 
 
64
class LandscapeSetupScript(object):
 
65
    """
 
66
    An interactive procedure which manages the prompting and temporary storage
 
67
    of configuration parameters.
 
68
 
 
69
    Various attributes on this object will be set on C{config} after L{run} is
 
70
    called.
 
71
 
 
72
    @ivar config: The L{BrokerConfiguration} object to read and set values from
 
73
        and to.
 
74
    """
 
75
 
 
76
    def __init__(self, config):
 
77
        self.config = config
 
78
 
 
79
    def show_help(self, text):
 
80
        lines = text.strip().splitlines()
 
81
        print_text("\n"+"".join([line.strip()+"\n" for line in lines]))
 
82
 
 
83
    def prompt(self, option, msg, required=False):
 
84
        """Prompt the user on the terminal for a value.
 
85
 
 
86
        @param option: The attribute of C{self.config} that contains the
 
87
            default and which the value will be assigned to.
 
88
        @param msg: The message to prompt the user with (via C{raw_input}).
 
89
        @param required: If True, the user will be required to enter a value
 
90
            before continuing.
 
91
        """
 
92
        default = getattr(self.config, option, None)
 
93
        if default:
 
94
            msg += " [%s]: " % default
 
95
        else:
 
96
            msg += ": "
 
97
        while True:
 
98
            value = raw_input(msg)
 
99
            if value:
 
100
                setattr(self.config, option, value)
 
101
                break
 
102
            elif default or not required:
 
103
                break
 
104
            self.show_help("This option is required to configure Landscape.")
 
105
 
 
106
    def password_prompt(self, option, msg, required=False):
 
107
        """Prompt the user on the terminal for a password and mask the value.
 
108
 
 
109
        This also prompts the user twice and errors if both values don't match.
 
110
 
 
111
        @param option: The attribute of C{self.config} that contains the
 
112
            default and which the value will be assigned to.
 
113
        @param msg: The message to prompt the user with (via C{raw_input}).
 
114
        @param required: If True, the user will be required to enter a value
 
115
            before continuing.
 
116
        """
 
117
        default = getattr(self.config, option, None)
 
118
        msg += ": "
 
119
        while True:
 
120
            value = getpass.getpass(msg)
 
121
            if value:
 
122
                value2 = getpass.getpass("Please confirm: ")
 
123
            if value:
 
124
                if value != value2:
 
125
                   self.show_help("Passwords must match.")
 
126
                else:
 
127
                    setattr(self.config, option, value)
 
128
                    break
 
129
            elif default or not required:
 
130
                break
 
131
            else:
 
132
                self.show_help("This option is required to configure "
 
133
                               "Landscape.")
 
134
 
 
135
    def prompt_yes_no(self, message, default=True):
 
136
        if default:
 
137
            default_msg = " [Y/n]"
 
138
        else:
 
139
            default_msg = " [y/N]"
 
140
        while True:
 
141
            value = raw_input(message + default_msg).lower()
 
142
            if value:
 
143
                if value.startswith("n"):
 
144
                    return False
 
145
                if value.startswith("y"):
 
146
                    return True
 
147
                self.show_help("Invalid input.")
 
148
            else:
 
149
                return default
 
150
 
 
151
    def query_computer_title(self):
 
152
        if "computer_title" in self.config.get_command_line_options():
 
153
            return
 
154
 
 
155
        self.show_help(
 
156
            """
 
157
            The computer title you provide will be used to represent this
 
158
            computer in the Landscape user interface. It's important to use
 
159
            a title that will allow the system to be easily recognized when
 
160
            it appears on the pending computers page.
 
161
            """)
 
162
 
 
163
        self.prompt("computer_title", "This computer's title", True)
 
164
 
 
165
    def query_account_name(self):
 
166
        if "account_name" in self.config.get_command_line_options():
 
167
            return
 
168
 
 
169
        self.show_help(
 
170
            """
 
171
            You must now specify the name of the Landscape account you
 
172
            want to register this computer with.  You can verify the
 
173
            names of the accounts you manage on your dashboard at
 
174
            https://landscape.canonical.com/dashboard
 
175
            """)
 
176
 
 
177
        self.prompt("account_name", "Account name", True)
 
178
 
 
179
    def query_registration_password(self):
 
180
        if "registration_password" in self.config.get_command_line_options():
 
181
            return
 
182
 
 
183
        self.show_help(
 
184
            """
 
185
            A registration password may be associated with your Landscape
 
186
            account to prevent unauthorized registration attempts.  This
 
187
            is not your personal login password.  It is optional, and unless
 
188
            explicitly set on the server, it may be skipped here.
 
189
 
 
190
            If you don't remember the registration password you can find it
 
191
            at https://landscape.canonical.com/account/%s
 
192
            """ % self.config.account_name)
 
193
 
 
194
        self.password_prompt("registration_password",
 
195
                             "Account registration password")
 
196
 
 
197
    def query_proxies(self):
 
198
        options = self.config.get_command_line_options()
 
199
        if "http_proxy" in options and "https_proxy" in options:
 
200
            return
 
201
 
 
202
        self.show_help(
 
203
            """
 
204
            The Landscape client communicates with the server over HTTP and
 
205
            HTTPS.  If your network requires you to use a proxy to access HTTP
 
206
            and/or HTTPS web sites, please provide the address of these
 
207
            proxies now.  If you don't use a proxy, leave these fields empty.
 
208
            """)
 
209
 
 
210
        if not "http_proxy" in options:
 
211
            self.prompt("http_proxy", "HTTP proxy URL")
 
212
        if not "https_proxy" in options:
 
213
            self.prompt("https_proxy", "HTTPS proxy URL")
 
214
 
 
215
    def query_script_plugin(self):
 
216
        options = self.config.get_command_line_options()
 
217
        if "include_manager_plugins" in options and "script_users" in options:
 
218
            return
 
219
 
 
220
        self.show_help(
 
221
            """
 
222
            Landscape has a feature which enables administrators to run
 
223
            arbitrary scripts on machines under their control. By default this
 
224
            feature is disabled in the client, disallowing any arbitrary script
 
225
            execution. If enabled, the set of users that scripts may run as is
 
226
            also configurable.
 
227
            """)
 
228
        msg = "Enable script execution?"
 
229
        included_plugins = [
 
230
            p.strip() for p in self.config.include_manager_plugins.split(",")]
 
231
        if included_plugins == [""]:
 
232
            included_plugins = []
 
233
        default = "ScriptExecution" in included_plugins
 
234
        if self.prompt_yes_no(msg, default=default):
 
235
            if "ScriptExecution" not in included_plugins:
 
236
                included_plugins.append("ScriptExecution")
 
237
            self.show_help(
 
238
                """
 
239
                By default, scripts are restricted to the 'landscape' and
 
240
                'nobody' users. Please enter a comma-delimited list of users
 
241
                that scripts will be restricted to. To allow scripts to be run
 
242
                by any user, enter "ALL".
 
243
                """)
 
244
            if not "script_users" in options:
 
245
                self.prompt("script_users", "Script users")
 
246
        else:
 
247
            if "ScriptExecution" in included_plugins:
 
248
                included_plugins.remove("ScriptExecution")
 
249
        self.config.include_manager_plugins = ", ".join(included_plugins)
 
250
 
 
251
    def show_header(self):
 
252
        self.show_help(
 
253
            """
 
254
            This script will interactively set up the Landscape client. It will
 
255
            ask you a few questions about this computer and your Landscape
 
256
            account, and will submit that information to the Landscape server.
 
257
            After this computer is registered it will need to be approved by an
 
258
            account administrator on the pending computers page.
 
259
 
 
260
            Please see https://landscape.canonical.com for more information.
 
261
            """)
 
262
 
 
263
    def run(self):
 
264
        """Kick off the interactive process which prompts the user for data.
 
265
 
 
266
        Data will be saved to C{self.config}.
 
267
        """
 
268
        self.show_header()
 
269
        self.query_computer_title()
 
270
        self.query_account_name()
 
271
        self.query_registration_password()
 
272
        self.query_proxies()
 
273
        self.query_script_plugin()
 
274
 
 
275
 
 
276
def setup_init_script_and_start_client():
 
277
    sysvconfig = SysVConfig()
 
278
    sysvconfig.set_start_on_boot(True)
 
279
 
 
280
 
 
281
def stop_client_and_disable_init_script():
 
282
    sysvconfig = SysVConfig()
 
283
    sysvconfig.stop_landscape()
 
284
    sysvconfig.set_start_on_boot(False)
 
285
 
 
286
 
 
287
def setup(config):
 
288
    sysvconfig = SysVConfig()
 
289
    if not config.no_start:
 
290
        if config.silent:
 
291
            setup_init_script_and_start_client()
 
292
        elif not sysvconfig.is_configured_to_run():
 
293
            answer = raw_input("\nThe Landscape client must be started "
 
294
                               "on boot to operate correctly.\n\n"
 
295
                               "Start Landscape client on boot? (Y/n): ")
 
296
            if not answer.upper().startswith("N"):
 
297
                setup_init_script_and_start_client()
 
298
            else:
 
299
                sys.exit("Aborting Landscape configuration")
 
300
 
 
301
    if config.http_proxy is None and os.environ.get("http_proxy"):
 
302
        config.http_proxy = os.environ["http_proxy"]
 
303
    if config.https_proxy is None and os.environ.get("https_proxy"):
 
304
        config.https_proxy = os.environ["https_proxy"]
 
305
 
 
306
    if config.silent:
 
307
        if not config.get("account_name") or not config.get("computer_title"):
 
308
            raise ConfigurationError("An account name and computer title are "
 
309
                                     "required.")
 
310
        if config.get("script_users") and not config.include_manager_plugins:
 
311
            config.include_manager_plugins = "ScriptExecution"
 
312
    else:
 
313
        script = LandscapeSetupScript(config)
 
314
        script.run()
 
315
 
 
316
    config.write()
 
317
    # Restart the client to ensure that it's using the new configuration.
 
318
    if not config.no_start:
 
319
        sysvconfig.restart_landscape()
 
320
 
 
321
 
 
322
def register(config, reactor=None):
 
323
    """Instruct the Landscape Broker to register the client.
 
324
 
 
325
    The broker will be instructed to reload its configuration and then to
 
326
    attempt a registration.
 
327
 
 
328
    @param reactor: The reactor to use.  Please only pass reactor when you
 
329
        have totally mangled everything with mocker.  Otherwise bad things
 
330
        will happen.
 
331
    """
 
332
    from twisted.internet.glib2reactor import install
 
333
    install()
 
334
    if reactor is None:
 
335
        from twisted.internet import reactor
 
336
 
 
337
    def failure():
 
338
        print_text("Invalid account name or "
 
339
                   "registration password.", error=True)
 
340
        reactor.stop()
 
341
 
 
342
    def success():
 
343
        print_text("System successfully registered.")
 
344
        reactor.stop()
 
345
 
 
346
    def exchange_failure():
 
347
        print_text("We were unable to contact the server. "
 
348
                   "Your internet connection may be down. "
 
349
                   "The landscape client will continue to try and contact "
 
350
                   "the server periodically.",
 
351
                   error=True)
 
352
        reactor.stop()
 
353
 
 
354
    def handle_registration_errors(failure):
 
355
        # We'll get invalid credentials through the signal.
 
356
        error = failure.trap(InvalidCredentialsError, NoReplyError)
 
357
        # This event is fired here so we can catch this case where
 
358
        # there is no reply in a test.  In the normal case when
 
359
        # running the client there is no trigger added for this event
 
360
        # and it is essentially a noop.
 
361
        reactor.fireSystemEvent("landscape-registration-error")
 
362
 
 
363
    def catch_all(failure):
 
364
        # We catch SecurityError here too, because on some DBUS configurations
 
365
        # if you try to connect to a dbus name that doesn't have a listener,
 
366
        # it'll try auto-starting the service, but then the StartServiceByName
 
367
        # call can raise a SecurityError.
 
368
        if failure.check(ServiceUnknownError, SecurityError):
 
369
            print_text("Error occurred contacting Landscape Client. "
 
370
                       "Is it running?", error=True)
 
371
        else:
 
372
            print_text(failure.getTraceback(), error=True)
 
373
            print_text("Unknown error occurred.", error=True)
 
374
        reactor.callLater(0, reactor.stop)
 
375
 
 
376
 
 
377
    print_text("Please wait... ", "")
 
378
 
 
379
    time.sleep(2)
 
380
    remote = RemoteBroker(get_bus(config.bus), retry_timeout=0)
 
381
    # This is a bit unfortunate. Every method of remote returns a deferred,
 
382
    # even stuff like connect_to_signal, because the fetching of the DBus
 
383
    # object itself is asynchronous. We can *mostly* fire-and-forget these
 
384
    # things, except that if the object isn't found, *all* of the deferreds
 
385
    # will fail. To prevent unhandled errors, we need to collect them all up
 
386
    # and add an errback.
 
387
    deferreds = [
 
388
        remote.reload_configuration(),
 
389
        remote.connect_to_signal("registration_done", success),
 
390
        remote.connect_to_signal("registration_failed", failure),
 
391
        remote.connect_to_signal("exchange_failed", exchange_failure),
 
392
        remote.register().addErrback(handle_registration_errors)]
 
393
    # We consume errors here to ignore errors after the first one. catch_all
 
394
    # will be called for the very first deferred that fails.
 
395
    gather_results(deferreds, consume_errors=True).addErrback(catch_all)
 
396
    reactor.run()
 
397
 
 
398
 
 
399
def main(args):
 
400
    config = LandscapeSetupConfiguration()
 
401
    config.load(args)
 
402
 
 
403
    # Disable startup on boot and stop the client, if one is running.
 
404
    if config.disable:
 
405
        stop_client_and_disable_init_script()
 
406
        return
 
407
 
 
408
    # Setup client configuration.
 
409
    try:
 
410
        setup(config)
 
411
    except Exception, e:
 
412
        print_text(str(e))
 
413
        sys.exit("Aborting Landscape configuration")
 
414
 
 
415
    # Attempt to register the client.
 
416
    if config.silent:
 
417
        register(config)
 
418
    else:
 
419
        answer = raw_input("\nRequest a new registration for "
 
420
                           "this computer now? (Y/n): ")
 
421
        if not answer.upper().startswith("N"):
 
422
            register(config)