Merge lp:~chad.smith/landscape-client/avoid-metadata-pycurl-traceback into lp:~landscape/landscape-client/trunk

Proposed by Chad Smith
Status: Merged
Approved by: Chad Smith
Approved revision: 728
Merged at revision: 719
Proposed branch: lp:~chad.smith/landscape-client/avoid-metadata-pycurl-traceback
Merge into: lp:~landscape/landscape-client/trunk
Diff against target: 446 lines (+258/-92)
4 files modified
landscape/lib/cloud.py (+45/-0)
landscape/lib/tests/test_cloud.py (+107/-0)
landscape/monitor/computerinfo.py (+28/-51)
landscape/monitor/tests/test_computerinfo.py (+78/-41)
To merge this branch: bzr merge lp:~chad.smith/landscape-client/avoid-metadata-pycurl-traceback
Reviewer Review Type Date Requested Status
Chris Glass (community) Approve
Free Ekanayaka (community) Approve
Review via email: mp+183279@code.launchpad.net

Commit message

Avoid pycurl tracebacks from missing EC2 API meta-data calls. Metadata queries allow 3 retries before giving up and logging warnings.

Description of the change

This branch reduces the noise of a metadata logs in a couple of ways:
  1. catch pycurl "host can't be contacted" error so that it doesn't read like a traceback.
  2. Remove the discrete "Queueing metadata at url ..../(instance-id|ami-id|instance-type)" and replace with single generic "Updating cloud meta-data." and "Updated cloud meta-data" upon success
  3. keep internal bool flag _check_cloud to prevent repetitive calls to _fetch_cloud_meta_data each message exchange when either the instance:
     -- is not a cloud instance (e.g. LXC or physical server)
     -- ls-client has already obtained the EC2 metadata (because instance-id, ami-id and instance-type don't change except across restarts)

To post a comment you must log in.
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Marking as needs fixing because of [1], thanks.

[0]

landscape/monitor/computerinfo.py:145:17: E126 continuation line over-indented for hanging indent
landscape/monitor/computerinfo.py:148:17: E126 continuation line over-indented for hanging indent

[1]

+ if error.check(PyCurlError):

I don't think there's a truly reliable way to discern if a fetch error is due to the client not being run in a cloud instance or to some other transient network error.

I propose that the code retries up to 3 times no matter what, then gives up.

[2]

         self._cloud_meta_data = None
+ self._check_cloud = True
+ self._cloud_retries = 0

Assuming that [1] gets fixed, we can drop self._check_cloud

    def __init__(self, ...)
        ...
        self._cloud_retries = 0
        ...

    def send_computer_message(self, urgent=False):
        if self._cloud_meta_data is None and self._cloud_retries < 3:
            self._cloud_retries += 1
            self._cloud_meta_data = yield self._fetch_cloud_meta_data()

fetch_cloud_meta_data should just return None in case any failure occurs.

[3]

Not related to this branch, but I re-raise a point I had made when this code was first put up for review. It'd be good to factor out ComputerInfo._fetch_cloud_meta_data into a standalone fetch_cloud_meta_data function (perhaps living in landscape.lib.cloud). This has the following advantages:

- it makes it possible to test fetch_cloud_meta_data in isolation

- it makes ComputerInfo info simpler, and it avoids the temptation of spreading stateful information around, like this branch is doing by updating self._check_cloud and self._cloud_retries inside ComputerInfo._fetch_cloud_meta_data

As a general note, keeping state generally makes code more complicated, so prefer passing data around, which is more explicit (you know exactly what the context is), and if keeping state is absolutely necessary (like with the retries count), try to keep all uses of such state close to each others (like checking and updating in the same spot).

review: Needs Fixing
Revision history for this message
Chad Smith (chad.smith) wrote :

Free, I'm still working the refactor of some of the fetch_cloud_meta_data callbacks into a separate lib that encapsulates the retries as well. May have questions for you tomorrow.

Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

I'm not totally sure encapsulating retries is a good idea, since you'll want some delay between subsequent attempts and that would be a bottle neck for calling code (the computer info plugin). I believe the current retry logic in the computer info plugin is good enough (we don't have any other use case and don't foresee any).

Revision history for this message
Chad Smith (chad.smith) wrote :

Hi Free,

   I think I have addressed your review comments. I have now separated the fetch_ec2_meta_data functions out into their own very simple cloud.py library with local unit tests.

I have left a number of computerinfo meta-data tests intact as they represented integration tests using the results of fetch_ec2_meta_data. I have also boiled down a bit of the existing computerinfo meta-data unit tests, dropping some duplicated add_query_results which are handled by the test class' setUp method.

Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Nice work Chad, a few other minor points, but looks good. +1!

[4]

+def fetch_ec2_item(path, accumulate, fetch=None):

Since this is an internal helper, not relevant to cloud.py consumers would you please make it private? And move it to the bottom of the file (you can still keep the unit tests and just mark them as wb, e.g. test_wb_fetch_ec2_item_error_returns_failure).

See also:

https://wiki.canonical.com/Landscape/SpecRegistry/0009#Public_and_private

[5]

+def fetch_ec2_meta_data(fetch=None):

Please document the fetch parameter and mention that it's there for testing purposes.

[6]

+ def _unicode_none(value):
+ if value is None:
+ return None
+ else:
+ return value.decode("utf-8")

This could be simply:

        def _unicode_none(value):
            if value is not None:
                return value.decode("utf-8")

I'd also rename it to unicode_or_none (no need to make it private since it's local scope).

