Merge lp:~harlowja/cloud-init/query-tool-is-back into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Scott Moser
Status: Rejected
Rejected by: Chad Smith
Proposed branch: lp:~harlowja/cloud-init/query-tool-is-back
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 395 lines (+216/-33)
6 files modified
Requires (+1/-0)
bin/cloud-init (+36/-10)
cloudinit/pprint.py (+107/-0)
cloudinit/sources/__init__.py (+30/-0)
cloudinit/stages.py (+37/-21)
cloudinit/util.py (+5/-2)
To merge this branch: bzr merge lp:~harlowja/cloud-init/query-tool-is-back
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Needs Fixing
cloud-init Commiters Pending
Review via email: mp+123394@code.launchpad.net
To post a comment you must log in.
648. By Joshua Harlow

Updated with no encryption and
clearing out of the userdata/raw fields
to prevent access when querying.

649. By Joshua Harlow

Fix some pylint issues and update the datasource query
to just return a map describing the database and use the
borrowed pprint code to show this map in a nice CLI format.

Also allow for printing of the init configuration as well
as the datasource via the query entrypoint.

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

I'd like to have the query tool back. Some comments:
 * I'd like some cmdline mechanism to request a single variable 'cloud-init query --instance-id' or something like that. Saving that, at very least well formatted data needs to be output for '--what=ds'. As it exists right now, I dont think its machine parsable output really. I kind of liked hte way it was in revno 649 in that respect.
    Basically, I want people to be able to use this as a replacement for 'ec2metadata --instance-id' or 'ec2metadata --local-hostname'. I dont think doing this consistently across data sources is easy, but I'd like to try.

 * 'self._write_to_cache(safe=False)' is more readable than below, which just looks odd.
    + self._write_to_cache(False)
    + self._write_to_cache(True)

 * seems like unused change for 'hash_blob' creeped in.
 * random newline in 'Requires' was added.
 * cloudinit/pprint.py has 'vim: not consistent with other 'vi:'. I'm not opposed to that change, but I'd prefer it done once all over if we wanted that.
   # vim: tabstop=4 shiftwidth=4 softtabstop=4

Revision history for this message
Joshua Harlow (harlowja) wrote :

Will try to fix these adjustments over the holidays and or do some stuff differently.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:649
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~harlowja/cloud-init/query-tool-is-back/+merge/123394/+edit-commit-message

https://server-team-jenkins.canonical.com/job/cloud-init-ci/33/
Executed test runs:
    None: https://server-team-jenkins.canonical.com/job/lp-vote-on-merge/7/console

Click here to trigger a rebuild:
https://server-team-jenkins.canonical.com/job/cloud-init-ci/33/rebuild

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

Hello,

Thank you for taking the time to contribute to cloud-init. Cloud-init has moved its revision control system to git. As a result, we are marking all bzr merge proposals as 'rejected'. If you would like to re-submit this proposal for review, please do so by following the current HACKING documentation at http://cloudinit.readthedocs.io/en/latest/topics/hacking.html.

This branch will no longer apply against master, the cloud-init utility has moved to cloudinit/cmd/main.py and I think there are some review comments that would need to be addressed here to make this solution a bit more generic.

Unmerged revisions

649. By Joshua Harlow

Fix some pylint issues and update the datasource query
to just return a map describing the database and use the
borrowed pprint code to show this map in a nice CLI format.

Also allow for printing of the init configuration as well
as the datasource via the query entrypoint.

648. By Joshua Harlow

Updated with no encryption and
clearing out of the userdata/raw fields
to prevent access when querying.

647. By Joshua Harlow

Start adding a query entrypoint with encryption using
aes of the userdata (raw and not raw) if possible using
the provided users (currently root) private ssh key sha256
hash as the secret (openssl was tried, didn't work due to
key file formats being all different).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Requires'
--- Requires 2012-07-09 20:41:45 +0000
+++ Requires 2012-09-10 21:48:19 +0000
@@ -26,3 +26,4 @@
2626
27# The new main entrypoint uses argparse instead of optparse27# The new main entrypoint uses argparse instead of optparse
28argparse28argparse
29
2930
=== modified file 'bin/cloud-init'
--- bin/cloud-init 2012-08-10 03:48:01 +0000
+++ bin/cloud-init 2012-09-10 21:48:19 +0000
@@ -35,6 +35,7 @@
3535
36from cloudinit import log as logging36from cloudinit import log as logging
37from cloudinit import netinfo37from cloudinit import netinfo
38from cloudinit import pprint as cp
38from cloudinit import sources39from cloudinit import sources
39from cloudinit import stages40from cloudinit import stages
40from cloudinit import templater41from cloudinit import templater
@@ -52,11 +53,10 @@
52# Module section template53# Module section template
53MOD_SECTION_TPL = "cloud_%s_modules"54MOD_SECTION_TPL = "cloud_%s_modules"
5455
55# Things u can query on56# Things u can query on...
56QUERY_DATA_TYPES = [57QUERY_NAMES = [
57 'data',58 'ds',
58 'data_raw',59 'cfg',
59 'instance_id',
60]60]
6161
62# Frequency shortname to full name62# Frequency shortname to full name
@@ -342,9 +342,35 @@
342 return run_module_section(mods, name, name)342 return run_module_section(mods, name, name)
343343
344344
345def main_query(name, _args):345def main_query(name, args):
346 raise NotImplementedError(("Action '%s' is not"346 w_msg = welcome_format(name)
347 " currently implemented") % (name))347 welcome(name, msg=w_msg)
348 items = args.what
349 if not items:
350 return 1
351 init = stages.Init()
352 ds = None
353 try:
354 # Try the 'privileged' datasource first
355 ds = init.fetch(attempt_find=False)
356 except Exception:
357 pass
358 if not ds:
359 # Use the safer version (if its there)
360 ds = init.fetch(safe=True)
361 if not ds:
362 print("No datasource available for querying!")
363 return 1
364 for i in items:
365 if i == 'ds':
366 print("Datasource details")
367 print("-" * len("Datasource details"))
368 cp.pprint(ds.describe())
369 elif i == 'cfg':
370 print("Configuration details")
371 print("-" * len("Configuration details"))
372 cp.pprint(init.cfg)
373 return 0
348374
349375
350def main_single(name, args):376def main_single(name, args):
@@ -464,10 +490,10 @@
464 parser_query = subparsers.add_parser('query',490 parser_query = subparsers.add_parser('query',
465 help=('query information stored '491 help=('query information stored '
466 'in cloud-init'))492 'in cloud-init'))
467 parser_query.add_argument("--name", '-n', action="store",493 parser_query.add_argument("--what", '-w', action="store",
468 help="item name to query on",494 help="item name to query on",
469 required=True,495 required=True,
470 choices=QUERY_DATA_TYPES)496 choices=sorted(QUERY_NAMES))
471 parser_query.set_defaults(action=('query', main_query))497 parser_query.set_defaults(action=('query', main_query))
472498
473 # This subcommand allows you to run a single module499 # This subcommand allows you to run a single module
474500
=== added file 'cloudinit/pprint.py'
--- cloudinit/pprint.py 1970-01-01 00:00:00 +0000
+++ cloudinit/pprint.py 2012-09-10 21:48:19 +0000
@@ -0,0 +1,107 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright (C) 2012 Yahoo! Inc. All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17
18def center_text(text, fill, max_len):
19 return '{0:{fill}{align}{size}}'.format(text,
20 fill=fill, align="^", size=max_len)
21
22
23def _pformat_list(lst, item_max_len):
24 lines = []
25 if not lst:
26 lines.append("+------+")
27 lines.append("'------'")
28 return "\n".join(lines)
29 entries = []
30 max_len = 0
31 for i in lst:
32 e = pformat(i, item_max_len)
33 for v in e.split("\n"):
34 max_len = max(max_len, len(v) + 2)
35 entries.append(e)
36 lines.append("+%s+" % ("-" * (max_len)))
37 for e in entries:
38 for line in e.split("\n"):
39 lines.append("|%s|" % (center_text(line, ' ', max_len)))
40 lines.append("'%s'" % ("-" * (max_len)))
41 return "\n".join(lines)
42
43
44
45def _pformat_hash(hsh, item_max_len):
46 lines = []
47 if not hsh:
48 lines.append("+-----+-----+")
49 lines.append("'-----+-----'")
50 return "\n".join(lines)
51 # Figure out the lengths to place items in...
52 max_key_len = 0
53 max_value_len = 0
54 entries = []
55 for (k, v) in hsh.items():
56 entry = ("%s" % (_pformat_escape(k, item_max_len)),
57 "%s" % (pformat(v, item_max_len)))
58 max_key_len = max(max_key_len, len(entry[0]) + 2)
59 for v in entry[1].split("\n"):
60 max_value_len = max(max_value_len, len(v) + 2)
61 entries.append(entry)
62 # Now actually do the placement since we have the lengths
63 lines.append("+%s+%s+" % ("-" * max_key_len, "-" * max_value_len))
64 for (key, value) in entries:
65 value_lines = value.split("\n")
66 lines.append("|%s|%s|" % (center_text(key, ' ', max_key_len),
67 center_text(value_lines[0], ' ',
68 max_value_len)))
69 if len(value_lines) > 1:
70 for j in range(1, len(value_lines)):
71 lines.append("|%s|%s|" % (center_text("-", ' ', max_key_len),
72 center_text(value_lines[j], ' ',
73 max_value_len)))
74 lines.append("'%s+%s'" % ("-" * max_key_len, "-" * max_value_len))
75 return "\n".join(lines)
76
77
78def _pformat_escape(item, item_max_len):
79 item = _pformat_simple(item, item_max_len)
80 item = item.replace("\n", "\\n")
81 item = item.replace("\t", "\\t")
82 return item
83
84
85def _pformat_simple(item, item_max_len):
86 if item_max_len is None or item_max_len < 0:
87 return "%s" % (item)
88 if item_max_len == 0:
89 return ''
90 item_str = "%s" % (item)
91 if len(item_str) > item_max_len:
92 # TODO(harlowja) use utf8 ellipse or '...'??
93 item_str = item_str[0:item_max_len] + '...'
94 return item_str
95
96
97def pformat(item, item_max_len=None):
98 if isinstance(item, (list, set, tuple)):
99 return _pformat_list(item, item_max_len)
100 elif isinstance(item, (dict)):
101 return _pformat_hash(item, item_max_len)
102 else:
103 return _pformat_simple(item, item_max_len)
104
105
106def pprint(item, item_max_len=None):
107 print("%s" % (pformat(item, item_max_len)))
0108
=== modified file 'cloudinit/sources/__init__.py'
--- cloudinit/sources/__init__.py 2012-08-28 03:51:00 +0000
+++ cloudinit/sources/__init__.py 2012-09-10 21:48:19 +0000
@@ -23,6 +23,7 @@
23from email.mime.multipart import MIMEMultipart23from email.mime.multipart import MIMEMultipart
2424
25import abc25import abc
26import copy
2627
27from cloudinit import importer28from cloudinit import importer
28from cloudinit import log as logging29from cloudinit import log as logging
@@ -56,6 +57,7 @@
56 name = util.obj_name(self)57 name = util.obj_name(self)
57 if name.startswith(DS_PREFIX):58 if name.startswith(DS_PREFIX):
58 name = name[len(DS_PREFIX):]59 name = name[len(DS_PREFIX):]
60 self.name = name
59 self.ds_cfg = util.get_cfg_by_path(self.sys_cfg,61 self.ds_cfg = util.get_cfg_by_path(self.sys_cfg,
60 ("datasource", name), {})62 ("datasource", name), {})
61 if not ud_proc:63 if not ud_proc:
@@ -63,6 +65,16 @@
63 else:65 else:
64 self.ud_proc = ud_proc66 self.ud_proc = ud_proc
6567
68 def copy(self, safe=False):
69 nds = copy.deepcopy(self)
70 if not safe:
71 return nds
72 # Clear it out, nothing to see here...
73 nds.ud_proc = None
74 nds.userdata = None
75 nds.userdata_raw = None
76 return nds
77
66 def get_userdata(self, apply_filter=False):78 def get_userdata(self, apply_filter=False):
67 if self.userdata is None:79 if self.userdata is None:
68 self.userdata = self.ud_proc.process(self.get_userdata_raw())80 self.userdata = self.ud_proc.process(self.get_userdata_raw())
@@ -78,6 +90,24 @@
78 return self.metadata['launch-index']90 return self.metadata['launch-index']
79 return None91 return None
8092
93 # describes the datasource as a nice map
94 # with basics such as userdata, raw userdata,
95 # anything else that a subclass can provide...
96 def describe(self):
97 return {
98 'hostname': self.get_hostname(),
99 'instance-id': self.get_instance_id(),
100 'launch-index': self.launch_index,
101 'locale': self.get_locale(),
102 'metadata': self.metadata,
103 'name': self.name,
104 'package-mirror-info': self.get_package_mirror_info(),
105 'public-ssh-keys': self.get_public_ssh_keys(),
106 'user-data': self.userdata,
107 'user-data-raw': self.userdata_raw,
108 'configuration': self.ds_cfg,
109 }
110
81 def _filter_userdata(self, processed_ud):111 def _filter_userdata(self, processed_ud):
82 filters = [112 filters = [
83 launch_index.Filter(util.safe_int(self.launch_index)),113 launch_index.Filter(util.safe_int(self.launch_index)),
84114
=== modified file 'cloudinit/stages.py'
--- cloudinit/stages.py 2012-08-26 22:04:06 +0000
+++ cloudinit/stages.py 2012-09-10 21:48:19 +0000
@@ -60,6 +60,8 @@
60 self._distro = None60 self._distro = None
61 # Created only when a fetch occurs61 # Created only when a fetch occurs
62 self.datasource = None62 self.datasource = None
63 # Only created if asked and available
64 self.safe_datasource = None
6365
64 @property66 @property
65 def distro(self):67 def distro(self):
@@ -170,11 +172,11 @@
170 base_cfg=self._read_base_cfg())172 base_cfg=self._read_base_cfg())
171 return merger.cfg173 return merger.cfg
172174
173 def _restore_from_cache(self):175 def _restore_from_cache(self, name):
174 # We try to restore from a current link and static path176 # We try to restore from a current link and static path
175 # by using the instance link, if purge_cache was called177 # by using the instance link, if purge_cache was called
176 # the file wont exist.178 # the file wont exist.
177 pickled_fn = self.paths.get_ipath_cur('obj_pkl')179 pickled_fn = self.paths.get_ipath_cur(name)
178 pickle_contents = None180 pickle_contents = None
179 try:181 try:
180 pickle_contents = util.load_file(pickled_fn)182 pickle_contents = util.load_file(pickled_fn)
@@ -190,21 +192,18 @@
190 util.logexc(LOG, "Failed loading pickled blob from %s", pickled_fn)192 util.logexc(LOG, "Failed loading pickled blob from %s", pickled_fn)
191 return None193 return None
192194
193 def _write_to_cache(self):195 def _write_to_cache(self, safe):
194 if not self.datasource:196 if not safe:
195 return False197 fmode = 0400
196 pickled_fn = self.paths.get_ipath_cur("obj_pkl")198 pickled_fn = self.paths.get_ipath_cur("obj_pkl")
197 try:199 else:
198 pk_contents = pickle.dumps(self.datasource)200 fmode = 0644
199 except Exception:201 pickled_fn = self.paths.get_ipath_cur("safe_obj_pkl")
200 util.logexc(LOG, "Failed pickling datasource %s", self.datasource)202 try:
201 return False203 pk_contents = pickle.dumps(self.datasource.copy(safe))
202 try:204 util.write_file(pickled_fn, pk_contents, mode=fmode)
203 util.write_file(pickled_fn, pk_contents, mode=0400)
204 except Exception:205 except Exception:
205 util.logexc(LOG, "Failed pickling datasource to %s", pickled_fn)206 util.logexc(LOG, "Failed pickling datasource to %s", pickled_fn)
206 return False
207 return True
208207
209 def _get_datasources(self):208 def _get_datasources(self):
210 # Any config provided???209 # Any config provided???
@@ -216,12 +215,23 @@
216 cfg_list = self.cfg.get('datasource_list') or []215 cfg_list = self.cfg.get('datasource_list') or []
217 return (cfg_list, pkg_list)216 return (cfg_list, pkg_list)
218217
219 def _get_data_source(self):218 def _get_safe_datasource(self):
219 if self.safe_datasource:
220 return self.safe_datasource
221 ds = self._restore_from_cache('safe_obj_pkl')
222 if ds:
223 LOG.debug("Restored from cache, safe datasource: %s", ds)
224 self.safe_datasource = ds
225 return ds
226
227 def _get_data_source(self, attempt_find):
220 if self.datasource:228 if self.datasource:
221 return self.datasource229 return self.datasource
222 ds = self._restore_from_cache()230 ds = self._restore_from_cache('obj_pkl')
223 if ds:231 if ds:
224 LOG.debug("Restored from cache, datasource: %s", ds)232 LOG.debug("Restored from cache, datasource: %s", ds)
233 if not ds and not attempt_find:
234 return None
225 if not ds:235 if not ds:
226 (cfg_list, pkg_list) = self._get_datasources()236 (cfg_list, pkg_list) = self._get_datasources()
227 # Deep copy so that user-data handlers can not modify237 # Deep copy so that user-data handlers can not modify
@@ -298,8 +308,11 @@
298 "%s\n" % (previous_iid))308 "%s\n" % (previous_iid))
299 return iid309 return iid
300310
301 def fetch(self):311 def fetch(self, safe=False, attempt_find=True):
302 return self._get_data_source()312 if not safe:
313 return self._get_data_source(attempt_find)
314 else:
315 return self._get_safe_datasource()
303316
304 def instancify(self):317 def instancify(self):
305 return self._reflect_cur_instance()318 return self._reflect_cur_instance()
@@ -311,8 +324,11 @@
311 self.distro, helpers.Runners(self.paths))324 self.distro, helpers.Runners(self.paths))
312325
313 def update(self):326 def update(self):
314 if not self._write_to_cache():327 if not self.datasource:
315 return328 raise RuntimeError(("Unable to update with the given datasource, "
329 "no datasource fetched!"))
330 self._write_to_cache(False)
331 self._write_to_cache(True)
316 self._store_userdata()332 self._store_userdata()
317333
318 def _store_userdata(self):334 def _store_userdata(self):
319335
=== modified file 'cloudinit/util.py'
--- cloudinit/util.py 2012-08-28 03:51:00 +0000
+++ cloudinit/util.py 2012-09-10 21:48:19 +0000
@@ -1093,10 +1093,13 @@
1093 log.debug(msg, exc_info=1, *args)1093 log.debug(msg, exc_info=1, *args)
10941094
10951095
1096def hash_blob(blob, routine, mlen=None):1096def hash_blob(blob, routine, mlen=None, give_hex=True):
1097 hasher = hashlib.new(routine)1097 hasher = hashlib.new(routine)
1098 hasher.update(blob)1098 hasher.update(blob)
1099 digest = hasher.hexdigest()1099 if give_hex:
1100 digest = hasher.hexdigest()
1101 else:
1102 digest = hasher.digest()
1100 # Don't get to long now1103 # Don't get to long now
1101 if mlen is not None:1104 if mlen is not None:
1102 return digest[0:mlen]1105 return digest[0:mlen]