Merge ~smoser/cloud-init:bug/1354694-whitespace-in-group-string into cloud-init:master

Proposed by Scott Moser
Status: Merged
Merged at revision: ca3ae67211d907b4cfdcd685c0ae4f9530cb7da1
Proposed branch: ~smoser/cloud-init:bug/1354694-whitespace-in-group-string
Merge into: cloud-init:master
Diff against target: 202 lines (+160/-9)
2 files modified
cloudinit/distros/__init__.py (+13/-9)
tests/unittests/test_distros/test_create_users.py (+147/-0)
Reviewer Review Type Date Requested Status
Scott Moser Needs Resubmitting
Review via email: mp+313458@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

this needs a test added.

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

Would like to test that this config works:
#cloud-config
users:
  - default
  - name: foobar
    gecos: "My User"
    groups: sudo, adm
  - name: wark
    groups: [sudo, adm]
  - name: wark2
    groups: [sudo, "adm "]
  - name: allnew
    groups: group1, group2, group3

It is probably sufficient to test the distros add_user specifically.

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

has tests now.

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

In addition to the unit tests, I've tested the user-data below and verified expected groups and users are added and now WARN in cloud-init.log.

#cloud-config
users:
  - default
  - name: foobar
    gecos: "My User"
    groups: sudo, adm
  - name: wark
    groups: [sudo, adm]
  - name: wark2
    groups: [sudo, "adm "]
  - name: allnew
    groups: group1, group2, group3

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
2index 1f73195..94f31f8 100755
3--- a/cloudinit/distros/__init__.py
4+++ b/cloudinit/distros/__init__.py
5@@ -398,12 +398,16 @@ class Distro(object):
6 # support kwargs having groups=[list] or groups="g1,g2"
7 groups = kwargs.get('groups')
8 if groups:
9- if isinstance(groups, (list, tuple)):
10- # kwargs.items loop below wants a comma delimeted string
11- # that can go right through to the command.
12- kwargs['groups'] = ",".join(groups)
13- else:
14- groups = [group.strip() for group in groups.split(",")]
15+ if isinstance(groups, six.string_types):
16+ groups = groups.split(",")
17+
18+ # remove any white spaces in group names, most likely
19+ # that came in as a string like: groups: group1, group2
20+ groups = [g.strip() for g in groups]
21+
22+ # kwargs.items loop below wants a comma delimeted string
23+ # that can go right through to the command.
24+ kwargs['groups'] = ",".join(groups)
25
26 primary_group = kwargs.get('primary_group')
27 if primary_group:
28@@ -413,10 +417,10 @@ class Distro(object):
29 for group in groups:
30 if not util.is_group(group):
31 self.create_group(group)
32- LOG.debug("created group %s for user %s", name, group)
33+ LOG.debug("created group '%s' for user '%s'", group, name)
34
35 # Check the values and create the command
36- for key, val in kwargs.items():
37+ for key, val in sorted(kwargs.items()):
38
39 if key in adduser_opts and val and isinstance(val, str):
40 adduser_cmd.extend([adduser_opts[key], val])
41@@ -433,7 +437,7 @@ class Distro(object):
42
43 # Don't create the home directory if directed so or if the user is a
44 # system user
45- if 'no_create_home' in kwargs or 'system' in kwargs:
46+ if kwargs.get('no_create_home') or kwargs.get('system'):
47 adduser_cmd.append('-M')
48 log_adduser_cmd.append('-M')
49 else:
50diff --git a/tests/unittests/test_distros/test_create_users.py b/tests/unittests/test_distros/test_create_users.py
51new file mode 100644
52index 0000000..01a49f4
53--- /dev/null
54+++ b/tests/unittests/test_distros/test_create_users.py
55@@ -0,0 +1,147 @@
56+from cloudinit import distros
57+from ..helpers import (TestCase, mock)
58+
59+
60+class MyBaseDistro(distros.Distro):
61+ # MyBaseDistro is here to test base Distro class implementations
62+
63+ def __init__(self, name="basedistro", cfg={}, paths={}):
64+ super(MyBaseDistro, self).__init__(name, cfg, paths)
65+
66+ def install_packages(self, pkglist):
67+ raise NotImplementedError()
68+
69+ def _write_network(self, settings):
70+ raise NotImplementedError()
71+
72+ def package_command(self, cmd, args=None, pkgs=None):
73+ raise NotImplementedError()
74+
75+ def update_package_sources(self):
76+ raise NotImplementedError()
77+
78+ def apply_locale(self, locale, out_fn=None):
79+ raise NotImplementedError()
80+
81+ def set_timezone(self, tz):
82+ raise NotImplementedError()
83+
84+ def _read_hostname(self, filename, default=None):
85+ raise NotImplementedError()
86+
87+ def _write_hostname(self, hostname, filename):
88+ raise NotImplementedError()
89+
90+ def _read_system_hostname(self):
91+ raise NotImplementedError()
92+
93+
94+class TestCreateUser(TestCase):
95+ def setUp(self):
96+ super(TestCase, self).setUp()
97+ self.dist = MyBaseDistro()
98+
99+ def _useradd2call(self, args):
100+ # return a mock call for the useradd command in args
101+ # with expected 'logstring'.
102+ args = ['useradd'] + args
103+ logcmd = [a for a in args]
104+ for i in range(len(args)):
105+ if args[i] in ('--password',):
106+ logcmd[i + 1] = 'REDACTED'
107+ return mock.call(args, logstring=logcmd)
108+
109+ @mock.patch("cloudinit.distros.util.subp")
110+ def test_basic(self, m_subp):
111+ user = 'foouser'
112+ self.dist.create_user(user)
113+ self.assertEqual(
114+ m_subp.call_args_list,
115+ [self._useradd2call([user, '-m']),
116+ mock.call(['passwd', '-l', user])])
117+
118+ @mock.patch("cloudinit.distros.util.subp")
119+ def test_no_home(self, m_subp):
120+ user = 'foouser'
121+ self.dist.create_user(user, no_create_home=True)
122+ self.assertEqual(
123+ m_subp.call_args_list,
124+ [self._useradd2call([user, '-M']),
125+ mock.call(['passwd', '-l', user])])
126+
127+ @mock.patch("cloudinit.distros.util.subp")
128+ def test_system_user(self, m_subp):
129+ # system user should have no home and get --system
130+ user = 'foouser'
131+ self.dist.create_user(user, system=True)
132+ self.assertEqual(
133+ m_subp.call_args_list,
134+ [self._useradd2call([user, '--system', '-M']),
135+ mock.call(['passwd', '-l', user])])
136+
137+ @mock.patch("cloudinit.distros.util.subp")
138+ def test_explicit_no_home_false(self, m_subp):
139+ user = 'foouser'
140+ self.dist.create_user(user, no_create_home=False)
141+ self.assertEqual(
142+ m_subp.call_args_list,
143+ [self._useradd2call([user, '-m']),
144+ mock.call(['passwd', '-l', user])])
145+
146+ @mock.patch("cloudinit.distros.util.subp")
147+ def test_unlocked(self, m_subp):
148+ user = 'foouser'
149+ self.dist.create_user(user, lock_passwd=False)
150+ self.assertEqual(
151+ m_subp.call_args_list,
152+ [self._useradd2call([user, '-m'])])
153+
154+ @mock.patch("cloudinit.distros.util.subp")
155+ def test_set_password(self, m_subp):
156+ user = 'foouser'
157+ password = 'passfoo'
158+ self.dist.create_user(user, passwd=password)
159+ self.assertEqual(
160+ m_subp.call_args_list,
161+ [self._useradd2call([user, '--password', password, '-m']),
162+ mock.call(['passwd', '-l', user])])
163+
164+ @mock.patch("cloudinit.distros.util.is_group")
165+ @mock.patch("cloudinit.distros.util.subp")
166+ def test_group_added(self, m_subp, m_is_group):
167+ m_is_group.return_value = False
168+ user = 'foouser'
169+ self.dist.create_user(user, groups=['group1'])
170+ expected = [
171+ mock.call(['groupadd', 'group1']),
172+ self._useradd2call([user, '--groups', 'group1', '-m']),
173+ mock.call(['passwd', '-l', user])]
174+ self.assertEqual(m_subp.call_args_list, expected)
175+
176+ @mock.patch("cloudinit.distros.util.is_group")
177+ @mock.patch("cloudinit.distros.util.subp")
178+ def test_only_new_group_added(self, m_subp, m_is_group):
179+ ex_groups = ['existing_group']
180+ groups = ['group1', ex_groups[0]]
181+ m_is_group.side_effect = lambda m: m in ex_groups
182+ user = 'foouser'
183+ self.dist.create_user(user, groups=groups)
184+ expected = [
185+ mock.call(['groupadd', 'group1']),
186+ self._useradd2call([user, '--groups', ','.join(groups), '-m']),
187+ mock.call(['passwd', '-l', user])]
188+ self.assertEqual(m_subp.call_args_list, expected)
189+
190+ @mock.patch("cloudinit.distros.util.is_group")
191+ @mock.patch("cloudinit.distros.util.subp")
192+ def test_create_groups_with_whitespace_string(self, m_subp, m_is_group):
193+ # groups supported as a comma delimeted string even with white space
194+ m_is_group.return_value = False
195+ user = 'foouser'
196+ self.dist.create_user(user, groups='group1, group2')
197+ expected = [
198+ mock.call(['groupadd', 'group1']),
199+ mock.call(['groupadd', 'group2']),
200+ self._useradd2call([user, '--groups', 'group1,group2', '-m']),
201+ mock.call(['passwd', '-l', user])]
202+ self.assertEqual(m_subp.call_args_list, expected)

Subscribers

People subscribed via source and target branches