1
"""Interactive configuration support for Landscape.
3
This module, and specifically L{LandscapeSetupScript}, implements the support
4
for the C{landscape-config} script.
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
17
from landscape.broker.registration import InvalidCredentialsError
18
from landscape.broker.deployment import BrokerConfiguration
19
from landscape.broker.remote import RemoteBroker
22
class ConfigurationError(Exception):
23
"""Raised when required configuration values are missing."""
26
def print_text(text, end="\n", error=False):
31
stream.write(text+end)
35
class LandscapeSetupConfiguration(BrokerConfiguration):
37
unsaved_options = ("no_start", "disable", "silent")
39
def make_parser(self):
41
Specialize L{Configuration.make_parser}, adding many
42
broker-specific options.
44
parser = super(LandscapeSetupConfiguration, self).make_parser()
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",
52
help="A comma-separated list of manager plugins to "
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 "
64
class LandscapeSetupScript(object):
66
An interactive procedure which manages the prompting and temporary storage
67
of configuration parameters.
69
Various attributes on this object will be set on C{config} after L{run} is
72
@ivar config: The L{BrokerConfiguration} object to read and set values from
76
def __init__(self, config):
79
def show_help(self, text):
80
lines = text.strip().splitlines()
81
print_text("\n"+"".join([line.strip()+"\n" for line in lines]))
83
def prompt(self, option, msg, required=False):
84
"""Prompt the user on the terminal for a value.
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
92
default = getattr(self.config, option, None)
94
msg += " [%s]: " % default
98
value = raw_input(msg)
100
setattr(self.config, option, value)
102
elif default or not required:
104
self.show_help("This option is required to configure Landscape.")
106
def password_prompt(self, option, msg, required=False):
107
"""Prompt the user on the terminal for a password and mask the value.
109
This also prompts the user twice and errors if both values don't match.
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
117
default = getattr(self.config, option, None)
120
value = getpass.getpass(msg)
122
value2 = getpass.getpass("Please confirm: ")
125
self.show_help("Passwords must match.")
127
setattr(self.config, option, value)
129
elif default or not required:
132
self.show_help("This option is required to configure "
135
def prompt_yes_no(self, message, default=True):
137
default_msg = " [Y/n]"
139
default_msg = " [y/N]"
141
value = raw_input(message + default_msg).lower()
143
if value.startswith("n"):
145
if value.startswith("y"):
147
self.show_help("Invalid input.")
151
def query_computer_title(self):
152
if "computer_title" in self.config.get_command_line_options():
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.
163
self.prompt("computer_title", "This computer's title", True)
165
def query_account_name(self):
166
if "account_name" in self.config.get_command_line_options():
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
177
self.prompt("account_name", "Account name", True)
179
def query_registration_password(self):
180
if "registration_password" in self.config.get_command_line_options():
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.
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)
194
self.password_prompt("registration_password",
195
"Account registration password")
197
def query_proxies(self):
198
options = self.config.get_command_line_options()
199
if "http_proxy" in options and "https_proxy" in options:
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.
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")
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:
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
228
msg = "Enable script execution?"
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")
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".
244
if not "script_users" in options:
245
self.prompt("script_users", "Script users")
247
if "ScriptExecution" in included_plugins:
248
included_plugins.remove("ScriptExecution")
249
self.config.include_manager_plugins = ", ".join(included_plugins)
251
def show_header(self):
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.
260
Please see https://landscape.canonical.com for more information.
264
"""Kick off the interactive process which prompts the user for data.
266
Data will be saved to C{self.config}.
269
self.query_computer_title()
270
self.query_account_name()
271
self.query_registration_password()
273
self.query_script_plugin()
276
def setup_init_script_and_start_client():
277
sysvconfig = SysVConfig()
278
sysvconfig.set_start_on_boot(True)
281
def stop_client_and_disable_init_script():
282
sysvconfig = SysVConfig()
283
sysvconfig.stop_landscape()
284
sysvconfig.set_start_on_boot(False)
288
sysvconfig = SysVConfig()
289
if not config.no_start:
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()
299
sys.exit("Aborting Landscape configuration")
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"]
307
if not config.get("account_name") or not config.get("computer_title"):
308
raise ConfigurationError("An account name and computer title are "
310
if config.get("script_users") and not config.include_manager_plugins:
311
config.include_manager_plugins = "ScriptExecution"
313
script = LandscapeSetupScript(config)
317
# Restart the client to ensure that it's using the new configuration.
318
if not config.no_start:
319
sysvconfig.restart_landscape()
322
def register(config, reactor=None):
323
"""Instruct the Landscape Broker to register the client.
325
The broker will be instructed to reload its configuration and then to
326
attempt a registration.
328
@param reactor: The reactor to use. Please only pass reactor when you
329
have totally mangled everything with mocker. Otherwise bad things
332
from twisted.internet.glib2reactor import install
335
from twisted.internet import reactor
338
print_text("Invalid account name or "
339
"registration password.", error=True)
343
print_text("System successfully registered.")
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.",
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")
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)
372
print_text(failure.getTraceback(), error=True)
373
print_text("Unknown error occurred.", error=True)
374
reactor.callLater(0, reactor.stop)
377
print_text("Please wait... ", "")
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.
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)
400
config = LandscapeSetupConfiguration()
403
# Disable startup on boot and stop the client, if one is running.
405
stop_client_and_disable_init_script()
408
# Setup client configuration.
413
sys.exit("Aborting Landscape configuration")
415
# Attempt to register the client.
419
answer = raw_input("\nRequest a new registration for "
420
"this computer now? (Y/n): ")
421
if not answer.upper().startswith("N"):