Merge lp:~harlowja/cloud-init/ug-cleanup-part-deuce into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Joshua Harlow
Status: Merged
Merged at revision: 677
Proposed branch: lp:~harlowja/cloud-init/ug-cleanup-part-deuce
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 603 lines (+236/-95)
9 files modified
cloudinit/config/cc_byobu.py (+18/-9)
cloudinit/config/cc_set_passwords.py (+7/-10)
cloudinit/config/cc_ssh.py (+7/-10)
cloudinit/config/cc_ssh_authkey_fingerprints.py (+11/-5)
cloudinit/config/cc_ssh_import_id.py (+18/-19)
cloudinit/config/cc_users_groups.py (+5/-2)
cloudinit/distros/__init__.py (+100/-30)
cloudinit/util.py (+30/-0)
tests/unittests/test_distros/test_user_data_normalize.py (+40/-10)
To merge this branch: bzr merge lp:~harlowja/cloud-init/ug-cleanup-part-deuce
Reviewer Review Type Date Requested Status
cloud-init Commiters Pending
Review via email: mp+127067@code.launchpad.net
To post a comment you must log in.
673. By Joshua Harlow

Add a comment as to why distros can't be
imported without being renamed due to
previous usage of the attribute 'distros'

674. By Joshua Harlow

Make byobu more tolerant of the user not being
located and warn when it is not found + only
run the shell command when actual contents
exist to run.

675. By Joshua Harlow

