Merge ~goneri/cloud-init:freebsd_set_password into cloud-init:master

Proposed by Gonéri Le Bouder
Status: Needs review
Proposed branch: ~goneri/cloud-init:freebsd_set_password
Merge into: cloud-init:master
Diff against target: 219 lines (+72/-63)
5 files modified
cloudinit/config/cc_set_passwords.py (+16/-56)
cloudinit/config/tests/test_set_passwords.py (+32/-4)
cloudinit/distros/__init__.py (+13/-1)
cloudinit/distros/freebsd.py (+11/-1)
tools/build-on-freebsd (+0/-1)
Reviewer Review Type Date Requested Status
Mina Galić (community) Needs Fixing
Chad Smith Needs Information
Ryan Harper Needs Fixing
Andrey Fesenko (community) it's now work Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+368507@code.launchpad.net

Commit message

set_passwords: support for FreeBSD

Add FreeBSD support to `cc_set_passwords`.
FreeBSD does not come with chpasswd binary, a chpasswd port[1] exists
but it's not the same tool than the one from Linux[2].

Finally, NetBSD and OpenBSD don't have the chpasswd binary at all, so
an abstraction layer will be needed.

This patch moves the Linux specific in the Distro class, this allow
anyone to override the default implementation. It also provides an
implementation for FreeBSD.

[1]: https://www.freshports.org/www/chpasswd/
[2]: https://github.com/shadow-maint/shadow/blob/master/src/chpasswd.c

To post a comment you must log in.
Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for the patch, some inline comments and questions.

review: Needs Fixing
Revision history for this message
Gonéri Le Bouder (goneri) :
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

Hi Ryan, I pushed a new update to address your comments.

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks, some inline comments.

review: Needs Fixing
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

thanks, it should be fine now.

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for refactoring. I've one more request inline.

Revision history for this message
Gonéri Le Bouder (goneri) wrote :

Update: set_password() was actually working fine. I've updated my patch to keep the original version.

Revision history for this message
Ryan Harper (raharper) wrote :

OK, then do we need this branch at all ?

Revision history for this message
Ryan Harper (raharper) :
review: Needs Information
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

Answered inline.

Revision history for this message
Gonéri Le Bouder (goneri) wrote :

Could you take a look at the last version to be sure I correctly understand what you want? I will refresh the test-suite later.

Revision history for this message
Ryan Harper (raharper) wrote :

OK. If the core issue is that freebsd doesn't have a chpasswd, which takes a list of users to modify, rather than a single user (like passwd). Then I think we need a different interface.

In cc_set_password, something like:

def chpassword(plist_in, hashed=False):
    if util.which('chpasswd'):
 util.subp(['chpasswd'], plist_in)
    else:
 for u, p in plist_in.splitlines().split(":"):
     distro.set_passwd(u, p, hashed=hashed)

And then the two places in handle() which call util.subp(['chpasswd'])
will use

chpassword(plist_in)
chpassword(plist_in, hashed=True)

And the expire already uses passwd directly.

The result of the changes should pass unittest without any changes, except on the
FreeBSD path, we'd mock out the util.which('chpasswd') to False, taking us down the
path that calls distro.set_passwd() for each user:pass pair since there's no chpasswd
binary on FreeBSD.

Make sense?

review: Needs Fixing
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

It sounds like you want to jumpback to versin #1. I initially started with a tiny patch with a limited impacted. Follow-up to the iterations of the review process, we end-up with this slightly more invasive change. I personally think it's a better patch since:

- there is now only one way to set the user password
- it resolves the chpasswd issue
- it removes > 40 lines of code and several variables.

Revision history for this message
Ryan Harper (raharper) wrote :

Gonéri ,

Yes. I'm sorry about the back and forth. I didn't understand the difference between chpasswd and passwd , the former operating on a list of passwords vs a single password. Thank for sticking with this work.

With that in mind; I worry about churn here; and I also don't like converting what was a single subprocess call to chpasswd to multiple calls to passwd. Obviously in the case where chpasswd is not available, we must use multiple calls to passwd.

So yes, I think something closer to your version 1, but I think we can abstract it in cc_set_password itself.