[7]

+ logging.info("Acquired cloud meta-data.")

It's generally better to keep logging functionality outside of library functions, this grants a bit more flexibility to the calling code, that can decide what, when and how to log (and indeed computerinfo.py is where the rest of the logging is done). This is minor, so feel free to address it or not.

[8]

+METADATA_RETRY_MAX = 3 # Number of retries to get EC2 meta-data

This should probably be moved to computerinfo.py, since it's where it's used (while cloud.py ignores it).

[9]

+ else:
+ logging.warning(
+ "Temporary failure accessing cloud meta-data, retrying.")

There's no need to log retries I think, especially because non-cloud clients will always get this message.

[10]

+ log_failure(
+ error, msg=(
+ "Max retries reached querying meta-data. %s" %
+ error.getErrorMessage()))

I don't think it should be logged as an error, as this just fine for non-cloud clients. I'd simply use logging.info with a message like "No cloud meta-data available.".

review: Approve
726. By Chad Smith

address free's remaining comments:
 - fetch_ec2_item made private
 - METADATA_RETRY_MAX moved from lib/cloud into computerinfo.py
 - drop retry logging message and handle all logging on computerinfo
 - metadata query failure is not a real failure, just info message for non-cloud computers

Revision history for this message
Chris Glass (tribaal) wrote :

Looks good! +1 with a couple of refactoring/renaming comments:

[1]
I feel the term "cloud" is a little overused everywhere, making things less obvious that they could be. In the future I suppose we will add meta data for more than just EC2, so how about:
- Renaming _fetch_cloud_meta_data to _fetch_and_log_ec2_data (or similar) as this is really an EC2 specific method.
- Changing the logs to say "EC2" instead of "Cloud" in the aforementioned method

[2]
I would like to keep the message content gathering and creation logic in _create_computer_info_message. This would simply mean moving

+ if (self._cloud_meta_data is None and
+ self._cloud_retries < METADATA_RETRY_MAX):
+ self._cloud_meta_data = yield self._fetch_cloud_meta_data()

to _create_computer_info_message, and marking _create_computer_info_message as @inlineCallback instead of send_computer_message

If this is done, I'm not certain it's useful to keep self._cloud_meta_data (maybe a local variable is sufficient inside _create_computer_info_message)

Then in the future we can then easily extend the functionality by adding _fetch_and_log_<whatever>_data() methods to the class and hooking it into _create_computer_info_message.

review: Approve
Revision history for this message
Chad Smith (chad.smith) wrote :

thanks Chris
 [1] addressed
  agree with point [2] addressed, though we still need the persistent self._cloud_meta_data across message exchanges to prevent us from trying to pull cloud information during subsequent exchanges if we already have it.

727. By Chad Smith

fetch_cloud_meta_data -> fetch_ec2_meta_data

728. By Chad Smith

shuffle METADATA_RETRY_MAX logic and fetch_ec2_metadata from send_computer_message and into _create_computer_message

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'landscape/lib/cloud.py'
--- landscape/lib/cloud.py 1970-01-01 00:00:00 +0000
+++ landscape/lib/cloud.py 2013-09-08 12:39:26 +0000
@@ -0,0 +1,45 @@
1from landscape.lib.fetch import fetch_async
2
3EC2_HOST = "169.254.169.254"
4EC2_API = "http://%s/latest" % (EC2_HOST,)
5
6
7def fetch_ec2_meta_data(fetch=None):
8 """Fetch EC2 information about the cloud instance.
9
10 The C{fetch} parameter provided above is non-mocker testing purposes.
11 """
12 cloud_data = []
13 # We're not using a DeferredList here because we want to keep the
14 # number of connections to the backend minimal. See lp:567515.
15 deferred = _fetch_ec2_item("instance-id", cloud_data, fetch)
16 deferred.addCallback(
17 lambda ignore: _fetch_ec2_item("instance-type", cloud_data, fetch))
18 deferred.addCallback(
19 lambda ignore: _fetch_ec2_item("ami-id", cloud_data, fetch))
20
21 def return_result(ignore):
22 """Record the instance data returned by the EC2 API."""
23
24 def _unicode_or_none(value):
25 if value is not None:
26 return value.decode("utf-8")
27
28 (instance_id, instance_type, ami_id) = cloud_data
29 return {
30 "instance-id": _unicode_or_none(instance_id),
31 "ami-id": _unicode_or_none(ami_id),
32 "instance-type": _unicode_or_none(instance_type)}
33 deferred.addCallback(return_result)
34 return deferred
35
36
37def _fetch_ec2_item(path, accumulate, fetch=None):
38 """
39 Get data at C{path} on the EC2 API endpoint, and add the result to the
40 C{accumulate} list. The C{fetch} parameter is provided for testing only.
41 """
42 url = EC2_API + "/meta-data/" + path
43 if fetch is None:
44 fetch = fetch_async
45 return fetch(url).addCallback(accumulate.append)
046
=== added file 'landscape/lib/tests/test_cloud.py'
--- landscape/lib/tests/test_cloud.py 1970-01-01 00:00:00 +0000
+++ landscape/lib/tests/test_cloud.py 2013-09-08 12:39:26 +0000
@@ -0,0 +1,107 @@
1from landscape.lib.cloud import (EC2_API, _fetch_ec2_item, fetch_ec2_meta_data)
2from landscape.lib.fetch import HTTPCodeError, PyCurlError
3from landscape.tests.helpers import LandscapeTest
4from twisted.internet.defer import succeed, fail
5
6
7class CloudTest(LandscapeTest):
8
9 def setUp(self):
10 LandscapeTest.setUp(self)
11 self.query_results = {}
12
13 def fetch_stub(url):
14 value = self.query_results[url]
15 if isinstance(value, Exception):
16 return fail(value)
17 else:
18 return succeed(value)
19
20 self.fetch_func = fetch_stub
21 self.add_query_result("instance-id", "i00001")
22 self.add_query_result("ami-id", "ami-00002")
23 self.add_query_result("instance-type", "hs1.8xlarge")
24
25 def add_query_result(self, name, value):
26 """
27 Add a url to self.query_results that is then available through
28 self.fetch_func.
29 """
30 url = "%s/meta-data/%s" % (EC2_API, name)
31 self.query_results[url] = value
32
33 def test_fetch_ec2_meta_data_error_on_any_item_error(self):
34 """
35 L{_fetch_ec2_meta_data} returns a deferred C{Failure} containing the
36 error message when an error occurs on any of the queried meta-data
37 items C{instance-id}, C{ami-id} or C{instance-type}.
38 """
39 self.log_helper.ignore_errors(HTTPCodeError)
40 error = HTTPCodeError(404, "notfound")
41 metadata_items = ["instance-id", "ami-id", "instance-type"]
42 for item in metadata_items:
43 # reset all item data adding the error to only 1 item per iteration
44 for setup_item in metadata_items:
45 if setup_item == item:
46 self.add_query_result(item, error)
47 else:
48 self.add_query_result(setup_item, "value%s" % setup_item)
49
50 deferred = fetch_ec2_meta_data(fetch=self.fetch_func)
51 failure = self.failureResultOf(deferred)
52 self.assertEqual(
53 "Server returned HTTP code 404",
54 failure.getErrorMessage())
55
56 def test_fetch_ec2_meta_data(self):
57 """
58 L{_fetch_ec2_meta_data} returns a C{dict} containing meta-data for
59 C{instance-id}, C{ami-id} and C{instance-type}.
60 """
61 deferred = fetch_ec2_meta_data(fetch=self.fetch_func)
62 result = self.successResultOf(deferred)
63 self.assertEqual(
64 {"ami-id": u"ami-00002",
65 "instance-id": u"i00001",
66 "instance-type": u"hs1.8xlarge"},
67 result)
68
69 def test_fetch_ec2_meta_data_utf8(self):
70 """
71 L{_fetch_ec2_meta_data} decodes utf-8 strings returned from the
72 external service.
73 """
74 self.add_query_result("ami-id", "asdf\xe1\x88\xb4")
75 deferred = fetch_ec2_meta_data(fetch=self.fetch_func)
76 result = self.successResultOf(deferred)
77 self.assertEqual({"instance-id": u"i00001",
78 "ami-id": u"asdf\u1234",
79 "instance-type": u"hs1.8xlarge"},
80 result)
81
82 def test_wb_fetch_ec2_item_multiple_items_appends_accumulate_list(self):
83 """
84 L{_fetch_ec2_item} retrieves individual meta-data items from the
85 EC2 api and appends them to the C{list} provided by the C{accumulate}
86 parameter.
87 """
88 accumulate = []
89 self.successResultOf(
90 _fetch_ec2_item("instance-id", accumulate, fetch=self.fetch_func))
91 self.successResultOf(
92 _fetch_ec2_item(
93 "instance-type", accumulate, fetch=self.fetch_func))
94 self.assertEqual(["i00001", "hs1.8xlarge"], accumulate)
95
96 def test_wb_fetch_ec2_item_error_returns_failure(self):
97 """
98 L{_fetch_ec2_item} returns a deferred C{Failure} containing the error
99 message when faced with no EC2 cloud API service.
100 """
101 self.log_helper.ignore_errors(PyCurlError)
102 self.add_query_result("other-id", PyCurlError(60, "pycurl error"))
103 accumulate = []
104 deferred = _fetch_ec2_item(
105 "other-id", accumulate, fetch=self.fetch_func)
106 failure = self.failureResultOf(deferred)
107 self.assertEqual("Error 60: pycurl error", failure.getErrorMessage())
0108
=== modified file 'landscape/monitor/computerinfo.py'
--- landscape/monitor/computerinfo.py 2013-08-23 21:06:26 +0000
+++ landscape/monitor/computerinfo.py 2013-09-08 12:39:26 +0000
@@ -1,16 +1,15 @@
1import os1import os
2import logging2import logging
3from twisted.internet.defer import inlineCallbacks3from twisted.internet.defer import inlineCallbacks, returnValue
44
5from landscape.lib.fetch import fetch_async5from landscape.lib.fetch import fetch_async
6from landscape.lib.fs import read_file6from landscape.lib.fs import read_file
7from landscape.lib.log import log_failure
8from landscape.lib.lsb_release import LSB_RELEASE_FILENAME, parse_lsb_release7from landscape.lib.lsb_release import LSB_RELEASE_FILENAME, parse_lsb_release
8from landscape.lib.cloud import fetch_ec2_meta_data
9from landscape.lib.network import get_fqdn9from landscape.lib.network import get_fqdn
10from landscape.monitor.plugin import MonitorPlugin10from landscape.monitor.plugin import MonitorPlugin
1111
12EC2_HOST = "169.254.169.254"12METADATA_RETRY_MAX = 3 # Number of retries to get EC2 meta-data
13EC2_API = "http://%s/latest" % (EC2_HOST,)
1413
1514
16class DistributionInfoError(Exception):15class DistributionInfoError(Exception):
@@ -32,6 +31,7 @@
32 self._lsb_release_filename = lsb_release_filename31 self._lsb_release_filename = lsb_release_filename
33 self._root_path = root_path32 self._root_path = root_path
34 self._cloud_meta_data = None33 self._cloud_meta_data = None
34 self._cloud_retries = 0
35 self._fetch_async = fetch_async35 self._fetch_async = fetch_async
3636
37 def register(self, registry):37 def register(self, registry):
@@ -44,10 +44,7 @@
4444
45 @inlineCallbacks45 @inlineCallbacks
46 def send_computer_message(self, urgent=False):46 def send_computer_message(self, urgent=False):
47 if self._cloud_meta_data is None:47 message = yield self._create_computer_info_message()
48 self._cloud_meta_data = yield self._fetch_cloud_meta_data()
49
50 message = self._create_computer_info_message()
51 if message:48 if message:
52 message["type"] = "computer-info"49 message["type"] = "computer-info"
53 logging.info("Queueing message with updated computer info.")50 logging.info("Queueing message with updated computer info.")
@@ -69,6 +66,7 @@
69 broker.call_if_accepted("distribution-info",66 broker.call_if_accepted("distribution-info",
70 self.send_distribution_message, urgent)67 self.send_distribution_message, urgent)
7168
69 @inlineCallbacks
72 def _create_computer_info_message(self):70 def _create_computer_info_message(self):
73 message = {}71 message = {}
74 self._add_if_new(message, "hostname",72 self._add_if_new(message, "hostname",
@@ -83,12 +81,16 @@
83 meta_data[key] = read_file(81 meta_data[key] = read_file(
84 os.path.join(self._meta_data_path, key))82 os.path.join(self._meta_data_path, key))
8583
84 if (self._cloud_meta_data is None and
85 self._cloud_retries < METADATA_RETRY_MAX):
86 self._cloud_meta_data = yield self._fetch_ec2_meta_data()
87
86 if self._cloud_meta_data:88 if self._cloud_meta_data:
87 meta_data = dict(89 meta_data = dict(
88 meta_data.items() + self._cloud_meta_data.items())90 meta_data.items() + self._cloud_meta_data.items())
89 if meta_data:91 if meta_data:
90 self._add_if_new(message, "meta-data", meta_data)92 self._add_if_new(message, "meta-data", meta_data)
91 return message93 returnValue(message)
9294
93 def _add_if_new(self, message, key, value):95 def _add_if_new(self, message, key, value):
94 if value != self._persist.get(key):96 if value != self._persist.get(key):
@@ -122,47 +124,22 @@
122 message.update(parse_lsb_release(self._lsb_release_filename))124 message.update(parse_lsb_release(self._lsb_release_filename))
123 return message125 return message
124126
125 def _fetch_data(self, path, accumulate):127 def _fetch_ec2_meta_data(self):
126 """
127 Get data at C{path} on the EC2 API endpoint, and add the result to the
128 C{accumulate} list.
129 """
130 url = EC2_API + "/meta-data/" + path
131 logging.info("Queueing url fetch %s." % url)
132 return self._fetch_async(url).addCallback(accumulate.append)
133
134 def _fetch_cloud_meta_data(self):
135 """Fetch information about the cloud instance."""128 """Fetch information about the cloud instance."""
136 cloud_data = []129 if self._cloud_retries == 0:
137 # We're not using a DeferredList here because we want to keep the130 logging.info("Querying cloud meta-data.")
138 # number of connections to the backend minimal. See lp:567515.131 deferred = fetch_ec2_meta_data(self._fetch_async)
139 deferred = self._fetch_data("instance-id", cloud_data)132
140 deferred.addCallback(133 def log_no_meta_data_found(error):
141 lambda ignore:134 self._cloud_retries += 1
142 self._fetch_data("instance-type", cloud_data))135 if self._cloud_retries >= METADATA_RETRY_MAX:
143 deferred.addCallback(136 logging.info("No cloud meta-data available. %s" %
144 lambda ignore:137 error.getErrorMessage())
145 self._fetch_data("ami-id", cloud_data))138
146139 def log_success(result):
147 def store_data(ignore):140 logging.info("Acquired cloud meta-data.")
148 """Record the instance data returned by the EC2 API."""141 return result
149142
150 def _unicode_none(value):143 deferred.addCallback(log_success)
151 if value is None:144 deferred.addErrback(log_no_meta_data_found)
152 return None
153 else:
154 return value.decode("utf-8")
155
156 (instance_id, instance_type, ami_id) = cloud_data
157 return {
158 "instance-id": _unicode_none(instance_id),
159 "instance-type": _unicode_none(instance_type),
160 "ami-id": _unicode_none(ami_id)}
161
162 def log_error(error):
163 log_failure(error, msg="Got error while fetching meta-data: %r"
164 % (error.value,))
165
166 deferred.addCallback(store_data)
167 deferred.addErrback(log_error)
168 return deferred145 return deferred
169146
=== modified file 'landscape/monitor/tests/test_computerinfo.py'
--- landscape/monitor/tests/test_computerinfo.py 2013-08-23 21:06:26 +0000
+++ landscape/monitor/tests/test_computerinfo.py 2013-09-08 12:39:26 +0000
@@ -3,9 +3,9 @@
33
4from twisted.internet.defer import succeed, fail, inlineCallbacks4from twisted.internet.defer import succeed, fail, inlineCallbacks
55
6from landscape.lib.fetch import HTTPCodeError6from landscape.lib.fetch import HTTPCodeError, PyCurlError
7from landscape.lib.fs import create_file7from landscape.lib.fs import create_file
8from landscape.monitor.computerinfo import ComputerInfo8from landscape.monitor.computerinfo import ComputerInfo, METADATA_RETRY_MAX
9from landscape.tests.helpers import LandscapeTest, MonitorHelper9from landscape.tests.helpers import LandscapeTest, MonitorHelper
10from landscape.tests.mocker import ANY10from landscape.tests.mocker import ANY
1111
@@ -405,9 +405,6 @@
405 def test_with_cloud_info(self):405 def test_with_cloud_info(self):
406 """Fetch cloud information"""406 """Fetch cloud information"""
407 self.config.cloud = True407 self.config.cloud = True
408 self.add_query_result("instance-id", "i00001")
409 self.add_query_result("ami-id", "ami-00002")
410 self.add_query_result("instance-type", "hs1.8xlarge")
411 self.mstore.set_accepted_types(["computer-info"])408 self.mstore.set_accepted_types(["computer-info"])
412409
413 plugin = ComputerInfo(fetch_async=self.fetch_func)410 plugin = ComputerInfo(fetch_async=self.fetch_func)
@@ -421,47 +418,87 @@
421 "instance-type": u"hs1.8xlarge"},418 "instance-type": u"hs1.8xlarge"},
422 messages[0]["meta-data"])419 messages[0]["meta-data"])
423420
421 def test_no_fetch_ec2_meta_data_when_cloud_retries_is_max(self):
422 """
423 Do not fetch EC2 info when C{_cloud_retries} is C{METADATA_RETRY_MAX}
424 """
425 self.config.cloud = True
426 self.mstore.set_accepted_types(["computer-info"])
427
428 plugin = ComputerInfo(fetch_async=self.fetch_func)
429 plugin._cloud_retries = METADATA_RETRY_MAX
430 self.monitor.add(plugin)
431 plugin.exchange()
432 messages = self.mstore.get_pending_messages()
433 self.assertEqual(1, len(messages))
434 self.assertNotIn("meta-data", messages[0])
435
424 @inlineCallbacks436 @inlineCallbacks
425 def test_fetch_cloud_meta_data(self):437 def test_fetch_ec2_meta_data(self):
426 """438 """
427 L{_fetch_cloud_meta_data} retrieves instance information from the439 L{_fetch_ec2_meta_data} retrieves instance information from the
428 EC2 api.440 EC2 api.
429 """441 """
430 self.add_query_result("instance-id", "i00001")
431 self.add_query_result("ami-id", "ami-00002")
432 self.add_query_result("instance-type", "hs1.8xlarge")
433
434 plugin = ComputerInfo(fetch_async=self.fetch_func)442 plugin = ComputerInfo(fetch_async=self.fetch_func)
435 result = yield plugin._fetch_cloud_meta_data()443 result = yield plugin._fetch_ec2_meta_data()
436 self.assertEqual({"instance-id": u"i00001", "ami-id": u"ami-00002",444 self.assertEqual({"instance-id": u"i00001", "ami-id": u"ami-00002",
437 "instance-type": u"hs1.8xlarge"}, result)445 "instance-type": u"hs1.8xlarge"}, result)
438446 self.assertEqual(
439 @inlineCallbacks447 " INFO: Querying cloud meta-data.\n"
440 def test_fetch_cloud_meta_data_bad_result(self):448 " INFO: Acquired cloud meta-data.\n",
441 """449 self.logfile.getvalue())
442 L{_fetch_cloud_meta_data} returns C{None} when faced with errors from450
443 the EC2 api.451 @inlineCallbacks
444 """452 def test_fetch_ec2_meta_data_no_cloud_api_max_retry(self):
445 self.log_helper.ignore_errors(HTTPCodeError)453 """
446 self.add_query_result("instance-id", "i7337")454 L{_fetch_ec2_meta_data} returns C{None} when faced with no EC2 cloud
447 self.add_query_result("ami-id", HTTPCodeError(404, "notfound"))455 API service and reports the specific C{PyCurlError} upon message
448 self.add_query_result("instance-type", "hs1.8xlarge")456 exchange when L{_cloud_retries} equals C{METADATA_RETRY_MAX}.
449 plugin = ComputerInfo(fetch_async=self.fetch_func)457 """
450 result = yield plugin._fetch_cloud_meta_data()458 self.log_helper.ignore_errors(PyCurlError)
451 self.assertEqual(None, result)459 self.add_query_result("instance-id", PyCurlError(60, "pycurl error"))
452460 plugin = ComputerInfo(fetch_async=self.fetch_func)
453 @inlineCallbacks461 plugin._cloud_retries = METADATA_RETRY_MAX
454 def test_fetch_cloud_meta_data_utf8(self):462 result = yield plugin._fetch_ec2_meta_data()
455 """463 self.assertIn(
456 L{_fetch_cloud_meta_data} decodes utf-8 strings returned from the464 "INFO: No cloud meta-data available. "
457 external service.465 "Error 60: pycurl error\n", self.logfile.getvalue())
458 """466 self.assertEqual(None, result)
459 self.add_query_result("instance-id", "i00001")467
460 self.add_query_result("ami-id", "asdf\xe1\x88\xb4")468 @inlineCallbacks
461 self.add_query_result("instance-type", "m1.large")469 def test_fetch_ec2_meta_data_bad_result_max_retry(self):
462 plugin = ComputerInfo(fetch_async=self.fetch_func)470 """
463 result = yield plugin._fetch_cloud_meta_data()471 L{_fetch_ec2_meta_data} returns C{None} and logs an error when
464 self.assertEqual({"instance-id": u"i00001",472 crossing the retry threshold C{METADATA_RETRY_MAX}.
465 "ami-id": u"asdf\u1234",473 """
466 "instance-type": u"m1.large"},474 self.log_helper.ignore_errors(HTTPCodeError)
475 self.add_query_result("ami-id", HTTPCodeError(404, "notfound"))
476 plugin = ComputerInfo(fetch_async=self.fetch_func)
477 plugin._cloud_retries = METADATA_RETRY_MAX
478 result = yield plugin._fetch_ec2_meta_data()
479 self.assertIn(
480 "INFO: No cloud meta-data available. Server returned "
481 "HTTP code 404",
482 self.logfile.getvalue())
483 self.assertEqual(None, result)
484
485 @inlineCallbacks
486 def test_fetch_ec2_meta_data_bad_result_retry(self):
487 """
488 L{_fetch_ec2_meta_data} returns C{None} when faced with spurious
489 errors from the EC2 api. The method increments L{_cloud_retries}
490 counter which allows L{_fetch_ec2_meta_data} to run again next
491 message exchange.
492 """
493 self.log_helper.ignore_errors(HTTPCodeError)
494 self.add_query_result("ami-id", HTTPCodeError(404, "notfound"))
495 plugin = ComputerInfo(fetch_async=self.fetch_func)
496 result = yield plugin._fetch_ec2_meta_data()
497 self.assertEqual(1, plugin._cloud_retries)
498 self.assertEqual(None, result)
499 # Fix the error condition for the retry.
500 self.add_query_result("ami-id", "ami-00002")
501 result = yield plugin._fetch_ec2_meta_data()
502 self.assertEqual({"instance-id": u"i00001", "ami-id": u"ami-00002",
503 "instance-type": u"hs1.8xlarge"},
467 result)504 result)

Subscribers

People subscribed via source and target branches

to all changes: