Merge lp:~free.ekanayaka/landscape-client/drop-cloud-registration into lp:~free.ekanayaka/landscape-client/drop-old-juju-info

Proposed by Free Ekanayaka
Status: Superseded
Proposed branch: lp:~free.ekanayaka/landscape-client/drop-cloud-registration
Merge into: lp:~free.ekanayaka/landscape-client/drop-old-juju-info
Diff against target: 1241 lines (+40/-942)
11 files modified
debian/cloud-default.conf (+0/-7)
debian/landscape-client.init (+2/-18)
debian/landscape-client.install (+0/-1)
landscape/broker/config.py (+0/-6)
landscape/broker/registration.py (+6/-210)
landscape/broker/tests/test_registration.py (+20/-669)
landscape/configuration.py (+1/-1)
landscape/message_schemas.py (+11/-1)
landscape/tests/test_configuration.py (+0/-16)
scripts/landscape-is-cloud-managed (+0/-12)
setup.py (+0/-1)
To merge this branch: bzr merge lp:~free.ekanayaka/landscape-client/drop-cloud-registration
Reviewer Review Type Date Requested Status
Landscape Pending
Landscape Pending
Review via email: mp+226992@code.launchpad.net

Description of the change

Drop the cloud registration code and associated tests.

To post a comment you must log in.
783. By Free Ekanayaka

Address review comments

Unmerged revisions

783. By Free Ekanayaka

Address review comments

782. By Free Ekanayaka

Add comment

781. By Free Ekanayaka

Revert drop-juju-info

780. By Free Ekanayaka

Merge from drop-old-juju-info

779. By Free Ekanayaka

Merge from drop-old-juju-info

778. By Free Ekanayaka

