Merge ~smoser/cloud-init:bug/1683038-ec2-no-warn-on-explicit into cloud-init:master

Proposed by Scott Moser
Status: Work in progress
Proposed branch: ~smoser/cloud-init:bug/1683038-ec2-no-warn-on-explicit
Merge into: cloud-init:master
Diff against target: 373 lines (+318/-10)
2 files modified
cloudinit/sources/DataSourceEc2.py (+44/-10)
tests/unittests/test_datasource/test_ec2.py (+274/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Chad Smith Approve
cloud-init Commiters Pending
Review via email: mp+324274@code.launchpad.net

Commit message

Ec2: do not warn user of strict if Ec2 is explicitly set.

If the system is configured with datasource_list of either just
'Ec2' or 'Ec2' and 'None', then consider this as if they had set
strict_id = true. A change is made to the signature of
'warn_if_necessary' to make unit testing easier.

Additionally, fix a bug in ds-identify. If the user configured:
  datasource_list: ["Ec2", "None"]
then ds-identify would write
  datasource_list: ["Ec2", "None", "None"]
which would break the logic to avoid warning.

LP: #1683038

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

Generally looks fine, one question w.r.t confirming explict_dslist check

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

On Mon, 22 May 2017, Ryan Harper wrote:

> Generally looks fine, one question w.r.t confirming explict_dslist check
>
> Diff comments:
>
> > diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
> > index 2f9c7ed..818a639 100644
> > --- a/cloudinit/sources/DataSourceEc2.py
> > +++ b/cloudinit/sources/DataSourceEc2.py
> > @@ -266,6 +267,12 @@ def parse_strict_mode(cfgval):
> > return mode, sleep
> >
> >
> > +def _is_explicit_dslist(dslist):
> > + if 'Ec2' not in dslist:
> > + return False
> > + return (len(dslist) == 1 or (len(dslist) == 2 and 'None' in dslist))
>
> if we know dslist is len == 2, then We could ensure it looks like:
> ['Ec2', None]
>
> Right? That's exactly what we're looking for? Verus this [None, 'Ec2']
> which would pass this test but not be what we're looking for IIUC

Yeah, I avoided checking for 'Ec2' because if this datasoruce is running,
then Ec2 is in the list. But I think it makes more sense to be explicit
Will cahnge as you sigguested.
  if dslist == ['Ec2', 'None'] or dslist == ['Ec2']

sm

Revision history for this message
Scott Moser (smoser) wrote :

some responses in line. i'll fix some things too. thanks for review.

Revision history for this message
Scott Moser (smoser) wrote :

I've pushed some new commits, please see and review.
thanks.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
review: Approve
Revision history for this message
Chad Smith (chad.smith) wrote :

pre-approved on assumption of a new unit test for behavior, if the following branch[1] lands before this one you could create a CiTestCase setting with_logs = True and self.assertIn(" not warning", self.logs.getvalue())

References:
[1] https://code.launchpad.net/~chad.smith/cloud-init/+git/cloud-init/+merge/324450

Revision history for this message
Scott Moser (smoser) wrote :

I split the ds-identify portion out into
 https://code.launchpad.net/~smoser/cloud-init/+git/cloud-init/+merge/324557
that way i can get it in now, and fix the ec2 warn portion itself with a nice test.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
e8fcbfd... by Scott Moser

Add a docstring for _collect_platform_data.

c4d0d40... by Scott Moser

initial test_ec2.py

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
092ea40... by Scott Moser

Add test for warn_if_necessary.

Change here also changes function signature of warn_if_necessary
to allow for more direct testing without using 'activate.'

Revision history for this message
Scott Moser (smoser) wrote :

I have to think some more on the 'FIXME' bits here.
But i'd like feedback on teh added unit tests.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
a247285... by Scott Moser

add warning on unexpected situation strict_id=true and platform unknown.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

I think that the signature change is fine.
the other fixme I think is best fixed by just having ds-identify write an entry in the config that says that it wrote a datasource_list, and how it did that.

Ie, it could declare that it essentially copied the datasource list like this:
   di_datasource_list_method: copy

or something to that affect. Then cloud-init could read that. A 'copy' there would be different than 'search'. The copy would show no warning, but the search would.

I'm not sure stil..
We *should* grab the other parts of this merge proposal and get them into main.

Revision history for this message
Scott Moser (smoser) wrote :

I grabbed the initial test portions of this merge proposal and put them into another
at https://code.launchpad.net/~smoser/cloud-init/+git/cloud-init/+merge/327534
note, those do not have the tests for WarnIfNecessary

Unmerged commits

a247285... by Scott Moser

add warning on unexpected situation strict_id=true and platform unknown.

092ea40... by Scott Moser

Add test for warn_if_necessary.

Change here also changes function signature of warn_if_necessary
to allow for more direct testing without using 'activate.'

c4d0d40... by Scott Moser

initial test_ec2.py

e8fcbfd... by Scott Moser

Add a docstring for _collect_platform_data.

e358e2f... by Scott Moser

Ec2: do not warn user of strict if Ec2 is explicitly set.

If the system is configured with datasource_list of either just
'Ec2' or 'Ec2' and 'None', then consider this as if they had set
strict_id = true.

LP: #1683038

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py
index 9e2fdc0..d0a2f8e 100644
--- a/cloudinit/sources/DataSourceEc2.py
+++ b/cloudinit/sources/DataSourceEc2.py
@@ -69,7 +69,8 @@ class DataSourceEc2(sources.DataSource):
69 LOG.debug("strict_mode: %s, cloud_platform=%s",69 LOG.debug("strict_mode: %s, cloud_platform=%s",
70 strict_mode, self.cloud_platform)70 strict_mode, self.cloud_platform)
71 if strict_mode == "true" and self.cloud_platform == Platforms.UNKNOWN:71 if strict_mode == "true" and self.cloud_platform == Platforms.UNKNOWN:
72 return False72 if not _is_explicit_dslist(self.sys_cfg.get('datasource_list')):
73 return False
73 elif self.cloud_platform == Platforms.NO_EC2_METADATA:74 elif self.cloud_platform == Platforms.NO_EC2_METADATA:
74 return False75 return False
7576
@@ -229,10 +230,15 @@ class DataSourceEc2(sources.DataSource):
229 def activate(self, cfg, is_new_instance):230 def activate(self, cfg, is_new_instance):
230 if not is_new_instance:231 if not is_new_instance:
231 return232 return
232 if self.cloud_platform == Platforms.UNKNOWN:233 # FIXME: Think about this function signature change.
233 warn_if_necessary(234 # This change allows 'warn_if_necessary' unit test to do more
234 util.get_cfg_by_path(cfg, STRICT_ID_PATH, STRICT_ID_DEFAULT),235 # without testing 'activate'. I need to test though the upgrade
235 cfg)236 # adn reboot path where an old obj.pkl file has 'activate' called
237 # on it. I think it is fine... as the new code will have a
238 # cloud_platform on it already.
239 warn_if_necessary(
240 util.get_cfg_by_path(cfg, STRICT_ID_PATH, STRICT_ID_DEFAULT),
241 cfg, platform=self.cloud_platform)
236242
237243
238def read_strict_mode(cfgval, default):244def read_strict_mode(cfgval, default):
@@ -273,7 +279,17 @@ def parse_strict_mode(cfgval):
273 return mode, sleep279 return mode, sleep
274280
275281
276def warn_if_necessary(cfgval, cfg):282def _is_explicit_dslist(dslist):
283 """Is dslist explicitly configured to search Ec2 datasource only?"""
284 if not dslist:
285 return False
286 return dslist in (['Ec2'], ['Ec2', 'None'])
287
288
289def warn_if_necessary(cfgval, cfg, platform):
290 if platform != Platforms.UNKNOWN:
291 return
292
277 try:293 try:
278 mode, sleep = parse_strict_mode(cfgval)294 mode, sleep = parse_strict_mode(cfgval)
279 except ValueError as e:295 except ValueError as e:
@@ -283,6 +299,18 @@ def warn_if_necessary(cfgval, cfg):
283 if mode == "false":299 if mode == "false":
284 return300 return
285301
302 dslist = cfg.get('datasource_list')
303 if _is_explicit_dslist(cfg.get('datasource_list')):
304 LOG.debug("mode=%s but explicit dslist (%s), not warning.",
305 mode, dslist)
306 return
307
308 if mode == "true":
309 # Unknown platform and strict is true. get_data should have
310 # returned false.
311 LOG.warning("Unexpected error. strict_mode=%s platform=%s (%s)",
312 mode, platform, cfgval)
313
286 warnings.show_warning('non_ec2_md', cfg, mode=True, sleep=sleep)314 warnings.show_warning('non_ec2_md', cfg, mode=True, sleep=sleep)
287315
288316
@@ -316,10 +344,16 @@ def identify_platform():
316344
317345
318def _collect_platform_data():346def _collect_platform_data():
319 # returns a dictionary with all lower case values:347 """Returns a dictionary of platform info from dmi or /sys/hypervisor.
320 # uuid: system-uuid from dmi or /sys/hypervisor348
321 # uuid_source: 'hypervisor' (/sys/hypervisor/uuid) or 'dmi'349 Keys in the dictionary are as follows:
322 # serial: dmi 'system-serial-number' (/sys/.../product_serial)350 uuid: system-uuid from dmi or /sys/hypervisor
351 uuid_source: 'hypervisor' (/sys/hypervisor/uuid) or 'dmi'
352 serial: dmi 'system-serial-number' (/sys/.../product_serial)
353
354 On Ec2 instances experimentation is that product_serial is upper case,
355 and product_uuid is lower case. This returns lower case values for both.
356 """
323 data = {}357 data = {}
324 try:358 try:
325 uuid = util.load_file("/sys/hypervisor/uuid").strip()359 uuid = util.load_file("/sys/hypervisor/uuid").strip()
diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py
326new file mode 100644360new file mode 100644
index 0000000..443eda9
--- /dev/null
+++ b/tests/unittests/test_datasource/test_ec2.py
@@ -0,0 +1,274 @@
1# This file is part of cloud-init. See LICENSE file for license information.
2
3import httpretty
4import mock
5
6from .. import helpers as test_helpers
7from cloudinit import helpers
8from cloudinit.sources import DataSourceEc2 as ec2
9
10
11# collected from api version 2009-04-04/ with
12# python3 -c 'import json
13# from cloudinit.ec2_utils import get_instance_metadata as gm
14# print(json.dumps(gm("2009-04-04"), indent=1, sort_keys=True))'
15DEFAULT_METADATA = {
16 "ami-id": "ami-80861296",
17 "ami-launch-index": "0",
18 "ami-manifest-path": "(unknown)",
19 "block-device-mapping": {"ami": "/dev/sda1", "root": "/dev/sda1"},
20 "hostname": "ip-10-0-0-149",
21 "instance-action": "none",
22 "instance-id": "i-0052913950685138c",
23 "instance-type": "t2.micro",
24 "local-hostname": "ip-10-0-0-149",
25 "local-ipv4": "10.0.0.149",
26 "placement": {"availability-zone": "us-east-1b"},
27 "profile": "default-hvm",
28 "public-hostname": "",
29 "public-ipv4": "107.23.188.247",
30 "public-keys": {"brickies": ["ssh-rsa AAAAB3Nz....w== brickies"]},
31 "reservation-id": "r-00a2c173fb5782a08",
32 "security-groups": "wide-open"
33}
34
35
36def register_ssh_keys(rfunc, base_url, keys_data):
37 """handle ssh key inconsistencies.
38
39 public-keys in the ec2 metadata is inconsistently formated compared
40 to other entries.
41 Given keys_data of {name1: pubkey1, name2: pubkey2}
42
43 This registers the following urls:
44 base_url 0={name1}\n1={name2} # (for each name)
45 base_url/ 0={name1}\n1={name2} # (for each name)
46 base_url/0 openssh-key
47 base_url/0/ openssh-key
48 base_url/0/openssh-key {pubkey1}
49 base_url/0/openssh-key/ {pubkey1}
50 ...
51 """
52
53 base_url = base_url.rstrip("/")
54 odd_index = '\n'.join(
55 ["{0}={1}".format(n, name)
56 for n, name in enumerate(sorted(keys_data))])
57
58 rfunc(base_url, odd_index)
59 rfunc(base_url + "/", odd_index)
60
61 for n, name in enumerate(sorted(keys_data)):
62 val = keys_data[name]
63 if isinstance(val, list):
64 val = '\n'.join(val)
65 burl = base_url + "/%s" % n
66 rfunc(burl, "openssh-key")
67 rfunc(burl + "/", "openssh-key")
68 rfunc(burl + "/%s/openssh-key" % name, val)
69 rfunc(burl + "/%s/openssh-key/" % name, val)
70
71
72def register_mock_metaserver(base_url, data):
73 """Register with httpretty a ec2 metadata like service serving 'data'.
74
75 If given a dictionary, it will populate urls under base_url for
76 that dictionary. For example, input of
77 {"instance-id": "i-abc", "mac": "00:16:3e:00:00:00"}
78 populates
79 base_url with 'instance-id\nmac'
80 base_url/ with 'instance-id\nmac'
81 base_url/instance-id with i-abc
82 base_url/mac with 00:16:3e:00:00:00
83 """
84 def register_helper(register, base_url, body):
85 if isinstance(body, str):
86 register(base_url, body)
87 elif isinstance(body, list):
88 register(base_url.rstrip('/'), '\n'.join(body) + '\n')
89 register(base_url.rstrip('/') + '/', '\n'.join(body) + '\n')
90 elif isinstance(body, dict):
91 vals = []
92 for k, v in body.items():
93 if k == 'public-keys':
94 register_ssh_keys(
95 register, base_url.rstrip('/') + '/public-keys/', v)
96 continue
97 if isinstance(v, (str, list)):
98 suffix = k.rstrip('/')
99 else:
100 suffix = k.rstrip('/') + '/'
101 vals.append(suffix)
102 url = base_url.rstrip('/') + '/' + suffix
103 register_helper(register, url, v)
104 register(base_url.rstrip('/'), '\n'.join(vals) + '\n')
105 register(base_url.rstrip('/') + '/', '\n'.join(vals) + '\n')
106 elif body is None:
107 register(base_url.rstrip('/'), 'not found', status_code=404)
108
109 def myreg(*argc, **kwargs):
110 # print("register_url(%s, %s)" % (argc, kwargs))
111 return httpretty.register_uri(httpretty.GET, *argc, **kwargs)
112
113 register_helper(myreg, base_url, data)
114
115
116class TestEc2(test_helpers.HttprettyTestCase):
117 valid_platform_data = {
118 'uuid': 'ec212f79-87d1-2f1d-588f-d86dc0fd5412',
119 'uuid_source': 'dmi',
120 'serial': 'ec212f79-87d1-2f1d-588f-d86dc0fd5412',
121 }
122
123 def setUp(self):
124 super(TestEc2, self).setUp()
125 self.metadata_addr = ec2.DataSourceEc2.metadata_urls[0]
126 self.api_ver = '2009-04-04'
127
128 @property
129 def metadata_url(self):
130 return '/'.join([self.metadata_addr, self.api_ver, 'meta-data', ''])
131
132 @property
133 def userdata_url(self):
134 return '/'.join([self.metadata_addr, self.api_ver, 'user-data'])
135
136 def _patch_add_cleanup(self, mpath, *args, **kwargs):
137 p = mock.patch(mpath, *args, **kwargs)
138 p.start()
139 self.addCleanup(p.stop)
140
141 def _setup_ds(self, sys_cfg=None, platform_data=None, md=None, ud=None):
142 distro = {}
143 paths = helpers.Paths({})
144 if sys_cfg is None:
145 sys_cfg = {}
146 ds = ec2.DataSourceEc2(sys_cfg=sys_cfg, distro=distro, paths=paths)
147 if platform_data is not None:
148 self._patch_add_cleanup(
149 "cloudinit.sources.DataSourceEc2._collect_platform_data",
150 return_value=platform_data)
151
152 if md:
153 register_mock_metaserver(self.metadata_url, md)
154 register_mock_metaserver(self.userdata_url, ud)
155
156 return ds
157
158 @httpretty.activate
159 def test_valid_platform_with_strict_true(self):
160 """Valid platform data should return true with strict_id true."""
161 ds = self._setup_ds(
162 platform_data=self.valid_platform_data,
163 sys_cfg={'datasource': {'Ec2': {'strict_id': True}}},
164 md=DEFAULT_METADATA)
165 ret = ds.get_data()
166 self.assertEqual(True, ret)
167
168 @httpretty.activate
169 def test_valid_platform_with_strict_false(self):
170 """Valid platform data should return true with strict_id false."""
171 ds = self._setup_ds(
172 platform_data=self.valid_platform_data,
173 sys_cfg={'datasource': {'Ec2': {'strict_id': False}}},
174 md=DEFAULT_METADATA)
175 ret = ds.get_data()
176 self.assertEqual(True, ret)
177
178 @httpretty.activate
179 def test_unknown_platform_with_strict_true(self):
180 """Unknown platform data with strict_id true should return False."""
181 uuid = 'ab439480-72bf-11d3-91fc-b8aded755F9a'
182 ds = self._setup_ds(
183 platform_data={'uuid': uuid, 'uuid_source': 'dmi', 'serial': ''},
184 sys_cfg={'datasource': {'Ec2': {'strict_id': True}}},
185 md=DEFAULT_METADATA)
186 ret = ds.get_data()
187 self.assertEqual(False, ret)
188
189 @httpretty.activate
190 def test_unknown_platform_with_strict_false(self):
191 """Unknown platform data with strict_id false should return True."""
192 uuid = 'ab439480-72bf-11d3-91fc-b8aded755F9a'
193 ds = self._setup_ds(
194 platform_data={'uuid': uuid, 'uuid_source': 'dmi', 'serial': ''},
195 sys_cfg={'datasource': {'Ec2': {'strict_id': False}}},
196 md=DEFAULT_METADATA)
197 ret = ds.get_data()
198 self.assertEqual(True, ret)
199
200
201class TestWarnIfNecessary(test_helpers.CiTestCase):
202 """Test warn_if_necessary behaves as expected.
203
204 warn_if_necessary is called with the value of the 'strict_id' setting.
205 It should call show_warning if a warning should be shown."""
206
207 with_logs = True
208
209 @mock.patch("cloudinit.sources.DataSourceEc2.warnings.show_warning")
210 def test_no_warnings_if_platform_identified(self, m_show_warning):
211 """If platform is something known, then no warninings expected."""
212 ec2.warn_if_necessary("warn", cfg={}, platform=ec2.Platforms.AWS)
213 ec2.warn_if_necessary("warn", cfg={}, platform=ec2.Platforms.BRIGHTBOX)
214 ec2.warn_if_necessary("warn", cfg={}, platform=ec2.Platforms.SEEDED)
215 ec2.warn_if_necessary("warn", cfg={}, platform=ec2.Platforms.ALIYUN)
216 self.assertEqual(0, m_show_warning.call_count)
217
218 @mock.patch("cloudinit.sources.DataSourceEc2.warnings.show_warning")
219 def test_warn_if_platform_unknown_and_strict_true(self, m_show_warning):
220 """If platform is unknown and strict is warn, warnings expected."""
221 ec2.warn_if_necessary("warn", cfg={}, platform=ec2.Platforms.UNKNOWN)
222 self.assertEqual(1, m_show_warning.call_count)
223
224 @mock.patch("cloudinit.sources.DataSourceEc2.warnings.show_warning")
225 def test_no_warn_if_platform_unknown_and_not_strict(self, m_show_warning):
226 """If platform is unknown but strict is false, no warnings."""
227 ec2.warn_if_necessary("false", cfg={}, platform=ec2.Platforms.UNKNOWN)
228 self.assertEqual(0, m_show_warning.call_count)
229
230 @mock.patch("cloudinit.sources.DataSourceEc2.warnings.show_warning")
231 def test_no_warn_if_unknown_strict_true_explicit_ds(self, m_show_warning):
232 """No warnings on strict_id=warn, platform=unknown, ec2 only."""
233 ec2.warn_if_necessary("false", cfg={'datasource_list': ['Ec2']},
234 platform=ec2.Platforms.UNKNOWN)
235 self.assertEqual(0, m_show_warning.call_count)
236
237 @mock.patch("cloudinit.sources.DataSourceEc2.warnings.show_warning")
238 def test_no_warn_if_unknown_strict_true_exp_and_none(self, m_show_warning):
239 """No warnings on strict_id=warn, platform=unknown, Ec2, None.
240
241 FIXME: If ds-identify writes datasource_list due to
242 strict_id = warn and DS_MAYBE, will this code path
243 be taken? That *should* get a warning.
244
245 If datasource_list contains ['Ec2', 'None'], then no warning
246 should be seen as this is explicit configuration of Ec2."""
247 ec2.warn_if_necessary(
248 "warn", cfg={'datasource_list': ['Ec2', 'None']},
249 platform=ec2.Platforms.UNKNOWN)
250 self.assertEqual(0, m_show_warning.call_count)
251
252 @mock.patch("cloudinit.sources.DataSourceEc2.warnings.show_warning")
253 def test_warn_if_unknown_strict_true_noexp_and_none(self, m_show_warning):
254 """Warn if strict_id=warn, platform=unknown, but not explicit list.
255
256 This is not an 'explicit' configuration because it has extra
257 entry in its datasource_list.
258 """
259 ec2.warn_if_necessary(
260 "warn", cfg={'datasource_list': ['OpenStack', 'Ec2', 'None']},
261 platform=ec2.Platforms.UNKNOWN)
262 self.assertNotIn('Unexpected error', self.logs.getvalue())
263 self.assertEqual(1, m_show_warning.call_count)
264
265 @mock.patch("cloudinit.sources.DataSourceEc2.warnings.show_warning")
266 def test_additional_warn_on_strict_and_unknown(self, m_show_warning):
267 """If strict_id is true and platform is unknown, unexpected error."""
268 ec2.warn_if_necessary(
269 "true", cfg={}, platform=ec2.Platforms.UNKNOWN)
270 self.assertIn('Unexpected error', self.logs.getvalue())
271 self.assertEqual(1, m_show_warning.call_count)
272
273
274# vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches