Merge lp:~trapnine/maas/fix-1510224 into lp:~maas-committers/maas/trunk

Proposed by Jeffrey C Jones
Status: Merged
Approved by: Jeffrey C Jones
Approved revision: no longer in the source branch.
Merged at revision: 4478
Proposed branch: lp:~trapnine/maas/fix-1510224
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 133 lines (+102/-1)
2 files modified
src/maasserver/management/commands/changepasswords.py (+53/-0)
src/maasserver/tests/test_commands.py (+49/-1)
To merge this branch: bzr merge lp:~trapnine/maas/fix-1510224
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+276725@code.launchpad.net

Commit message

New changepasswords command. Updates multiple MAAS passwords non-interactively from STDIN, just like chpasswd.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Looks good. I have a few suggestions. Dealing with input encoding is important. There's a blocker in the tests where global state is mutated and not reset; more in the diff comments.

review: Needs Fixing
Revision history for this message
Jeffrey C Jones (trapnine) wrote :

> Looks good. I have a few suggestions. Dealing with input encoding is
> important. There's a blocker in the tests where global state is mutated and
> not reset; more in the diff comments.

Thanks Gavin. Everything's been addressed, please take a look.

Revision history for this message
Gavin Panella (allenap) wrote :

Looks great!

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (75.7 KiB)

The attempt to merge lp:~trapnine/maas/fix-1510224 into lp:maas failed. Below is the output from the failed tests.

Get:1 http://security.ubuntu.com trusty-security InRelease [64.4 kB]
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages
Get:2 http://security.ubuntu.com trusty-security/main Sources [98.0 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Get:3 http://security.ubuntu.com trusty-security/universe Sources [30.9 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Get:4 http://security.ubuntu.com trusty-security/main amd64 Packages [362 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Get:5 http://security.ubuntu.com trusty-security/universe amd64 Packages [117 kB]
Get:6 http://security.ubuntu.com trusty-security/main Translation-en [196 kB]
Get:7 http://security.ubuntu.com trusty-security/universe Translation-en [68.4 kB]
Fetched 937 kB in 3s (246 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm pep8 phantomjs postgresql pyflakes python-apt python-bson python-bzrlib python-convoy python-coverage python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lxml python-mock python-netaddr python-netifaces python-nose python-oauth python-openssl python-paramiko python-pexpect python-pip python-pocket-lint python-psycopg2 python-pyinotify python-pyparsing python-seamicroclient python-simplejson python-simplestreams python-sphinx python-subunit python-tempita python-testresources python-testscenarios python-testtools python-twisted python-txtftp py...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'src/maasserver/management/commands/changepasswords.py'
2--- src/maasserver/management/commands/changepasswords.py 1970-01-01 00:00:00 +0000
3+++ src/maasserver/management/commands/changepasswords.py 2015-11-09 07:40:54 +0000
4@@ -0,0 +1,53 @@
5+# Copyright 2015 Canonical Ltd. This software is licensed under the
6+# GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+"""Django command: Batch update multiple passwords non-interactively."""
9+
10+from fileinput import (
11+ hook_encoded,
12+ input,
13+)
14+from textwrap import dedent
15+
16+from django.contrib.auth import get_user_model
17+from django.core.management.base import (
18+ BaseCommand,
19+ CommandError,
20+)
21+from maasserver.utils.orm import transactional
22+
23+
24+class Command(BaseCommand):
25+ help = dedent("""\
26+ Update passwords in batch mode.
27+
28+ Like the chpasswd command, this command reads a list of username and
29+ password pairs from standard input and uses this information to update
30+ a group of existing users. The input must be UTF8 encoded, and each
31+ line is of the format:
32+
33+ username:password
34+
35+ A list of files can be provided as arguments. If provided, the input
36+ will be read from the files instead of standard input.""")
37+
38+ @transactional
39+ def handle(self, *args, **options):
40+ count = 0
41+ UserModel = get_user_model()
42+ for line in input(args, openhook=hook_encoded("utf-8")):
43+ try:
44+ username, password = line.rstrip('\r\n').split(":", 1)
45+ except ValueError:
46+ raise CommandError(
47+ "Invalid input provided. "
48+ "Format is 'username:password', one per line.")
49+ try:
50+ user = UserModel._default_manager.get(
51+ **{UserModel.USERNAME_FIELD: username})
52+ except UserModel.DoesNotExist:
53+ raise CommandError("User '%s' does not exist." % username)
54+ user.set_password(password)
55+ user.save()
56+ count += 1
57+ return "%d password(s) successfully changed." % count
58
59=== modified file 'src/maasserver/tests/test_commands.py'
60--- src/maasserver/tests/test_commands.py 2015-06-16 15:51:05 +0000
61+++ src/maasserver/tests/test_commands.py 2015-11-09 07:40:54 +0000
62@@ -16,15 +16,20 @@
63
64 from codecs import getwriter
65 from io import BytesIO
66+import StringIO
67
68 from apiclient.creds import convert_tuple_to_string
69 import django
70 from django.contrib.auth.models import User
71 from django.core.management import call_command
72 from django.core.management.base import CommandError
73-from maasserver.management.commands import createadmin
74+from maasserver.management.commands import (
75+ changepasswords,
76+ createadmin,
77+)
78 from maasserver.models.user import get_creds_tuple
79 from maasserver.testing.factory import factory
80+from maasserver.testing.orm import reload_object
81 from maasserver.utils.orm import get_one
82 from maastesting.djangotestcase import DjangoTestCase
83 from testtools.matchers import StartsWith
84@@ -182,6 +187,49 @@
85 createadmin.prompt_for_email)
86
87
88+class TestChangePasswords(DjangoTestCase):
89+
90+ def test_bad_input(self):
91+ stdin = StringIO.StringIO("nobody")
92+ self.patch(changepasswords, 'input').return_value = stdin
93+ error_text = assertCommandErrors(self, 'changepasswords')
94+ self.assertIn(
95+ "Invalid input provided. "
96+ "Format is 'username:password', one per line.", error_text)
97+
98+ def test_nonexistent_user(self):
99+ stdin = StringIO.StringIO("nobody:nopass")
100+ self.patch(changepasswords, 'input').return_value = stdin
101+ error_text = assertCommandErrors(self, 'changepasswords')
102+ self.assertIn("User 'nobody' does not exist.", error_text)
103+
104+ def test_changes_one_password(self):
105+ username = factory.make_username()
106+ password = factory.make_string(size=16, spaces=True, prefix="password")
107+ user = factory.make_User(username=username, password=password)
108+ self.assertTrue(user.check_password(password))
109+ newpass = factory.make_string(size=16, spaces=True, prefix="newpass")
110+ stdin = StringIO.StringIO("%s:%s" % (username, newpass))
111+ self.patch(changepasswords, 'input').return_value = stdin
112+ call_command('changepasswords')
113+ self.assertTrue(reload_object(user).check_password(newpass))
114+
115+ def test_changes_ten_passwords(self):
116+ users_passwords = []
117+ stringio = StringIO.StringIO()
118+ for _ in range(10):
119+ username = factory.make_username()
120+ user = factory.make_User(username=username)
121+ newpass = factory.make_string(spaces=True, prefix="newpass")
122+ users_passwords.append((user, newpass))
123+ stringio.write("%s:%s\n" % (username, newpass))
124+ stringio.seek(0)
125+ self.patch(changepasswords, 'input').return_value = stringio
126+ call_command('changepasswords')
127+ for user, newpass in users_passwords:
128+ self.assertTrue(reload_object(user).check_password(newpass))
129+
130+
131 class TestApikeyCommand(DjangoTestCase):
132
133 def test_apikey_requires_username(self):