Drop cloud registration

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'debian/cloud-default.conf'
2--- debian/cloud-default.conf 2009-03-28 19:04:34 +0000
3+++ debian/cloud-default.conf 1970-01-01 00:00:00 +0000
4@@ -1,7 +0,0 @@
5-[client]
6-cloud = True
7-url = https://landscape.canonical.com/message-system
8-data_path = /var/lib/landscape/client
9-ping_url = http://landscape.canonical.com/ping
10-include_manager_plugins = ScriptExecution
11-script_users = ALL
12
13=== modified file 'debian/landscape-client.init'
14--- debian/landscape-client.init 2012-09-05 15:37:17 +0000
15+++ debian/landscape-client.init 2014-07-16 10:26:46 +0000
16@@ -31,24 +31,8 @@
17 # This $RUN check should match the semantics of
18 # l.sysvconfig.SysVConfig.is_configured_to_run.
19 if [ $RUN -eq 0 ]; then
20- if [ $CLOUD -eq 1 ]; then
21- if landscape-is-cloud-managed; then
22- # Install the cloud default configuration file
23- cp /usr/share/landscape/cloud-default.conf /etc/landscape/client.conf
24- # Override default file for not going in this conditional again at
25- # next startup
26- sed -i "s/^RUN=.*/RUN=1/" $LANDSCAPE_DEFAULTS
27- if ! grep -q "^RUN=" $LANDSCAPE_DEFAULTS; then
28- echo "RUN=1" >> $LANDSCAPE_DEFAULTS
29- fi
30- else
31- echo "$NAME is not configured, please run landscape-config."
32- exit 0
33- fi
34- else
35- echo "$NAME is not configured, please run landscape-config."
36- exit 0
37- fi
38+ echo "$NAME is not configured, please run landscape-config."
39+ exit 0
40 fi
41 }
42
43
44=== modified file 'debian/landscape-client.install'
45--- debian/landscape-client.install 2013-05-16 09:15:13 +0000
46+++ debian/landscape-client.install 2014-07-16 10:26:46 +0000
47@@ -7,7 +7,6 @@
48 usr/bin/landscape-package-changer
49 usr/bin/landscape-package-reporter
50 usr/bin/landscape-release-upgrader
51-usr/bin/landscape-is-cloud-managed
52 usr/bin/landscape-dbus-proxy
53 usr/share/landscape/cloud-default.conf
54 usr/lib/landscape
55
56=== modified file 'landscape/broker/config.py'
57--- landscape/broker/config.py 2014-03-25 15:21:52 +0000
58+++ landscape/broker/config.py 2014-07-16 10:26:46 +0000
59@@ -32,8 +32,6 @@
60 - C{urgent_exchange_interval} (C{1*60})
61 - C{http_proxy}
62 - C{https_proxy}
63- - C{cloud}
64- - C{otp}
65 - C{provisioning_otp}
66 """
67 parser = super(BrokerConfiguration, self).make_parser()
68@@ -60,10 +58,6 @@
69 help="The URL of the HTTP proxy, if one is needed.")
70 parser.add_option("--https-proxy", metavar="URL",
71 help="The URL of the HTTPS proxy, if one is needed.")
72- parser.add_option("--cloud", action="store_true",
73- help="Set this if your computer is in an EC2 cloud.")
74- parser.add_option("--otp", default="",
75- help="The OTP to use in cloud configuration.")
76 parser.add_option("--access-group", default="",
77 help="Suggested access group for this computer.")
78 parser.add_option("--tags",
79
80=== modified file 'landscape/broker/registration.py'
81--- landscape/broker/registration.py 2014-07-13 07:19:38 +0000
82+++ landscape/broker/registration.py 2014-07-16 10:26:46 +0000
83@@ -8,25 +8,16 @@
84 credentials yet and that the server accepts registration messages, so it
85 will craft an appropriate one and send it out.
86 """
87-import time
88 import logging
89-import socket
90
91 from twisted.internet.defer import Deferred
92
93-from landscape.lib.bpickle import loads
94-from landscape.lib.log import log_failure
95 from landscape.lib.juju import get_juju_info
96-from landscape.lib.fetch import fetch, FetchError
97 from landscape.lib.tag import is_valid_tag_list
98 from landscape.lib.network import get_fqdn
99 from landscape.lib.vm_info import get_vm_info, get_container_info
100
101
102-EC2_HOST = "169.254.169.254"
103-EC2_API = "http://%s/latest" % (EC2_HOST,)
104-
105-
106 class InvalidCredentialsError(Exception):
107 """
108 Raised when an invalid account title and/or registration key
109@@ -99,7 +90,6 @@
110 self._exchange = exchange
111 self._pinger = pinger
112 self._message_store = message_store
113- self._reactor.call_on("run", self._fetch_ec2_data)
114 self._reactor.call_on("run", self._get_juju_data)
115 self._reactor.call_on("pre-exchange", self._handle_pre_exchange)
116 self._reactor.call_on("exchange-done", self._handle_exchange_done)
117@@ -109,21 +99,15 @@
118 self._handle_registration)
119 self._should_register = None
120 self._fetch_async = fetch_async
121- self._otp = None
122 self._ec2_data = None
123 self._juju_data = None
124
125 def should_register(self):
126 id = self._identity
127 if id.secure_id:
128- # We already have a secure ID, no need to register
129- logging.info("Machine already has a secure-id. Skipping "
130- "registration.")
131 return False
132
133- if self._config.cloud:
134- return self._message_store.accepts("register-cloud-vm")
135- elif self._config.provisioning_otp:
136+ if self._config.provisioning_otp:
137 return self._message_store.accepts("register-provisioned-machine")
138
139 return bool(id.computer_title and id.account_name
140@@ -157,94 +141,6 @@
141 """
142 return self._fetch_async(EC2_API + path).addCallback(accumulate.append)
143
144- def _fetch_ec2_data(self):
145- """Retrieve available EC2 information, if in a EC2 compatible cloud."""
146- id = self._identity
147- if self._config.cloud and not id.secure_id:
148- # Fetch data from the EC2 API, to be used later in the registration
149- # process
150- # We ignore errors from user-data because it's common for the
151- # URL to return a 404 when the data is unavailable.
152- ec2_data = []
153- deferred = self._fetch_async(EC2_API + "/user-data").addErrback(
154- log_failure).addCallback(ec2_data.append)
155- paths = [
156- "/meta-data/instance-id",
157- "/meta-data/reservation-id",
158- "/meta-data/local-hostname",
159- "/meta-data/public-hostname",
160- "/meta-data/ami-launch-index",
161- "/meta-data/ami-id",
162- "/meta-data/local-ipv4",
163- "/meta-data/public-ipv4"]
164- # We're not using a DeferredList here because we want to keep the
165- # number of connections to the backend minimal. See lp:567515.
166- for path in paths:
167- deferred.addCallback(
168- lambda ignore, path=path: self._get_data(path, ec2_data))
169- # Special case the ramdisk retrieval, because it may not be present
170- deferred.addCallback(
171- lambda ignore: self._fetch_async(
172- EC2_API + "/meta-data/ramdisk-id").addErrback(log_failure))
173- deferred.addCallback(ec2_data.append)
174- # And same for kernel
175- deferred.addCallback(
176- lambda ignore: self._fetch_async(
177- EC2_API + "/meta-data/kernel-id").addErrback(log_failure))
178- deferred.addCallback(ec2_data.append)
179-
180- def record_data(ignore):
181- """Record the instance data returned by the EC2 API."""
182- (raw_user_data, instance_key, reservation_key,
183- local_hostname, public_hostname, launch_index,
184- ami_key, local_ip, public_ip, ramdisk_key,
185- kernel_key) = ec2_data
186- self._ec2_data = {
187- "instance_key": instance_key,
188- "reservation_key": reservation_key,
189- "local_hostname": local_hostname,
190- "public_hostname": public_hostname,
191- "launch_index": launch_index,
192- "kernel_key": kernel_key,
193- "ramdisk_key": ramdisk_key,
194- "image_key": ami_key,
195- "public_ipv4": public_ip,
196- "local_ipv4": local_ip}
197- for k, v in self._ec2_data.items():
198- if v is None and k in ("ramdisk_key", "kernel_key"):
199- continue
200- self._ec2_data[k] = v.decode("utf-8")
201- self._ec2_data["launch_index"] = int(
202- self._ec2_data["launch_index"])
203-
204- if self._config.otp:
205- self._otp = self._config.otp
206- return
207- instance_data = _extract_ec2_instance_data(
208- raw_user_data, int(launch_index))
209- if instance_data is not None:
210- self._otp = instance_data["otp"]
211- exchange_url = instance_data["exchange-url"]
212- ping_url = instance_data["ping-url"]
213- self._exchange._transport.set_url(exchange_url)
214- self._config.url = exchange_url
215- self._config.ping_url = ping_url
216- if "ssl-ca-certificate" in instance_data:
217- from landscape.configuration import \
218- store_public_key_data
219- public_key_file = store_public_key_data(
220- self._config, instance_data["ssl-ca-certificate"])
221- self._config.ssl_public_key = public_key_file
222- self._exchange._transport._pubkey = public_key_file
223- self._config.write()
224-
225- def log_error(error):
226- log_failure(error, msg="Got error while fetching meta-data: %r"
227- % (error.value,))
228-
229- deferred.addCallback(record_data)
230- deferred.addErrback(log_error)
231-
232 def _handle_exchange_done(self):
233 """Registered handler for the C{"exchange-done"} event.
234
235@@ -264,12 +160,8 @@
236 message with the server.
237
238 A computer can fall into several categories:
239- - a "cloud VM"
240 - a "normal" computer
241 - a "provisionned machine".
242-
243- Furthermore, Cloud VMs can be registered with either a One Time
244- Password (OTP), or with a normal registration password.
245 """
246 registration_failed = False
247
248@@ -293,10 +185,9 @@
249
250 if not is_valid_tag_list(tags):
251 tags = None
252- logging.error("Invalid tags provided for cloud registration.")
253+ logging.error("Invalid tags provided for registration.")
254
255 message = {"type": None, # either "register" or "register-cloud-vm"
256- "otp": None,
257 "hostname": get_fqdn(),
258 "account_name": identity.account_name,
259 "registration_password": None,
260@@ -306,28 +197,7 @@
261 if group:
262 message["access_group"] = group
263
264- if self._config.cloud and self._ec2_data is not None:
265- # This is the "cloud VM" case.
266- message["type"] = "register-cloud-vm"
267-
268- message.update(self._ec2_data)
269- if self._otp:
270- logging.info("Queueing message to register with OTP")
271- message["otp"] = self._otp
272-
273- elif account_name:
274- with_tags = "and tags %s " % tags if tags else ""
275- with_group = "in access group '%s' " % group if group else ""
276- logging.info(
277- u"Queueing message to register with account %r %s%s"
278- u"as an EC2 instance." % (
279- account_name, with_group, with_tags))
280- message["registration_password"] = registration_key
281-
282- else:
283- registration_failed = True
284-
285- elif account_name:
286+ if account_name:
287 # The computer is a normal computer, possibly a container.
288 with_word = "with" if bool(registration_key) else "without"
289 with_tags = "and tags %s " % tags if tags else ""
290@@ -342,6 +212,9 @@
291 message["container-info"] = get_container_info()
292
293 if self._juju_data is not None:
294+ # For backwards compatibility, set the juju-info to be one of
295+ # the juju infos (it used not to be a list).
296+ message["juju-info"] = self._juju_data[0]
297 message["juju-info-list"] = self._juju_data
298
299 elif self._config.provisioning_otp:
300@@ -430,80 +303,3 @@
301 def _failed(self):
302 self.deferred.errback(InvalidCredentialsError())
303 self._cancel_calls()
304-
305-
306-def _extract_ec2_instance_data(raw_user_data, launch_index):
307- """
308- Given the raw string of EC2 User Data, parse it and return the dict of
309- instance data for this particular instance.
310-
311- If the data can't be parsed, a debug message will be logged and None
312- will be returned.
313- """
314- try:
315- user_data = loads(raw_user_data)
316- except ValueError:
317- logging.debug("Got invalid user-data %r" % (raw_user_data,))
318- return
319-
320- if not isinstance(user_data, dict):
321- logging.debug("user-data %r is not a dict" % (user_data,))
322- return
323- for key in "otps", "exchange-url", "ping-url":
324- if key not in user_data:
325- logging.debug("user-data %r doesn't have key %r."
326- % (user_data, key))
327- return
328- if len(user_data["otps"]) <= launch_index:
329- logging.debug("user-data %r doesn't have OTP for launch index %d"
330- % (user_data, launch_index))
331- return
332- instance_data = {"otp": user_data["otps"][launch_index],
333- "exchange-url": user_data["exchange-url"],
334- "ping-url": user_data["ping-url"]}
335- if "ssl-ca-certificate" in user_data:
336- instance_data["ssl-ca-certificate"] = user_data["ssl-ca-certificate"]
337- return instance_data
338-
339-
340-def _wait_for_network():
341- """
342- Keep trying to connect to the EC2 metadata server until it becomes
343- accessible or until five minutes pass.
344-
345- This is necessary because the networking init script on Ubuntu is
346- asynchronous; the network may not actually be up by the time the
347- landscape-client init script is invoked.
348- """
349- timeout = 5 * 60
350- port = 80
351-
352- start = time.time()
353- while True:
354- s = socket.socket()
355- try:
356- s.connect((EC2_HOST, port))
357- s.close()
358- return
359- except socket.error:
360- time.sleep(1)
361- if time.time() - start > timeout:
362- break
363-
364-
365-def is_cloud_managed(fetch=fetch):
366- """
367- Return C{True} if the machine has been started by Landscape, i.e. if we can
368- find the expected data inside the EC2 user-data field.
369- """
370- _wait_for_network()
371- try:
372- raw_user_data = fetch(EC2_API + "/user-data",
373- connect_timeout=5)
374- launch_index = fetch(EC2_API + "/meta-data/ami-launch-index",
375- connect_timeout=5)
376- except FetchError:
377- return False
378- instance_data = _extract_ec2_instance_data(
379- raw_user_data, int(launch_index))
380- return instance_data is not None
381
382=== modified file 'landscape/broker/tests/test_registration.py'
383--- landscape/broker/tests/test_registration.py 2014-07-13 07:01:19 +0000
384+++ landscape/broker/tests/test_registration.py 2014-07-16 10:26:46 +0000
385@@ -1,24 +1,15 @@
386 import json
387-import os
388 import logging
389-import pycurl
390 import socket
391
392-from twisted.internet.defer import succeed, fail
393-
394 from landscape.broker.registration import (
395- InvalidCredentialsError, RegistrationHandler, is_cloud_managed, EC2_HOST,
396- EC2_API, Identity)
397+ InvalidCredentialsError, Identity)
398
399-from landscape.broker.config import BrokerConfiguration
400 from landscape.tests.helpers import LandscapeTest
401 from landscape.broker.tests.helpers import (
402 BrokerConfigurationHelper, RegistrationHelper)
403-from landscape.lib.bpickle import dumps
404-from landscape.lib.fetch import HTTPCodeError, FetchError
405 from landscape.lib.persist import Persist
406 from landscape.lib.vm_info import get_vm_info
407-from landscape.configuration import print_text
408
409
410 class IdentityTest(LandscapeTest):
411@@ -250,8 +241,7 @@
412 If the admin has defined tags for this computer, but they are not
413 valid, we drop them, and report an error.
414 """
415- self.log_helper.ignore_errors("Invalid tags provided for cloud "
416- "registration")
417+ self.log_helper.ignore_errors("Invalid tags provided for registration")
418 self.mstore.set_accepted_types(["register"])
419 self.config.computer_title = "Computer Title"
420 self.config.account_name = "account_name"
421@@ -261,8 +251,7 @@
422 messages = self.mstore.get_pending_messages()
423 self.assertIs(None, messages[0]["tags"])
424 self.assertEqual(self.logfile.getvalue().strip(),
425- "ERROR: Invalid tags provided for cloud "
426- "registration.\n "
427+ "ERROR: Invalid tags provided for registration.\n "
428 "INFO: Queueing message to register with account "
429 "'account_name' with a password.\n "
430 "INFO: Sending registration message to exchange.")
431@@ -536,6 +525,23 @@
432 "unit-name": "service/0"}
433 self.assertEqual(expected, messages[0]["juju-info-list"][0])
434
435+ def test_juju_info_compatibility_present(self):
436+ """
437+ When Juju information is found in $data_dir/juju-info.d/*.json,
438+ the registration message also contains a "juju-info" key for
439+ backwards compatibility with older servers.
440+ """
441+ self.mstore.set_accepted_types(["register"])
442+ self.config.account_name = "account_name"
443+ self.reactor.fire("run")
444+ self.reactor.fire("pre-exchange")
445+
446+ messages = self.mstore.get_pending_messages()
447+ expected = {"environment-uuid": "DEAD-BEEF",
448+ "api-addresses": ["10.0.3.1:17070"],
449+ "unit-name": "service/0"}
450+ self.assertEqual(expected, messages[0]["juju-info"])
451+
452 def test_multiple_juju_information_added_when_present(self):
453 """
454 When Juju information is found in $data_dir/juju-info.json,
455@@ -571,661 +577,6 @@
456 self.assertIn(expected2, juju_info)
457
458
459-class CloudRegistrationHandlerTest(RegistrationHandlerTestBase):
460-
461- cloud = True
462-
463- def setUp(self):
464- super(CloudRegistrationHandlerTest, self).setUp()
465- self.query_results = {}
466-
467- def fetch_stub(url):
468- value = self.query_results[url]
469- if isinstance(value, Exception):
470- return fail(value)
471- else:
472- return succeed(value)
473-
474- self.fetch_func = fetch_stub
475-
476- def get_user_data(self, otps=None,
477- exchange_url="https://example.com/message-system",
478- ping_url="http://example.com/ping",
479- ssl_ca_certificate=None):
480- if otps is None:
481- otps = ["otp1"]
482- user_data = {"otps": otps, "exchange-url": exchange_url,
483- "ping-url": ping_url}
484- if ssl_ca_certificate is not None:
485- user_data["ssl-ca-certificate"] = ssl_ca_certificate
486- return user_data
487-
488- def prepare_query_results(
489- self, user_data=None, instance_key="key1", launch_index=0,
490- local_hostname="ooga.local", public_hostname="ooga.amazon.com",
491- reservation_key=u"res1", ramdisk_key=u"ram1", kernel_key=u"kernel1",
492- image_key=u"image1", local_ip="10.0.0.1", public_ip="10.0.0.2",
493- ssl_ca_certificate=None):
494- if user_data is None:
495- user_data = self.get_user_data(
496- ssl_ca_certificate=ssl_ca_certificate)
497- if not isinstance(user_data, Exception):
498- user_data = dumps(user_data)
499- api_base = "http://169.254.169.254/latest"
500- self.query_results.clear()
501- for url_suffix, value in [
502- ("/user-data", user_data),
503- ("/meta-data/instance-id", instance_key),
504- ("/meta-data/reservation-id", reservation_key),
505- ("/meta-data/local-hostname", local_hostname),
506- ("/meta-data/public-hostname", public_hostname),
507- ("/meta-data/ami-launch-index", str(launch_index)),
508- ("/meta-data/kernel-id", kernel_key),
509- ("/meta-data/ramdisk-id", ramdisk_key),
510- ("/meta-data/ami-id", image_key),
511- ("/meta-data/local-ipv4", local_ip),
512- ("/meta-data/public-ipv4", public_ip),
513- ]:
514- self.query_results[api_base + url_suffix] = value
515-
516- def prepare_cloud_registration(self, account_name=None,
517- registration_key=None, tags=None,
518- access_group=None):
519- # Set things up so that the client thinks it should register
520- self.mstore.set_accepted_types(list(self.mstore.get_accepted_types())
521- + ["register-cloud-vm"])
522- self.config.account_name = account_name
523- self.config.registration_key = registration_key
524- self.config.computer_title = None
525- self.config.tags = tags
526- self.config.access_group = access_group
527- self.identity.secure_id = None
528- self.assertTrue(self.handler.should_register())
529-
530- def get_expected_cloud_message(self, **kwargs):
531- """
532- Return the message which is expected from a similar call to
533- L{get_registration_handler_for_cloud}.
534- """
535- message = dict(type="register-cloud-vm",
536- otp="otp1",
537- hostname="ooga.local",
538- local_hostname="ooga.local",
539- public_hostname="ooga.amazon.com",
540- instance_key=u"key1",
541- reservation_key=u"res1",
542- ramdisk_key=u"ram1",
543- kernel_key=u"kernel1",
544- launch_index=0,
545- image_key=u"image1",
546- account_name=None,
547- registration_password=None,
548- local_ipv4=u"10.0.0.1",
549- public_ipv4=u"10.0.0.2",
550- tags=None)
551- # The get_vm_info() needs to be deferred to the else. If vm-info is
552- # not specified in kwargs, get_vm_info() will typically be mocked.
553- if "vm_info" in kwargs:
554- message["vm-info"] = kwargs.pop("vm_info")
555- else:
556- message["vm-info"] = get_vm_info()
557- message.update(kwargs)
558- return message
559-
560- def test_cloud_registration(self):
561- """
562- When the 'cloud' configuration variable is set, cloud registration is
563- done instead of normal password-based registration. This means:
564-
565- - "Launch Data" is fetched from the EC2 Launch Data URL. This contains
566- a one-time password that is used during registration.
567- - A different "register-cloud-vm" message is sent to the server instead
568- of "register", containing the OTP. This message is handled by
569- immediately accepting the computer, instead of going through the
570- pending computer stage.
571- """
572- get_vm_info_mock = self.mocker.replace(get_vm_info)
573- get_vm_info_mock()
574- self.mocker.result("xen")
575- self.mocker.replay()
576- self.prepare_query_results()
577- self.prepare_cloud_registration(tags=u"server,london")
578-
579- # metadata is fetched and stored at reactor startup:
580- self.reactor.fire("run")
581-
582- # And the metadata returned determines the URLs that are used
583- self.assertEqual(self.transport.get_url(),
584- "https://example.com/message-system")
585- self.assertEqual(self.pinger.get_url(),
586- "http://example.com/ping")
587- # Lets make sure those values were written back to the config file
588- new_config = BrokerConfiguration()
589- new_config.load_configuration_file(self.config_filename)
590- self.assertEqual(new_config.url, "https://example.com/message-system")
591- self.assertEqual(new_config.ping_url, "http://example.com/ping")
592-
593- # Okay! Exchange should cause the registration to happen.
594- self.exchanger.exchange()
595- # This *should* be asynchronous, but I think a billion tests are
596- # written like this
597- self.assertEqual(len(self.transport.payloads), 1)
598- self.assertMessages(
599- self.transport.payloads[0]["messages"],
600- [self.get_expected_cloud_message(tags=u"server,london",
601- vm_info="xen")])
602-
603- def test_cloud_registration_with_access_group(self):
604- """
605- If the access_group field is presnet in the configuration, the
606- access_group field is present in the outgoing message for a VM
607- registration, and a notice appears in the logs.
608- """
609- self.prepare_query_results()
610- self.prepare_cloud_registration(access_group=u"dinosaurs",
611- tags=u"server,london")
612-
613- self.reactor.fire("run")
614- self.exchanger.exchange()
615- self.assertEqual(len(self.transport.payloads), 1)
616- self.assertMessages(
617- self.transport.payloads[0]["messages"],
618- [self.get_expected_cloud_message(
619- access_group=u"dinosaurs", tags=u"server,london")])
620-
621- def test_cloud_registration_with_otp(self):
622- """
623- If the OTP is present in the configuration, it's used to trigger the
624- registration instead of using the user data.
625- """
626- self.config.otp = "otp1"
627- self.prepare_query_results(user_data=None)
628-
629- self.prepare_cloud_registration()
630-
631- # metadata is fetched and stored at reactor startup:
632- self.reactor.fire("run")
633-
634- # Okay! Exchange should cause the registration to happen.
635- self.exchanger.exchange()
636- # This *should* be asynchronous, but I think a billion tests are
637- # written like this
638- self.assertEqual(len(self.transport.payloads), 1)
639- self.assertMessages(
640- self.transport.payloads[0]["messages"],
641- [self.get_expected_cloud_message()])
642-
643- def test_cloud_registration_with_invalid_tags(self):
644- """
645- Invalid tags in the configuration should result in the tags not being
646- sent to the server, and this fact logged.
647- """
648- self.log_helper.ignore_errors("Invalid tags provided for cloud "
649- "registration")
650- self.prepare_query_results()
651- self.prepare_cloud_registration(tags=u"<script>alert()</script>,hardy")
652-
653- # metadata is fetched and stored at reactor startup:
654- self.reactor.fire("run")
655- self.exchanger.exchange()
656- self.assertEqual(len(self.transport.payloads), 1)
657- self.assertMessages(self.transport.payloads[0]["messages"],
658- [self.get_expected_cloud_message(tags=None)])
659- self.assertEqual(self.logfile.getvalue().strip()[:-7],
660- "ERROR: Invalid tags provided for cloud "
661- "registration.\n "
662- "INFO: Queueing message to register with OTP\n "
663- "INFO: Sending registration message to exchange.\n "
664- " INFO: Starting message exchange with "
665- "https://example.com/message-system.\n "
666- "INFO: Message exchange completed in")
667-
668- def test_cloud_registration_with_ssl_ca_certificate(self):
669- """
670- If we have an SSL certificate CA included in the user-data, this should
671- be written out, and the configuration updated to reflect this.
672- """
673- key_filename = os.path.join(self.config.data_path,
674- "%s.ssl_public_key" % os.path.basename(self.config_filename))
675-
676- print_text_mock = self.mocker.replace(print_text)
677- print_text_mock("Writing SSL CA certificate to %s..." %
678- key_filename)
679- self.mocker.replay()
680- self.prepare_query_results(ssl_ca_certificate=u"1234567890")
681- self.prepare_cloud_registration(tags=u"server,london")
682- # metadata is fetched and stored at reactor startup:
683- self.reactor.fire("run")
684- # And the metadata returned determines the URLs that are used
685- self.assertEqual("https://example.com/message-system",
686- self.transport.get_url())
687- self.assertEqual(key_filename, self.transport._pubkey)
688- self.assertEqual("http://example.com/ping",
689- self.pinger.get_url())
690- # Let's make sure those values were written back to the config file
691- new_config = BrokerConfiguration()
692- new_config.load_configuration_file(self.config_filename)
693- self.assertEqual("https://example.com/message-system", new_config.url)
694- self.assertEqual("http://example.com/ping", new_config.ping_url)
695- self.assertEqual(key_filename, new_config.ssl_public_key)
696- self.assertEqual("1234567890", open(key_filename, "r").read())
697-
698- def test_wrong_user_data(self):
699- self.prepare_query_results(user_data="other stuff, not a bpickle")
700- self.prepare_cloud_registration()
701-
702- # Mock registration-failed call
703- reactor_mock = self.mocker.patch(self.reactor)
704- reactor_mock.fire("registration-failed")
705- self.mocker.replay()
706-
707- self.reactor.fire("run")
708- self.exchanger.exchange()
709-
710- def test_wrong_object_type_in_user_data(self):
711- self.prepare_query_results(user_data=True)
712- self.prepare_cloud_registration()
713-
714- # Mock registration-failed call
715- reactor_mock = self.mocker.patch(self.reactor)
716- reactor_mock.fire("registration-failed")
717- self.mocker.replay()
718-
719- self.reactor.fire("run")
720- self.exchanger.exchange()
721-
722- def test_user_data_with_not_enough_elements(self):
723- """
724- If the AMI launch index isn't represented in the list of OTPs in the
725- user data then BOOM.
726- """
727- self.prepare_query_results(launch_index=1)
728- self.prepare_cloud_registration()
729-
730- # Mock registration-failed call
731- reactor_mock = self.mocker.patch(self.reactor)
732- reactor_mock.fire("registration-failed")
733- self.mocker.replay()
734-
735- self.reactor.fire("run")
736- self.exchanger.exchange()
737-
738- def test_user_data_bpickle_without_otp(self):
739- self.prepare_query_results(user_data={"foo": "bar"})
740- self.prepare_cloud_registration()
741-
742- # Mock registration-failed call
743- reactor_mock = self.mocker.patch(self.reactor)
744- reactor_mock.fire("registration-failed")
745- self.mocker.replay()
746-
747- self.reactor.fire("run")
748- self.exchanger.exchange()
749-
750- def test_no_otp_fallback_to_account(self):
751- self.prepare_query_results(user_data="other stuff, not a bpickle",
752- instance_key=u"key1")
753- self.prepare_cloud_registration(account_name=u"onward",
754- registration_key=u"password",
755- tags=u"london,server")
756-
757- self.reactor.fire("run")
758- self.exchanger.exchange()
759-
760- self.assertEqual(len(self.transport.payloads), 1)
761- self.assertMessages(self.transport.payloads[0]["messages"],
762- [self.get_expected_cloud_message(
763- otp=None,
764- account_name=u"onward",
765- registration_password=u"password",
766- tags=u"london,server")])
767- self.assertEqual(self.logfile.getvalue().strip()[:-7],
768- "INFO: Queueing message to register with account u'onward' and "
769- "tags london,server as an EC2 instance.\n "
770- "INFO: Sending registration message to exchange.\n "
771- "INFO: Starting message exchange with http://localhost:91919.\n "
772- " INFO: Message exchange completed in")
773-
774- def test_queueing_cloud_registration_message_resets_message_store(self):
775- """
776- When a registration from a cloud is about to happen, the message store
777- is reset, because all previous messages are now meaningless.
778- """
779- self.mstore.set_accepted_types(list(self.mstore.get_accepted_types())
780- + ["test"])
781-
782- self.mstore.add({"type": "test"})
783-
784- self.prepare_query_results()
785-
786- self.prepare_cloud_registration()
787-
788- self.reactor.fire("run")
789- self.reactor.fire("pre-exchange")
790-
791- messages = self.mstore.get_pending_messages()
792- self.assertEqual(len(messages), 1)
793- self.assertEqual(messages[0]["type"], "register-cloud-vm")
794-
795- def test_cloud_registration_fetch_errors(self):
796- """
797- If fetching metadata fails, and we have no account details to fall
798- back to, we fire 'registration-failed'.
799- """
800- self.log_helper.ignore_errors(pycurl.error)
801-
802- def fetch_stub(url):
803- return fail(pycurl.error(7, "couldn't connect to host"))
804-
805- self.handler = RegistrationHandler(
806- self.config, self.identity, self.reactor, self.exchanger,
807- self.pinger, self.mstore, fetch_async=fetch_stub)
808-
809- self.fetch_stub = fetch_stub
810- self.prepare_query_results()
811- self.fetch_stub = fetch_stub
812-
813- self.prepare_cloud_registration()
814-
815- failed = []
816- self.reactor.call_on(
817- "registration-failed", lambda: failed.append(True))
818-
819- self.log_helper.ignore_errors("Got error while fetching meta-data")
820- self.reactor.fire("run")
821- self.exchanger.exchange()
822- self.assertEqual(failed, [True])
823- self.assertIn('error: (7, "couldn\'t connect to host")',
824- self.logfile.getvalue())
825-
826- def test_cloud_registration_continues_without_user_data(self):
827- """
828- If no user-data exists (i.e., the user-data URL returns a 404), then
829- register-cloud-vm still occurs.
830- """
831- self.log_helper.ignore_errors(HTTPCodeError)
832- self.prepare_query_results(user_data=HTTPCodeError(404, "ohno"))
833- self.prepare_cloud_registration(account_name="onward",
834- registration_key="password")
835-
836- self.reactor.fire("run")
837- self.exchanger.exchange()
838- self.assertIn("HTTPCodeError: Server returned HTTP code 404",
839- self.logfile.getvalue())
840- self.assertEqual(len(self.transport.payloads), 1)
841- self.assertMessages(self.transport.payloads[0]["messages"],
842- [self.get_expected_cloud_message(
843- otp=None,
844- account_name=u"onward",
845- registration_password=u"password")])
846-
847- def test_cloud_registration_continues_without_ramdisk(self):
848- """
849- If the instance doesn't have a ramdisk (ie, the query for ramdisk
850- returns a 404), then register-cloud-vm still occurs.
851- """
852- self.log_helper.ignore_errors(HTTPCodeError)
853- self.prepare_query_results(ramdisk_key=HTTPCodeError(404, "ohno"))
854- self.prepare_cloud_registration()
855-
856- self.reactor.fire("run")
857- self.exchanger.exchange()
858- self.assertIn("HTTPCodeError: Server returned HTTP code 404",
859- self.logfile.getvalue())
860- self.assertEqual(len(self.transport.payloads), 1)
861- self.assertMessages(self.transport.payloads[0]["messages"],
862- [self.get_expected_cloud_message(
863- ramdisk_key=None)])
864-
865- def test_cloud_registration_continues_without_kernel(self):
866- """
867- If the instance doesn't have a kernel (ie, the query for kernel
868- returns a 404), then register-cloud-vm still occurs.
869- """
870- self.log_helper.ignore_errors(HTTPCodeError)
871- self.prepare_query_results(kernel_key=HTTPCodeError(404, "ohno"))
872- self.prepare_cloud_registration()
873-
874- self.reactor.fire("run")
875- self.exchanger.exchange()
876- self.assertIn("HTTPCodeError: Server returned HTTP code 404",
877- self.logfile.getvalue())
878- self.assertEqual(len(self.transport.payloads), 1)
879- self.assertMessages(self.transport.payloads[0]["messages"],
880- [self.get_expected_cloud_message(
881- kernel_key=None)])
882-
883- def test_fall_back_to_normal_registration_when_metadata_fetch_fails(self):
884- """
885- If fetching metadata fails, but we do have an account name, then we
886- fall back to normal 'register' registration.
887- """
888- self.mstore.set_accepted_types(["register"])
889- self.log_helper.ignore_errors(HTTPCodeError)
890- self.prepare_query_results(
891- public_hostname=HTTPCodeError(404, "ohnoes"))
892- self.prepare_cloud_registration(account_name="onward",
893- registration_key="password")
894- self.config.computer_title = "whatever"
895- self.reactor.fire("run")
896- self.exchanger.exchange()
897- self.assertIn("HTTPCodeError: Server returned HTTP code 404",
898- self.logfile.getvalue())
899- self.assertEqual(len(self.transport.payloads), 1)
900- messages = self.transport.payloads[0]["messages"]
901- self.assertEqual("register", messages[0]["type"])
902-
903- def test_should_register_in_cloud(self):
904- """
905- The client should register when it's in the cloud even though
906- it doesn't have the normal account details.
907- """
908- self.mstore.set_accepted_types(self.mstore.get_accepted_types()
909- + ("register-cloud-vm",))
910- self.config.account_name = None
911- self.config.registration_key = None
912- self.config.computer_title = None
913- self.identity.secure_id = None
914- self.assertTrue(self.handler.should_register())
915-
916- def test_launch_index(self):
917- """
918- The client used the value in C{ami-launch-index} to choose the
919- appropriate OTP in the user data.
920- """
921- otp = "correct otp for launch index"
922- self.prepare_query_results(
923- user_data=self.get_user_data(otps=["wrong index", otp,
924- "wrong again"],),
925- instance_key="key1",
926- launch_index=1)
927-
928- self.prepare_cloud_registration()
929-
930- self.reactor.fire("run")
931- self.exchanger.exchange()
932- self.assertEqual(len(self.transport.payloads), 1)
933- self.assertMessages(self.transport.payloads[0]["messages"],
934- [self.get_expected_cloud_message(otp=otp,
935- launch_index=1)])
936-
937- def test_should_not_register_in_cloud(self):
938- """
939- Having a secure ID means we shouldn't register, even in the cloud.
940- """
941- self.mstore.set_accepted_types(self.mstore.get_accepted_types()
942- + ("register-cloud-vm",))
943- self.config.account_name = None
944- self.config.registration_key = None
945- self.config.computer_title = None
946- self.identity.secure_id = "hello"
947- self.assertFalse(self.handler.should_register())
948-
949- def test_should_not_register_without_register_cloud_vm(self):
950- """
951- If the server isn't accepting a 'register-cloud-vm' message,
952- we shouldn't register.
953- """
954- self.config.account_name = None
955- self.config.registration_key = None
956- self.config.computer_title = None
957- self.identity.secure_id = None
958- self.assertFalse(self.handler.should_register())
959-
960-
961-class IsCloudManagedTests(LandscapeTest):
962-
963- def setUp(self):
964- super(IsCloudManagedTests, self).setUp()
965- self.urls = []
966- self.responses = []
967-
968- def fake_fetch(self, url, connect_timeout=None):
969- self.urls.append((url, connect_timeout))
970- return self.responses.pop(0)
971-
972- def mock_socket(self):
973- """
974- Mock out socket usage by is_cloud_managed to wait for the network.
975- """
976- # Mock the socket.connect call that it also does
977- socket_class = self.mocker.replace("socket.socket", passthrough=False)
978- socket = socket_class()
979- socket.connect((EC2_HOST, 80))
980- socket.close()
981-
982- def test_is_managed(self):
983- """
984- L{is_cloud_managed} returns True if the EC2 user-data contains
985- Landscape instance information. It fetches the EC2 data with low
986- timeouts.
987- """
988- user_data = {"otps": ["otp1"], "exchange-url": "http://exchange",
989- "ping-url": "http://ping"}
990- self.responses = [dumps(user_data), "0"]
991-
992- self.mock_socket()
993- self.mocker.replay()
994-
995- self.assertTrue(is_cloud_managed(self.fake_fetch))
996- self.assertEqual(
997- self.urls,
998- [(EC2_API + "/user-data", 5),
999- (EC2_API + "/meta-data/ami-launch-index", 5)])
1000-
1001- def test_is_managed_index(self):
1002- user_data = {"otps": ["otp1", "otp2"],
1003- "exchange-url": "http://exchange",
1004- "ping-url": "http://ping"}
1005- self.responses = [dumps(user_data), "1"]
1006- self.mock_socket()
1007- self.mocker.replay()
1008- self.assertTrue(is_cloud_managed(self.fake_fetch))
1009-
1010- def test_is_managed_wrong_index(self):
1011- user_data = {"otps": ["otp1"], "exchange-url": "http://exchange",
1012- "ping-url": "http://ping"}
1013- self.responses = [dumps(user_data), "1"]
1014- self.mock_socket()
1015- self.mocker.replay()
1016- self.assertFalse(is_cloud_managed(self.fake_fetch))
1017-
1018- def test_is_managed_exchange_url(self):
1019- user_data = {"otps": ["otp1"], "ping-url": "http://ping"}
1020- self.responses = [dumps(user_data), "0"]
1021- self.mock_socket()
1022- self.mocker.replay()
1023- self.assertFalse(is_cloud_managed(self.fake_fetch))
1024-
1025- def test_is_managed_ping_url(self):
1026- user_data = {"otps": ["otp1"], "exchange-url": "http://exchange"}
1027- self.responses = [dumps(user_data), "0"]
1028- self.mock_socket()
1029- self.mocker.replay()
1030- self.assertFalse(is_cloud_managed(self.fake_fetch))
1031-
1032- def test_is_managed_bpickle(self):
1033- self.responses = ["some other user data", "0"]
1034- self.mock_socket()
1035- self.mocker.replay()
1036- self.assertFalse(is_cloud_managed(self.fake_fetch))
1037-
1038- def test_is_managed_no_data(self):
1039- self.responses = ["", "0"]
1040- self.mock_socket()
1041- self.mocker.replay()
1042- self.assertFalse(is_cloud_managed(self.fake_fetch))
1043-
1044- def test_is_managed_fetch_not_found(self):
1045-
1046- def fake_fetch(url, connect_timeout=None):
1047- raise HTTPCodeError(404, "ohnoes")
1048-
1049- self.mock_socket()
1050- self.mocker.replay()
1051- self.assertFalse(is_cloud_managed(fake_fetch))
1052-
1053- def test_is_managed_fetch_error(self):
1054-
1055- def fake_fetch(url, connect_timeout=None):
1056- raise FetchError(7, "couldn't connect to host")
1057-
1058- self.mock_socket()
1059- self.mocker.replay()
1060- self.assertFalse(is_cloud_managed(fake_fetch))
1061-
1062- def test_waits_for_network(self):
1063- """
1064- is_cloud_managed will wait until the network before trying to fetch
1065- the EC2 user data.
1066- """
1067- user_data = {"otps": ["otp1"], "exchange-url": "http://exchange",
1068- "ping-url": "http://ping"}
1069- self.responses = [dumps(user_data), "0"]
1070-
1071- self.mocker.order()
1072- time_sleep = self.mocker.replace("time.sleep", passthrough=False)
1073- socket_class = self.mocker.replace("socket.socket", passthrough=False)
1074- socket_obj = socket_class()
1075- socket_obj.connect((EC2_HOST, 80))
1076- self.mocker.throw(socket.error("woops"))
1077- time_sleep(1)
1078- socket_obj = socket_class()
1079- socket_obj.connect((EC2_HOST, 80))
1080- self.mocker.result(None)
1081- socket_obj.close()
1082- self.mocker.replay()
1083- self.assertTrue(is_cloud_managed(self.fake_fetch))
1084-
1085- def test_waiting_times_out(self):
1086- """
1087- We'll only wait five minutes for the network to come up.
1088- """
1089-
1090- def fake_fetch(url, connect_timeout=None):
1091- raise FetchError(7, "couldn't connect to host")
1092-
1093- self.mocker.order()
1094- time_sleep = self.mocker.replace("time.sleep", passthrough=False)
1095- time_time = self.mocker.replace("time.time", passthrough=False)
1096- time_time()
1097- self.mocker.result(100)
1098- socket_class = self.mocker.replace("socket.socket", passthrough=False)
1099- socket_obj = socket_class()
1100- socket_obj.connect((EC2_HOST, 80))
1101- self.mocker.throw(socket.error("woops"))
1102- time_sleep(1)
1103- time_time()
1104- self.mocker.result(401)
1105- self.mocker.replay()
1106- # Mocking time.time is dangerous, because the test harness calls it. So
1107- # we explicitly reset mocker before returning from the test.
1108- try:
1109- self.assertFalse(is_cloud_managed(fake_fetch))
1110- finally:
1111- self.mocker.reset()
1112-
1113-
1114 class ProvisioningRegistrationTest(RegistrationHandlerTestBase):
1115
1116 def test_provisioned_machine_registration_with_otp(self):
1117
1118=== modified file 'landscape/configuration.py'
1119--- landscape/configuration.py 2014-07-01 14:52:02 +0000
1120+++ landscape/configuration.py 2014-07-16 10:26:46 +0000
1121@@ -581,7 +581,7 @@
1122 decode_base64_ssl_public_certificate(config)
1123 config.write()
1124 # Restart the client to ensure that it's using the new configuration.
1125- if not config.no_start and not config.otp:
1126+ if not config.no_start:
1127 try:
1128 sysvconfig.restart_landscape()
1129 except ProcessError:
1130
1131=== modified file 'landscape/message_schemas.py'
1132--- landscape/message_schemas.py 2014-07-13 07:01:19 +0000
1133+++ landscape/message_schemas.py 2014-07-16 10:26:46 +0000
1134@@ -112,6 +112,11 @@
1135 "unit-name": Unicode(),
1136 "private-address": Unicode()}
1137
1138+# The copy is needed because Message mutates the dictionary
1139+JUJU_INFO = Message("juju-info",
1140+ juju_data.copy(),
1141+ optional=["private-address"])
1142+
1143 JUJU_UNITS_INFO = Message("juju-units-info", {
1144 "juju-info-list": List(KeyDict(juju_data.copy(),
1145 optional=["private-address"]))
1146@@ -191,6 +196,9 @@
1147 "tags": Any(Unicode(), Constant(None)),
1148 "vm-info": Bytes(),
1149 "container-info": Unicode(),
1150+ "juju-info": KeyDict(juju_data, optional=["private-address"]),
1151+ # Because of backwards compatibility we need another member with the list
1152+ # of juju-info, so it can safely be ignored by old servers.
1153 "juju-info-list": List(KeyDict(juju_data, optional=["private-address"])),
1154 "access_group": Unicode()},
1155 optional=["registration_password", "hostname", "tags", "vm-info",
1156@@ -203,6 +211,8 @@
1157 {"otp": Bytes()})
1158
1159
1160+# XXX The register-cloud-vm message is obsolete, it's kept around just to not
1161+# break older LDS releases that import it. Eventually it shall be dropped.
1162 REGISTER_CLOUD_VM = Message(
1163 "register-cloud-vm",
1164 {"hostname": Unicode(),
1165@@ -476,5 +486,5 @@
1166 EUCALYPTUS_INFO_ERROR, NETWORK_DEVICE, NETWORK_ACTIVITY,
1167 REBOOT_REQUIRED_INFO, UPDATE_MANAGER_INFO, CPU_USAGE,
1168 CEPH_USAGE, SWIFT_USAGE, SWIFT_DEVICE_INFO, KEYSTONE_TOKEN,
1169- CHANGE_HA_SERVICE, JUJU_UNITS_INFO, CLOUD_METADATA]:
1170+ CHANGE_HA_SERVICE, JUJU_INFO, JUJU_UNITS_INFO, CLOUD_METADATA]:
1171 message_schemas[schema.type] = schema
1172
1173=== modified file 'landscape/tests/test_configuration.py'
1174--- landscape/tests/test_configuration.py 2014-02-25 18:06:48 +0000
1175+++ landscape/tests/test_configuration.py 2014-07-16 10:26:46 +0000
1176@@ -820,7 +820,6 @@
1177 "--ping-interval", "30",
1178 "--http-proxy", "",
1179 "--https-proxy", "",
1180- "--otp", "",
1181 "--tags", "",
1182 "--provisioning-otp", ""]
1183 config = self.get_config(args)
1184@@ -837,7 +836,6 @@
1185 "https_proxy = \n"
1186 "url = https://landscape.canonical.com/message-system\n"
1187 "exchange_interval = 900\n"
1188- "otp = \n"
1189 "ping_interval = 30\n"
1190 "ping_url = http://landscape.canonical.com/ping\n"
1191 "provisioning_otp = \n"
1192@@ -862,20 +860,6 @@
1193 config = self.get_config(["--silent", "-t", "rex"])
1194 self.assertRaises(ConfigurationError, setup, config)
1195
1196- def test_silent_setup_with_otp(self):
1197- """
1198- If the OTP is specified, there is no need to pass the account name and
1199- the computer title.
1200- """
1201- sysvconfig_mock = self.mocker.patch(SysVConfig)
1202- sysvconfig_mock.set_start_on_boot(True)
1203- self.mocker.replay()
1204-
1205- config = self.get_config(["--silent", "--otp", "otp1"])
1206- setup(config)
1207-
1208- self.assertEqual("otp1", config.otp)
1209-
1210 def test_silent_setup_with_provisioning_otp(self):
1211 """
1212 If the provisioning OTP is specified, there is no need to pass the
1213
1214=== removed file 'scripts/landscape-is-cloud-managed'
1215--- scripts/landscape-is-cloud-managed 2009-04-09 14:32:45 +0000
1216+++ scripts/landscape-is-cloud-managed 1970-01-01 00:00:00 +0000
1217@@ -1,12 +0,0 @@
1218-#!/usr/bin/python
1219-import sys, os
1220-if os.path.dirname(os.path.abspath(sys.argv[0])) == os.path.abspath("scripts"):
1221- sys.path.insert(0, "./")
1222-else:
1223- from landscape.lib.warning import hide_warnings
1224- hide_warnings()
1225-
1226-from landscape.broker.registration import is_cloud_managed
1227-
1228-# We return 0 if it succeeds
1229-sys.exit(not is_cloud_managed())
1230
1231=== modified file 'setup.py'
1232--- setup.py 2013-06-03 12:26:30 +0000
1233+++ setup.py 2014-07-16 10:26:46 +0000
1234@@ -56,7 +56,6 @@
1235 "scripts/landscape-package-reporter",
1236 "scripts/landscape-release-upgrader",
1237 "scripts/landscape-sysinfo",
1238- "scripts/landscape-is-cloud-managed",
1239 "scripts/landscape-dbus-proxy",
1240 "scripts/landscape-client-settings-mechanism",
1241 "scripts/landscape-client-registration-mechanism",

Subscribers

People subscribed via source and target branches

to all changes: