Merge lp:~daniel-thewatkins/cloud-init/lp1403617 into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Dan Watkins on 2015-04-20
Status: Merged
Approved by: Joshua Harlow on 2015-04-21
Approved revision: 1096
Merged at revision: 1096
Proposed branch: lp:~daniel-thewatkins/cloud-init/lp1403617
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 191 lines (+95/-46)
2 files modified
cloudinit/sources/DataSourceGCE.py (+50/-42)
tests/unittests/test_datasource/test_gce.py (+45/-4)
To merge this branch: bzr merge lp:~daniel-thewatkins/cloud-init/lp1403617
Reviewer Review Type Date Requested Status
cloud-init commiters 2015-04-20 Pending
Review via email: mp+256812@code.launchpad.net
To post a comment you must log in.
Dan Watkins (daniel-thewatkins) wrote :

I've tested this on a GCE instance, and it works as expected.

Scott Moser (smoser) wrote :

My only real comment is that I thikn i'd take 'get_metadata' out of 'get_data'.
You can either move it to a stand alone method or a class method. either way, moving it out allows to easier patch it for testing.

1093. By Dan Watkins on 2015-04-20

Refactor GCE metadata fetching to use a helper class.

1094. By Dan Watkins on 2015-04-20

Rename found variable in GCE data source.

1095. By Dan Watkins on 2015-04-20

Support multiple metadata paths for metadata keys in GCE data source.

1096. By Dan Watkins on 2015-04-20

GCE instance-level SSH keys override project-level keys. (LP: #1403617)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/sources/DataSourceGCE.py'
2--- cloudinit/sources/DataSourceGCE.py 2015-02-26 00:40:33 +0000
3+++ cloudinit/sources/DataSourceGCE.py 2015-04-20 15:25:12 +0000
4@@ -30,6 +30,31 @@
5 REQUIRED_FIELDS = ('instance-id', 'availability-zone', 'local-hostname')
6
7
8+class GoogleMetadataFetcher(object):
9+ headers = {'X-Google-Metadata-Request': True}
10+
11+ def __init__(self, metadata_address):
12+ self.metadata_address = metadata_address
13+
14+ def get_value(self, path, is_text):
15+ value = None
16+ try:
17+ resp = url_helper.readurl(url=self.metadata_address + path,
18+ headers=self.headers)
19+ except url_helper.UrlError as exc:
20+ msg = "url %s raised exception %s"
21+ LOG.debug(msg, path, exc)
22+ else:
23+ if resp.code == 200:
24+ if is_text:
25+ value = util.decode_binary(resp.contents)
26+ else:
27+ value = resp.contents
28+ else:
29+ LOG.debug("url %s returned code %s", path, resp.code)
30+ return value
31+
32+
33 class DataSourceGCE(sources.DataSource):
34 def __init__(self, sys_cfg, distro, paths):
35 sources.DataSource.__init__(self, sys_cfg, distro, paths)
36@@ -50,17 +75,15 @@
37 return public_key
38
39 def get_data(self):
40- # GCE metadata server requires a custom header since v1
41- headers = {'X-Google-Metadata-Request': True}
42-
43 # url_map: (our-key, path, required, is_text)
44 url_map = [
45- ('instance-id', 'instance/id', True, True),
46- ('availability-zone', 'instance/zone', True, True),
47- ('local-hostname', 'instance/hostname', True, True),
48- ('public-keys', 'project/attributes/sshKeys', False, True),
49- ('user-data', 'instance/attributes/user-data', False, False),
50- ('user-data-encoding', 'instance/attributes/user-data-encoding',
51+ ('instance-id', ('instance/id',), True, True),
52+ ('availability-zone', ('instance/zone',), True, True),
53+ ('local-hostname', ('instance/hostname',), True, True),
54+ ('public-keys', ('project/attributes/sshKeys',
55+ 'instance/attributes/sshKeys'), False, True),
56+ ('user-data', ('instance/attributes/user-data',), False, False),
57+ ('user-data-encoding', ('instance/attributes/user-data-encoding',),
58 False, True),
59 ]
60
61@@ -69,40 +92,25 @@
62 LOG.debug("%s is not resolvable", self.metadata_address)
63 return False
64
65+ metadata_fetcher = GoogleMetadataFetcher(self.metadata_address)
66 # iterate over url_map keys to get metadata items
67- found = False
68- for (mkey, path, required, is_text) in url_map:
69- try:
70- resp = url_helper.readurl(url=self.metadata_address + path,
71- headers=headers)
72- if resp.code == 200:
73- found = True
74- if is_text:
75- self.metadata[mkey] = util.decode_binary(resp.contents)
76- else:
77- self.metadata[mkey] = resp.contents
78+ running_on_gce = False
79+ for (mkey, paths, required, is_text) in url_map:
80+ value = None
81+ for path in paths:
82+ new_value = metadata_fetcher.get_value(path, is_text)
83+ if new_value is not None:
84+ value = new_value
85+ if value:
86+ running_on_gce = True
87+ if required and value is None:
88+ msg = "required key %s returned nothing. not GCE"
89+ if not running_on_gce:
90+ LOG.debug(msg, mkey)
91 else:
92- if required:
93- msg = "required url %s returned code %s. not GCE"
94- if not found:
95- LOG.debug(msg, path, resp.code)
96- else:
97- LOG.warn(msg, path, resp.code)
98- return False
99- else:
100- self.metadata[mkey] = None
101- except url_helper.UrlError as e:
102- if required:
103- msg = "required url %s raised exception %s. not GCE"
104- if not found:
105- LOG.debug(msg, path, e)
106- else:
107- LOG.warn(msg, path, e)
108- return False
109- msg = "Failed to get %s metadata item: %s."
110- LOG.debug(msg, path, e)
111-
112- self.metadata[mkey] = None
113+ LOG.warn(msg, mkey)
114+ return False
115+ self.metadata[mkey] = value
116
117 if self.metadata['public-keys']:
118 lines = self.metadata['public-keys'].splitlines()
119@@ -116,7 +124,7 @@
120 else:
121 LOG.warn('unknown user-data-encoding: %s, ignoring', encoding)
122
123- return found
124+ return running_on_gce
125
126 @property
127 def launch_index(self):
128
129=== modified file 'tests/unittests/test_datasource/test_gce.py'
130--- tests/unittests/test_datasource/test_gce.py 2015-03-04 12:29:29 +0000
131+++ tests/unittests/test_datasource/test_gce.py 2015-04-20 15:25:12 +0000
132@@ -113,10 +113,6 @@
133 self.assertEqual(GCE_META.get('instance/attributes/user-data'),
134 self.ds.get_userdata_raw())
135
136- # we expect a list of public ssh keys with user names stripped
137- self.assertEqual(['ssh-rsa AA2..+aRD0fyVw== root@server'],
138- self.ds.get_public_ssh_keys())
139-
140 # test partial metadata (missing user-data in particular)
141 @httpretty.activate
142 def test_metadata_partial(self):
143@@ -141,3 +137,48 @@
144 decoded = b64decode(
145 GCE_META_ENCODING.get('instance/attributes/user-data'))
146 self.assertEqual(decoded, self.ds.get_userdata_raw())
147+
148+ @httpretty.activate
149+ def test_missing_required_keys_return_false(self):
150+ for required_key in ['instance/id', 'instance/zone',
151+ 'instance/hostname']:
152+ meta = GCE_META_PARTIAL.copy()
153+ del meta[required_key]
154+ httpretty.register_uri(httpretty.GET, MD_URL_RE,
155+ body=_new_request_callback(meta))
156+ self.assertEqual(False, self.ds.get_data())
157+ httpretty.reset()
158+
159+ @httpretty.activate
160+ def test_project_level_ssh_keys_are_used(self):
161+ httpretty.register_uri(httpretty.GET, MD_URL_RE,
162+ body=_new_request_callback())
163+ self.ds.get_data()
164+
165+ # we expect a list of public ssh keys with user names stripped
166+ self.assertEqual(['ssh-rsa AA2..+aRD0fyVw== root@server'],
167+ self.ds.get_public_ssh_keys())
168+
169+ @httpretty.activate
170+ def test_instance_level_ssh_keys_are_used(self):
171+ key_content = 'ssh-rsa JustAUser root@server'
172+ meta = GCE_META.copy()
173+ meta['instance/attributes/sshKeys'] = 'user:{0}'.format(key_content)
174+
175+ httpretty.register_uri(httpretty.GET, MD_URL_RE,
176+ body=_new_request_callback(meta))
177+ self.ds.get_data()
178+
179+ self.assertIn(key_content, self.ds.get_public_ssh_keys())
180+
181+ @httpretty.activate
182+ def test_instance_level_keys_replace_project_level_keys(self):
183+ key_content = 'ssh-rsa JustAUser root@server'
184+ meta = GCE_META.copy()
185+ meta['instance/attributes/sshKeys'] = 'user:{0}'.format(key_content)
186+
187+ httpretty.register_uri(httpretty.GET, MD_URL_RE,
188+ body=_new_request_callback(meta))
189+ self.ds.get_data()
190+
191+ self.assertEqual([key_content], self.ds.get_public_ssh_keys())