Something like this:

http://paste.ubuntu.com/p/xh5DmKvWy5/

Revision history for this message
Gonéri Le Bouder (goneri) wrote :

Thank for your feedback. There is another chpasswd binary on FreeBSD (https://www.freshports.org/www/chpasswd/) that is supposed to be compatible with the one Linux. But it's not maintained anymore and so, may not be as reliable as the official version. I've afraid the use of 'which' may create some lack of consitency in the plugins. Since it's pretty hard to troubleshoot Cloud-Init, I prefer to have something absolutely deterministic.

Also, depending on the format of the metadata, two different functions are used to set the password. This lack of consistency increases the complexity of the software (two functions for the same result) and result of subtile bugs. For instance, in the case of FreeBSD, one of the two strategy was working fine, the other was broken.

I understand you feel nervous with the change, but I'm convinced it will improve the quality of the code base. I suggest we try test it more intensivly instead. If you are willing to go in this direction, I will be happy to design some functional tests to validate the result on different operating system image. For instance, I already use Ansible locally to valide cloud-init behaviour.

Revision history for this message
Andrey Fesenko (f0andrey) wrote :

Wow
This Fix all mode cc_set_passwords (chpasswd:), hash, plain text, random generated.

I'm submit update in port FreeBSD
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=240372

Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

done

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

This looks good to me Gonéri, the only thing we 'lose' in terms of behavior:
 - visibility to log message distinguishing whether we are hashed or not
 - bailing on first set_passwd error instead of trying to set all and raising errors at the end

The second point I'd like to fix.

Revision history for this message
Ryan Harper (raharper) wrote :

Shouldn't we fix both? My biggest objection here was that
this path is rather critical and I hesitate to make changes.

The least we can do is ensure we have the same path on !FreeBSD
w.r.t behavior and message logs.

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

PASSED: Continuous integration, rev:f653072bbbb21aac0330206982e84a171a78ab53
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1122/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1122//rebuild

review: Approve (continuous-integration)
Revision history for this message
Gonéri Le Bouder (goneri) :
Revision history for this message
Gonéri Le Bouder (goneri) :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:9e18f6780cc4999cc31332e13ad2d22a4d27e8da
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1136/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1136//rebuild

review: Approve (continuous-integration)
Revision history for this message
Andrey Fesenko (f0andrey) wrote :
review: Approve (it's now work)
Revision history for this message
Ryan Harper (raharper) wrote :

For non-FreeBSD, I don't understand why we would trade a single exec of chpasswd reading stdin list of users for an exec per-user.

IIUC, the previous patch I proposed:

http://paste.ubuntu.com/p/xh5DmKvWy5/

The objection was that util.which could find a chpasswd on freebsd which could be problematic.

If that were changed to use:

if not util.is_FreeBSD():
   # chpasswd path
else:
   distro.set_password()

Which would ensure that on the FreeBSD path it wouldn't use chpasswd even if present in the image. And for non-FreeBSD we keep the same code path we've had.

review: Needs Fixing
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

> For non-FreeBSD, I don't understand why we would trade a single exec of chpasswd
> reading stdin list of users for an exec per-user.

cloud-init has two different way to set a password, this without any concrete benefit.
It's a lot of duplicated code and this creates case like this one where only one of the
two is broken. So, instead of adding an extra if/else block to deal with the current situation,
it sounds reasonable to me to just reduce this complexity.

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

Gonéri. Thanks for the continued discussion here.

Per your discussion points:

<Gonéri> cloud-init has two different way to set a password, this without any concrete benefit.
<chad> the concrete benefit is a single bulk operation vs single operations which involve the overhead of spinning up subprocess for each call.

<Gonéri> It's a lot of duplicated code and this creates case like this one where only one of the
two is broken. So, instead of adding an extra if/else block to deal with the current situation,
it sounds reasonable to me to just reduce this complexity.

<chad> yes, bulk operations is broken for freeBSD; but not on centos, sles, opensuse, rhel, ubuntu, debian, gentoo or arch. I'm hesitant to move all distribution behavior out of an optimal feature path because one distro doesn't have supported tooling. If we followed that policy in general, we could only have cloudinit perform only as good as the worst case scenario on any distro or cloud for any feature.

cloud-init would like distributions and individual clouds to be able to differentiate each other with various features and perform relatively better based on feature sets that each platform/distro has. So with that comes a bit of complexity. If there is a better way to do things on one cloud or distro, we want cloud-init to have the best experience there (at the cost of developer complexity).

As such, I've taken both Ryan's and your suggestions and tried to merge them so that bulk-chpasswd OSes can perform one operation and bsd can perform unique chpasswd calls per user.

Here is the attached diff, hopefully it tries to meet the intent of your branch:

https://paste.ubuntu.com/p/YP9yW864zT/

If we can agree on something close to this, I think we can merge it and resolve the bug.

review: Needs Information
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

Thanks Chad! the patch works great.

Revision history for this message
Mina Galić (minagalic) wrote :

could we add ~chad.smith's change and get this merged?

review: Needs Fixing
Revision history for this message
Gonéri Le Bouder (goneri) wrote :

> could we add ~chad.smith's change and get this merged?

I would prefer if Chad just merge his patch directly. He owns the patch and it will be easier this way.

Revision history for this message
Gonéri Le Bouder (goneri) wrote :

Unmerged commits

9e18f67... by Gonéri Le Bouder

set_passwords: support for FreeBSD

Add FreeBSD support to `cc_set_passwords`.
FreeBSD does not come with chpasswd binary, a chpasswd port[1] exists
but it's not the same tool than the one from Linux[2].

Finally, NetBSD and OpenBSD don't have the chpasswd binary at all, so
an abstraction layer will be needed.

This patch moves the Linux specific in the Distro class, this allow
anyone to override the default implementation. It also provides an
implementation for FreeBSD.

[1]: https://www.freshports.org/www/chpasswd/
[2]: https://github.com/shadow-maint/shadow/blob/master/src/chpasswd.c

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py
index cf9b5ab..9e6238a 100755
--- a/cloudinit/config/cc_set_passwords.py
+++ b/cloudinit/config/cc_set_passwords.py
@@ -166,74 +166,34 @@ def handle(_name, cfg, cloud, log, args):
166 else:166 else:
167 log.warn("No default or defined user to change password for.")167 log.warn("No default or defined user to change password for.")
168168
169 errors = []169 random_msg = (
170 "Set the following 'random' password:\n"
171 " '%s' to %s")
172
170 if plist:173 if plist:
171 plist_in = []
172 hashed_plist_in = []
173 hashed_users = []
174 randlist = []
175 users = []174 users = []
176 # N.B. This regex is included in the documentation (i.e. the module175 # N.B. This regex is included in the documentation (i.e. the module
177 # docstring), so any changes to it should be reflected there.176 # docstring), so any changes to it should be reflected there.
178 prog = re.compile(r'\$(1|2a|2y|5|6)(\$.+){2}')177 prog = re.compile(r'\$(1|2a|2y|5|6)(\$.+){2}')
179 for line in plist:178 for line in plist:
180 u, p = line.split(':', 1)
181 if prog.match(p) is not None and ":" not in p:
182 hashed_plist_in.append("%s:%s" % (u, p))
183 hashed_users.append(u)
184 else:
185 if p == "R" or p == "RANDOM":
186 p = rand_user_password()
187 randlist.append("%s:%s" % (u, p))
188 plist_in.append("%s:%s" % (u, p))
189 users.append(u)
190
191 ch_in = '\n'.join(plist_in) + '\n'
192 if users:
193 try:179 try:
194 log.debug("Changing password for %s:", users)180 u, p = line.split(':', 1)
195 util.subp(['chpasswd'], ch_in)181 log.debug("Setting password for: %s" % u)
196 except Exception as e:182 if prog.match(p) is not None and ":" not in p:
197 errors.append(e)183 cloud.distro.set_passwd(u, p, hashed=True, expire=expire)
198 util.logexc(184 elif p == "R" or p == "RANDOM":
199 log, "Failed to set passwords with chpasswd for %s", users)185 p = rand_user_password()
200186 sys.stderr.write(random_msg % (p, u))
201 hashed_ch_in = '\n'.join(hashed_plist_in) + '\n'187 cloud.distro.set_passwd(u, p, expire=expire)
202 if hashed_users:188 else:
203 try:189 cloud.distro.set_passwd(u, p, expire=expire)
204 log.debug("Setting hashed password for %s:", hashed_users)190 except Exception:
205 util.subp(['chpasswd', '-e'], hashed_ch_in)191 log.exception('failed to set password')
206 except Exception as e:
207 errors.append(e)
208 util.logexc(
209 log, "Failed to set hashed passwords with chpasswd for %s",
210 hashed_users)
211
212 if len(randlist):
213 blurb = ("Set the following 'random' passwords\n",
214 '\n'.join(randlist))
215 sys.stderr.write("%s\n%s\n" % blurb)
216
217 if expire:
218 expired_users = []
219 for u in users:
220 try:
221 util.subp(['passwd', '--expire', u])
222 expired_users.append(u)
223 except Exception as e:
224 errors.append(e)
225 util.logexc(log, "Failed to set 'expire' for %s", u)
226 if expired_users:
227 log.debug("Expired passwords for: %s users", expired_users)
228192
229 handle_ssh_pwauth(193 handle_ssh_pwauth(
230 cfg.get('ssh_pwauth'), service_cmd=cloud.distro.init_cmd,194 cfg.get('ssh_pwauth'), service_cmd=cloud.distro.init_cmd,
231 service_name=cloud.distro.get_option('ssh_svcname', 'ssh'))195 service_name=cloud.distro.get_option('ssh_svcname', 'ssh'))
232196
233 if len(errors):
234 log.debug("%s errors occured, re-raising the last one", len(errors))
235 raise errors[-1]
236
237197
238def rand_user_password(pwlen=9):198def rand_user_password(pwlen=9):
239 return util.rand_str(pwlen, select_from=PW_SET)199 return util.rand_str(pwlen, select_from=PW_SET)
diff --git a/cloudinit/config/tests/test_set_passwords.py b/cloudinit/config/tests/test_set_passwords.py
index a2ea5ec..274c253 100644
--- a/cloudinit/config/tests/test_set_passwords.py
+++ b/cloudinit/config/tests/test_set_passwords.py
@@ -101,11 +101,39 @@ class TestSetPasswordsHandle(CiTestCase):
101 'DEBUG: Handling input for chpasswd as list.',101 'DEBUG: Handling input for chpasswd as list.',
102 self.logs.getvalue())102 self.logs.getvalue())
103 self.assertIn(103 self.assertIn(
104 "DEBUG: Setting hashed password for ['root', 'ubuntu']",104 "DEBUG: Setting password for: root",
105 self.logs.getvalue())105 self.logs.getvalue())
106 self.assertEqual(106 self.assertIn(
107 [mock.call(['chpasswd', '-e'],107 "DEBUG: Setting password for: ubuntu",
108 '\n'.join(valid_hashed_pwds) + '\n')],108 self.logs.getvalue())
109 self.assertEqual([
110 mock.call(['chpasswd', '-e'], valid_hashed_pwds[0],
111 logstring='chpasswd for root'),
112 mock.call(['passwd', '--expire', 'root']),
113 mock.call(['chpasswd', '-e'], valid_hashed_pwds[1],
114 logstring='chpasswd for ubuntu'),
115 mock.call(['passwd', '--expire', 'ubuntu'])],
109 m_subp.call_args_list)116 m_subp.call_args_list)
110117
118 @mock.patch(MODPATH + "util.subp")
119 def test_handle_on_broken_list(self, m_subp):
120 """Ensure the other password is set even if the first is broken."""
121 cloud = self.tmp_cloud(distro='ubuntu')
122 pwds = ['broken:a', 'continue:c']
123 cfg = {'chpasswd': {'list': pwds, 'expire': False}}
124 with mock.patch(MODPATH + 'util.subp') as m_subp:
125 def side_effect(*args, **kwargs):
126 if len(args) > 1 and args[1] == 'broken:a':
127 raise Exception('Boom!')
128 m_subp.side_effect = side_effect
129 setpass.handle(
130 'IGNORED', cfg=cfg, cloud=cloud, log=self.logger, args=[])
131 self.assertIn(
132 "WARNING: Failed to set password for broken",
133 self.logs.getvalue())
134 self.assertIn(
135 "DEBUG: Setting password for: continue",
136 self.logs.getvalue())
137
138
111# vi: ts=4 expandtab139# vi: ts=4 expandtab
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index 00bdee3..ff740b6 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -591,7 +591,7 @@ class Distro(object):
591 util.logexc(LOG, 'Failed to disable password for user %s', name)591 util.logexc(LOG, 'Failed to disable password for user %s', name)
592 raise e592 raise e
593593
594 def set_passwd(self, user, passwd, hashed=False):594 def set_passwd(self, user, passwd, hashed=False, expire=False):
595 pass_string = '%s:%s' % (user, passwd)595 pass_string = '%s:%s' % (user, passwd)
596 cmd = ['chpasswd']596 cmd = ['chpasswd']
597597
@@ -607,8 +607,20 @@ class Distro(object):
607 util.logexc(LOG, "Failed to set password for %s", user)607 util.logexc(LOG, "Failed to set password for %s", user)
608 raise e608 raise e
609609
610 if expire:
611 self._expire_password(user)
610 return True612 return True
611613
614 def _expire_password(self, user):
615 """
616 The users will have to enter a new password the first time they log in
617 """
618 try:
619 util.subp(['passwd', '--expire', user])
620 except Exception as e:
621 util.logexc(LOG, "Failed to set pw expiration for %s", user)
622 raise e
623
612 def ensure_sudo_dir(self, path, sudo_base='/etc/sudoers'):624 def ensure_sudo_dir(self, path, sudo_base='/etc/sudoers'):
613 # Ensure the dir is included and that625 # Ensure the dir is included and that
614 # it actually exists as a directory626 # it actually exists as a directory
diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py
index f7825fd..67cd42e 100644
--- a/cloudinit/distros/freebsd.py
+++ b/cloudinit/distros/freebsd.py
@@ -233,7 +233,7 @@ class Distro(distros.Distro):
233 if passwd_val is not None:233 if passwd_val is not None:
234 self.set_passwd(name, passwd_val, hashed=True)234 self.set_passwd(name, passwd_val, hashed=True)
235235
236 def set_passwd(self, user, passwd, hashed=False):236 def set_passwd(self, user, passwd, hashed=False, expire=False):
237 if hashed:237 if hashed:
238 hash_opt = "-H"238 hash_opt = "-H"
239 else:239 else:
@@ -246,6 +246,16 @@ class Distro(distros.Distro):
246 util.logexc(LOG, "Failed to set password for %s", user)246 util.logexc(LOG, "Failed to set password for %s", user)
247 raise e247 raise e
248248
249 if expire:
250 self._expire_password(user)
251
252 def _expire_password(self, user):
253 try:
254 util.subp(['pw', 'usermod', user, '-p', '01-Jan-1970'])
255 except Exception as e:
256 util.logexc(LOG, "Failed to set pw expiration for %s", user)
257 raise e
258
249 def lock_passwd(self, name):259 def lock_passwd(self, name):
250 try:260 try:
251 util.subp(['pw', 'usermod', name, '-h', '-'])261 util.subp(['pw', 'usermod', name, '-h', '-'])
diff --git a/tools/build-on-freebsd b/tools/build-on-freebsd
index 8ae6456..876368a 100755
--- a/tools/build-on-freebsd
+++ b/tools/build-on-freebsd
@@ -18,7 +18,6 @@ py_prefix=$(${PYTHON} -c 'import sys; print("py%d%d" % (sys.version_info.major,
18depschecked=/tmp/c-i.dependencieschecked18depschecked=/tmp/c-i.dependencieschecked
19pkgs="19pkgs="
20 bash20 bash
21 chpasswd
22 dmidecode21 dmidecode
23 e2fsprogs22 e2fsprogs
24 $py_prefix-Jinja223 $py_prefix-Jinja2

Subscribers

People subscribed via source and target branches