Sync with head and fix conflicts.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/config/cc_byobu.py'
2--- cloudinit/config/cc_byobu.py 2012-06-23 03:58:50 +0000
3+++ cloudinit/config/cc_byobu.py 2012-09-28 21:23:20 +0000
4@@ -18,12 +18,17 @@
5 # You should have received a copy of the GNU General Public License
6 # along with this program. If not, see <http://www.gnu.org/licenses/>.
7
8+# Ensure this is aliased to a name not 'distros'
9+# since the module attribute 'distros'
10+# is a list of distros that are supported, not a sub-module
11+from cloudinit import distros as ds
12+
13 from cloudinit import util
14
15 distros = ['ubuntu', 'debian']
16
17
18-def handle(name, cfg, _cloud, log, args):
19+def handle(name, cfg, cloud, log, args):
20 if len(args) != 0:
21 value = args[0]
22 else:
23@@ -56,16 +61,20 @@
24
25 shcmd = ""
26 if mod_user:
27- user = util.get_cfg_option_str(cfg, "user", "ubuntu")
28- shcmd += " sudo -Hu \"%s\" byobu-launcher-%s" % (user, bl_inst)
29- shcmd += " || X=$(($X+1)); "
30+ (users, _groups) = ds.normalize_users_groups(cfg, cloud.distro)
31+ (user, _user_config) = ds.extract_default(users)
32+ if not user:
33+ log.warn(("No default byobu user provided, "
34+ "can not launch %s for the default user"), bl_inst)
35+ else:
36+ shcmd += " sudo -Hu \"%s\" byobu-launcher-%s" % (user, bl_inst)
37+ shcmd += " || X=$(($X+1)); "
38 if mod_sys:
39 shcmd += "echo \"%s\" | debconf-set-selections" % dc_val
40 shcmd += " && dpkg-reconfigure byobu --frontend=noninteractive"
41 shcmd += " || X=$(($X+1)); "
42
43- cmd = ["/bin/sh", "-c", "%s %s %s" % ("X=0;", shcmd, "exit $X")]
44-
45- log.debug("Setting byobu to %s", value)
46-
47- util.subp(cmd, capture=False)
48+ if len(shcmd):
49+ cmd = ["/bin/sh", "-c", "%s %s %s" % ("X=0;", shcmd, "exit $X")]
50+ log.debug("Setting byobu to %s", value)
51+ util.subp(cmd, capture=False)
52
53=== modified file 'cloudinit/config/cc_set_passwords.py'
54--- cloudinit/config/cc_set_passwords.py 2012-08-31 21:45:15 +0000
55+++ cloudinit/config/cc_set_passwords.py 2012-09-28 21:23:20 +0000
56@@ -20,6 +20,11 @@
57
58 import sys
59
60+# Ensure this is aliased to a name not 'distros'
61+# since the module attribute 'distros'
62+# is a list of distros that are supported, not a sub-module
63+from cloudinit import distros as ds
64+
65 from cloudinit import ssh_util
66 from cloudinit import util
67
68@@ -50,18 +55,10 @@
69 expire = util.get_cfg_option_bool(chfg, 'expire', expire)
70
71 if not plist and password:
72- user = cloud.distro.get_default_user()
73-
74- if 'users' in cfg:
75-
76- user_zero = cfg['users'][0]
77-
78- if isinstance(user_zero, dict) and 'name' in user_zero:
79- user = user_zero['name']
80-
81+ (users, _groups) = ds.normalize_users_groups(cfg, cloud.distro)
82+ (user, _user_config) = ds.extract_default(users)
83 if user:
84 plist = "%s:%s" % (user, password)
85-
86 else:
87 log.warn("No default or defined user to change password for.")
88
89
90=== modified file 'cloudinit/config/cc_ssh.py'
91--- cloudinit/config/cc_ssh.py 2012-08-31 18:45:40 +0000
92+++ cloudinit/config/cc_ssh.py 2012-09-28 21:23:20 +0000
93@@ -21,6 +21,11 @@
94 import glob
95 import os
96
97+# Ensure this is aliased to a name not 'distros'
98+# since the module attribute 'distros'
99+# is a list of distros that are supported, not a sub-module
100+from cloudinit import distros as ds
101+
102 from cloudinit import ssh_util
103 from cloudinit import util
104
105@@ -102,16 +107,8 @@
106 " %s to file %s"), keytype, keyfile)
107
108 try:
109- # TODO(utlemming): consolidate this stanza that occurs in:
110- # cc_ssh_import_id, cc_set_passwords, maybe cc_users_groups.py
111- user = cloud.distro.get_default_user()
112-
113- if 'users' in cfg:
114- user_zero = cfg['users'][0]
115-
116- if user_zero != "default":
117- user = user_zero
118-
119+ (users, _groups) = ds.normalize_users_groups(cfg, cloud.distro)
120+ (user, _user_config) = ds.extract_default(users)
121 disable_root = util.get_cfg_option_bool(cfg, "disable_root", True)
122 disable_root_opts = util.get_cfg_option_str(cfg, "disable_root_opts",
123 DISABLE_ROOT_OPTS)
124
125=== modified file 'cloudinit/config/cc_ssh_authkey_fingerprints.py'
126--- cloudinit/config/cc_ssh_authkey_fingerprints.py 2012-09-28 20:35:53 +0000
127+++ cloudinit/config/cc_ssh_authkey_fingerprints.py 2012-09-28 21:23:20 +0000
128@@ -21,7 +21,11 @@
129
130 from prettytable import PrettyTable
131
132-from cloudinit import distros
133+# Ensure this is aliased to a name not 'distros'
134+# since the module attribute 'distros'
135+# is a list of distros that are supported, not a sub-module
136+from cloudinit import distros as ds
137+
138 from cloudinit import ssh_util
139 from cloudinit import util
140
141@@ -41,8 +45,10 @@
142 hasher = hashlib.new(hash_meth)
143 hasher.update(base64.b64decode(b64_text))
144 return ":".join(_split_hash(hasher.hexdigest()))
145- except TypeError:
146+ except (TypeError, ValueError):
147 # Raised when b64 not really b64...
148+ # or when the hash type is not really
149+ # a known/supported hash type...
150 return '?'
151
152
153@@ -92,8 +98,8 @@
154
155 hash_meth = util.get_cfg_option_str(cfg, "authkey_hash", "md5")
156 extract_func = ssh_util.extract_authorized_keys
157- (users, _groups) = distros.normalize_users_groups(cfg, cloud.distro)
158+ (users, _groups) = ds.normalize_users_groups(cfg, cloud.distro)
159 for (user_name, _cfg) in users.items():
160 (auth_key_fn, auth_key_entries) = extract_func(user_name, cloud.paths)
161- _pprint_key_entries(user_name, auth_key_fn, auth_key_entries,
162- hash_meth)
163+ _pprint_key_entries(user_name, auth_key_fn,
164+ auth_key_entries, hash_meth)
165
166=== modified file 'cloudinit/config/cc_ssh_import_id.py'
167--- cloudinit/config/cc_ssh_import_id.py 2012-08-31 19:36:05 +0000
168+++ cloudinit/config/cc_ssh_import_id.py 2012-09-28 21:23:20 +0000
169@@ -18,6 +18,11 @@
170 # You should have received a copy of the GNU General Public License
171 # along with this program. If not, see <http://www.gnu.org/licenses/>.
172
173+# Ensure this is aliased to a name not 'distros'
174+# since the module attribute 'distros'
175+# is a list of distros that are supported, not a sub-module
176+from cloudinit import distros as ds
177+
178 from cloudinit import util
179 import pwd
180
181@@ -39,33 +44,27 @@
182 return
183
184 # import for cloudinit created users
185+ (users, _groups) = ds.normalize_users_groups(cfg, cloud.distro)
186 elist = []
187- for user_cfg in cfg['users']:
188- user = None
189+ for (user, user_cfg) in users.items():
190 import_ids = []
191-
192- if isinstance(user_cfg, str) and user_cfg == "default":
193- user = cloud.distro.get_default_user()
194- if not user:
195- continue
196-
197+ if user_cfg['default']:
198 import_ids = util.get_cfg_option_list(cfg, "ssh_import_id", [])
199-
200- elif isinstance(user_cfg, dict):
201- user = None
202- import_ids = []
203-
204+ else:
205 try:
206- user = user_cfg['name']
207 import_ids = user_cfg['ssh_import_id']
208-
209- if import_ids and isinstance(import_ids, str):
210- import_ids = str(import_ids).split(',')
211-
212 except:
213- log.debug("user %s is not configured for ssh_import" % user)
214+ log.debug("User %s is not configured for ssh_import_id", user)
215 continue
216
217+ try:
218+ import_ids = util.uniq_merge(import_ids)
219+ import_ids = [str(i) for i in import_ids]
220+ except:
221+ log.debug("User %s is not correctly configured for ssh_import_id",
222+ user)
223+ continue
224+
225 if not len(import_ids):
226 continue
227
228
229=== modified file 'cloudinit/config/cc_users_groups.py'
230--- cloudinit/config/cc_users_groups.py 2012-09-28 20:35:53 +0000
231+++ cloudinit/config/cc_users_groups.py 2012-09-28 21:23:20 +0000
232@@ -16,7 +16,10 @@
233 # You should have received a copy of the GNU General Public License
234 # along with this program. If not, see <http://www.gnu.org/licenses/>.
235
236-from cloudinit import distros
237+# Ensure this is aliased to a name not 'distros'
238+# since the module attribute 'distros'
239+# is a list of distros that are supported, not a sub-module
240+from cloudinit import distros as ds
241
242 from cloudinit.settings import PER_INSTANCE
243
244@@ -24,7 +27,7 @@
245
246
247 def handle(name, cfg, cloud, _log, _args):
248- (users, groups) = distros.normalize_users_groups(cfg, cloud.distro)
249+ (users, groups) = ds.normalize_users_groups(cfg, cloud.distro)
250 for (name, members) in groups.items():
251 cloud.distro.create_group(name, members)
252 for (user, config) in users.items():
253
254=== modified file 'cloudinit/distros/__init__.py'
255--- cloudinit/distros/__init__.py 2012-09-28 19:38:48 +0000
256+++ cloudinit/distros/__init__.py 2012-09-28 21:23:20 +0000
257@@ -24,6 +24,7 @@
258 from StringIO import StringIO
259
260 import abc
261+import itertools
262 import os
263 import re
264
265@@ -186,8 +187,10 @@
266 'gecos': "%s" % (self.default_user.title()),
267 'sudo': "ALL=(ALL) NOPASSWD:ALL",
268 }
269- if self.default_user_groups:
270- user_cfg['groups'] = _uniq_merge_sorted(self.default_user_groups)
271+ def_groups = self.default_user_groups
272+ if not def_groups:
273+ def_groups = []
274+ user_cfg['groups'] = util.uniq_merge_sorted(def_groups)
275 return user_cfg
276
277 def create_user(self, name, **kwargs):
278@@ -398,39 +401,27 @@
279 return default
280
281
282-def _uniq_merge_sorted(*lists):
283- return sorted(_uniq_merge(*lists))
284-
285-
286-def _uniq_merge(*lists):
287- combined_list = []
288- for a_list in lists:
289- if isinstance(a_list, (str, basestring)):
290- a_list = a_list.strip().split(",")
291- else:
292- a_list = [str(a) for a in a_list]
293- a_list = [a.strip() for a in a_list if a.strip()]
294- combined_list.extend(a_list)
295- uniq_list = []
296- for a in combined_list:
297- if a in uniq_list:
298- continue
299- else:
300- uniq_list.append(a)
301- return uniq_list
302-
303-
304+# Normalizes a input group configuration
305+# which can be a comma seperated list of
306+# group names, or a list of group names
307+# or a python dictionary of group names
308+# to a list of members of that group.
309+#
310+# The output is a dictionary of group
311+# names => members of that group which
312+# is the standard form used in the rest
313+# of cloud-init
314 def _normalize_groups(grp_cfg):
315 if isinstance(grp_cfg, (str, basestring, list)):
316 c_grp_cfg = {}
317- for i in _uniq_merge(grp_cfg):
318+ for i in util.uniq_merge(grp_cfg):
319 c_grp_cfg[i] = []
320 grp_cfg = c_grp_cfg
321
322 groups = {}
323 if isinstance(grp_cfg, (dict)):
324 for (grp_name, grp_members) in grp_cfg.items():
325- groups[grp_name] = _uniq_merge_sorted(grp_members)
326+ groups[grp_name] = util.uniq_merge_sorted(grp_members)
327 else:
328 raise TypeError(("Group config must be list, dict "
329 " or string types only and not %s") %
330@@ -438,6 +429,21 @@
331 return groups
332
333
334+# Normalizes a input group configuration
335+# which can be a comma seperated list of
336+# user names, or a list of string user names
337+# or a list of dictionaries with components
338+# that define the user config + 'name' (if
339+# a 'name' field does not exist then the
340+# default user is assumed to 'own' that
341+# configuration.
342+#
343+# The output is a dictionary of user
344+# names => user config which is the standard
345+# form used in the rest of cloud-init. Note
346+# the default user will have a special config
347+# entry 'default' which will be marked as true
348+# all other users will be marked as false.
349 def _normalize_users(u_cfg, def_user_cfg=None):
350 if isinstance(u_cfg, (dict)):
351 ad_ucfg = []
352@@ -453,12 +459,12 @@
353 " for key %s") % (util.obj_name(v), k))
354 u_cfg = ad_ucfg
355 elif isinstance(u_cfg, (str, basestring)):
356- u_cfg = _uniq_merge_sorted(u_cfg)
357+ u_cfg = util.uniq_merge_sorted(u_cfg)
358
359 users = {}
360 for user_config in u_cfg:
361 if isinstance(user_config, (str, basestring, list)):
362- for u in _uniq_merge(user_config):
363+ for u in util.uniq_merge(user_config):
364 if u and u not in users:
365 users[u] = {}
366 elif isinstance(user_config, (dict)):
367@@ -491,22 +497,59 @@
368
369 # Fixup the default user into the real
370 # default user name and replace it...
371+ def_user = None
372 if users and 'default' in users:
373 def_config = users.pop('default')
374 if def_user_cfg:
375+ # Pickup what the default 'real name' is
376+ # and any groups that are provided by the
377+ # default config
378 def_user = def_user_cfg.pop('name')
379 def_groups = def_user_cfg.pop('groups', [])
380+ # Pickup any config + groups for that user name
381+ # that we may have previously extracted
382 parsed_config = users.pop(def_user, {})
383- users_groups = _uniq_merge_sorted(parsed_config.get('groups', []),
384- def_groups)
385+ parsed_groups = parsed_config.get('groups', [])
386+ # Now merge our extracted groups with
387+ # anything the default config provided
388+ users_groups = util.uniq_merge_sorted(parsed_groups, def_groups)
389 parsed_config['groups'] = ",".join(users_groups)
390+ # The real config for the default user is the
391+ # combination of the default user config provided
392+ # by the distro, the default user config provided
393+ # by the above merging for the user 'default' and
394+ # then the parsed config from the user's 'real name'
395+ # which does not have to be 'default' (but could be)
396 users[def_user] = util.mergemanydict([def_user_cfg,
397 def_config,
398 parsed_config])
399
400+ # Ensure that only the default user that we
401+ # found (if any) is actually marked as being
402+ # the default user
403+ if users:
404+ for (uname, uconfig) in users.items():
405+ if def_user and uname == def_user:
406+ uconfig['default'] = True
407+ else:
408+ uconfig['default'] = False
409+
410 return users
411
412
413+# Normalizes a set of user/users and group
414+# dictionary configuration into a useable
415+# format that the rest of cloud-init can
416+# understand using the default user
417+# provided by the input distrobution (if any)
418+# to allow for mapping of the 'default' user.
419+#
420+# Output is a dictionary of group names -> [member] (list)
421+# and a dictionary of user names -> user configuration (dict)
422+#
423+# If 'user' exists it will override
424+# the 'users'[0] entry (if a list) otherwise it will
425+# just become an entry in the returned dictionary (no override)
426 def normalize_users_groups(cfg, distro):
427 if not cfg:
428 cfg = {}
429@@ -548,6 +591,33 @@
430 return (users, groups)
431
432
433+# Given a user dictionary config it will
434+# extract the default user name and user config
435+# from that list and return that tuple or
436+# return (None, None) if no default user is
437+# found in the given input
438+def extract_default(users, default_name=None, default_config=None):
439+ if not users:
440+ users = {}
441+
442+ def safe_find(entry):
443+ config = entry[1]
444+ if not config or 'default' not in config:
445+ return False
446+ else:
447+ return config['default']
448+
449+ tmp_users = users.items()
450+ tmp_users = dict(itertools.ifilter(safe_find, tmp_users))
451+ if not tmp_users:
452+ return (default_name, default_config)
453+ else:
454+ name = tmp_users.keys()[0]
455+ config = tmp_users[name]
456+ config.pop('default', None)
457+ return (name, config)
458+
459+
460 def fetch(name):
461 locs = importer.find_module(name,
462 ['', __name__],
463
464=== modified file 'cloudinit/util.py'
465--- cloudinit/util.py 2012-09-28 20:31:50 +0000
466+++ cloudinit/util.py 2012-09-28 21:23:20 +0000
467@@ -249,6 +249,36 @@
468 raise
469
470
471+# Merges X lists, and then keeps the
472+# unique ones, but orders by sort order
473+# instead of by the original order
474+def uniq_merge_sorted(*lists):
475+ return sorted(uniq_merge(*lists))
476+
477+
478+# Merges X lists and then iterates over those
479+# and only keeps the unique items (order preserving)
480+# and returns that merged and uniqued list as the
481+# final result.
482+#
483+# Note: if any entry is a string it will be
484+# split on commas and empty entries will be
485+# evicted and merged in accordingly.
486+def uniq_merge(*lists):
487+ combined_list = []
488+ for a_list in lists:
489+ if isinstance(a_list, (str, basestring)):
490+ a_list = a_list.strip().split(",")
491+ # Kickout the empty ones
492+ a_list = [a for a in a_list if len(a)]
493+ combined_list.extend(a_list)
494+ uniq_list = []
495+ for i in combined_list:
496+ if i not in uniq_list:
497+ uniq_list.append(i)
498+ return uniq_list
499+
500+
501 def clean_filename(fn):
502 for (k, v) in FN_REPLACEMENTS.iteritems():
503 fn = fn.replace(k, v)
504
505=== modified file 'tests/unittests/test_distros/test_user_data_normalize.py'
506--- tests/unittests/test_distros/test_user_data_normalize.py 2012-09-28 01:30:01 +0000
507+++ tests/unittests/test_distros/test_user_data_normalize.py 2012-09-28 21:23:20 +0000
508@@ -119,8 +119,8 @@
509 (users, _groups) = self._norm(ug_cfg, distro)
510 self.assertIn('joe', users)
511 self.assertIn('bob', users)
512- self.assertEquals({}, users['joe'])
513- self.assertEquals({}, users['bob'])
514+ self.assertEquals({'default': False}, users['joe'])
515+ self.assertEquals({'default': False}, users['bob'])
516
517 def test_users_simple(self):
518 distro = self._make_distro('ubuntu')
519@@ -133,8 +133,8 @@
520 (users, _groups) = self._norm(ug_cfg, distro)
521 self.assertIn('joe', users)
522 self.assertIn('bob', users)
523- self.assertEquals({}, users['joe'])
524- self.assertEquals({}, users['bob'])
525+ self.assertEquals({'default': False}, users['joe'])
526+ self.assertEquals({'default': False}, users['bob'])
527
528 def test_users_old_user(self):
529 distro = self._make_distro('ubuntu', 'bob')
530@@ -179,8 +179,7 @@
531 }
532 (users, _groups) = self._norm(ug_cfg, distro)
533 self.assertIn('zetta', users)
534- ug_cfg = {
535- }
536+ ug_cfg = {}
537 (users, groups) = self._norm(ug_cfg, distro)
538 self.assertEquals({}, users)
539 self.assertEquals({}, groups)
540@@ -198,6 +197,35 @@
541 users['bob']['groups'])
542 self.assertEquals(True,
543 users['bob']['blah'])
544+ self.assertEquals(True,
545+ users['bob']['default'])
546+
547+ def test_users_dict_extract(self):
548+ distro = self._make_distro('ubuntu', 'bob')
549+ ug_cfg = {
550+ 'users': [
551+ 'default',
552+ ],
553+ }
554+ (users, _groups) = self._norm(ug_cfg, distro)
555+ self.assertIn('bob', users)
556+ (name, config) = distros.extract_default(users)
557+ self.assertEquals(name, 'bob')
558+ expected_config = {}
559+ def_config = None
560+ try:
561+ def_config = distro.get_default_user()
562+ except NotImplementedError:
563+ pass
564+ if not def_config:
565+ def_config = {}
566+ expected_config.update(def_config)
567+
568+ # Ignore these for now
569+ expected_config.pop('name', None)
570+ expected_config.pop('groups', None)
571+ config.pop('groups', None)
572+ self.assertEquals(config, expected_config)
573
574 def test_users_dict_default(self):
575 distro = self._make_distro('ubuntu', 'bob')
576@@ -210,6 +238,8 @@
577 self.assertIn('bob', users)
578 self.assertEquals(",".join(distro.get_default_user()['groups']),
579 users['bob']['groups'])
580+ self.assertEquals(True,
581+ users['bob']['default'])
582
583 def test_users_dict_trans(self):
584 distro = self._make_distro('ubuntu')
585@@ -223,8 +253,8 @@
586 (users, _groups) = self._norm(ug_cfg, distro)
587 self.assertIn('joe', users)
588 self.assertIn('bob', users)
589- self.assertEquals({'tr_me': True}, users['joe'])
590- self.assertEquals({}, users['bob'])
591+ self.assertEquals({'tr_me': True, 'default': False}, users['joe'])
592+ self.assertEquals({'default': False}, users['bob'])
593
594 def test_users_dict(self):
595 distro = self._make_distro('ubuntu')
596@@ -237,5 +267,5 @@
597 (users, _groups) = self._norm(ug_cfg, distro)
598 self.assertIn('joe', users)
599 self.assertIn('bob', users)
600- self.assertEquals({}, users['joe'])
601- self.assertEquals({}, users['bob'])
602+ self.assertEquals({'default': False}, users['joe'])
603+ self.assertEquals({'default': False}, users['bob'])