Merge lp:~stub/launchpad/botbake into lp:launchpad

Proposed by Stuart Bishop
Status: Merged
Merged at revision: 18333
Proposed branch: lp:~stub/launchpad/botbake
Merge into: lp:launchpad
Diff against target: 215 lines (+201/-0)
3 files modified
lib/lp/registry/scripts/createbotaccount.py (+123/-0)
lib/lp/registry/scripts/tests/test_createbotaccount.py (+65/-0)
scripts/create-bot-account.py (+13/-0)
To merge this branch: bzr merge lp:~stub/launchpad/botbake
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+319445@code.launchpad.net

Description of the change

Command line tool to create bot accounts, for Canonical IS automation work.

Please let me know if this is an acceptable approach. If ok I'll add
tests.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

I don't see anything obviously terrible here, though William should definitely take a look as well.

Revision history for this message
William Grant (wgrant) :
Revision history for this message
William Grant (wgrant) wrote :

I'd marginally prefer that the team stuff were moved into an API script, but if you don't care then this is fine.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/lp/registry/scripts/createbotaccount.py'
2--- lib/lp/registry/scripts/createbotaccount.py 1970-01-01 00:00:00 +0000
3+++ lib/lp/registry/scripts/createbotaccount.py 2017-03-21 07:36:10 +0000
4@@ -0,0 +1,123 @@
5+# Copyright 2017 Canonical Ltd. This software is licensed under the
6+# GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+"""Create a bot account."""
9+
10+from zope.component import getUtility
11+
12+from lp.registry.interfaces.person import IPersonSet
13+from lp.registry.interfaces.ssh import ISSHKeySet
14+from lp.services.scripts.base import (
15+ LaunchpadScript,
16+ LaunchpadScriptFailure,
17+ )
18+from lp.services.webapp import canonical_url
19+
20+
21+class CreateBotAccountScript(LaunchpadScript):
22+
23+ description = "Create a bot account."
24+ output = None
25+
26+ def add_my_options(self):
27+ self.parser.add_option(
28+ '-u', '--username', metavar='NAME', action='store',
29+ type='string', dest='username', default='',
30+ help='Username for the bot')
31+ self.parser.add_option(
32+ '--openid', metavar='SUFFIX', action='store',
33+ type='string', dest='openid', default='',
34+ help=('OpenID identifier suffix. Normally unnecessary because '
35+ 'SSO account creation handles it'))
36+ self.parser.add_option(
37+ '-e', '--email', metavar='ADDR', action='store',
38+ type='string', dest='email', default='',
39+ help='Email address. Defaults to webops+username@canonical.com')
40+ self.parser.add_option(
41+ '-k', '--sshkey', metavar='TXT', action='store',
42+ type='string', dest='sshkey', default='',
43+ help='SSH public key. Defaults to no ssh key.')
44+ self.parser.add_option(
45+ '-t', '--teams', metavar='TEAMS', action='store',
46+ type='string', dest='teams',
47+ default='canonical-is-devopsolution-bots',
48+ help='Add bot to this comma separated list of teams')
49+
50+ def main(self):
51+ username = unicode(self.options.username)
52+ if not username:
53+ raise LaunchpadScriptFailure('--username is required')
54+ openid_suffix = unicode(self.options.openid)
55+ if '/' in openid_suffix:
56+ raise LaunchpadScriptFailure(
57+ 'Invalid OpenID suffix {}'.format(openid_suffix))
58+
59+ displayname = u'\U0001f916 {}'.format(username) # U+1f916==ROBOT FACE
60+
61+ if self.options.email:
62+ emailaddress = unicode(self.options.email)
63+ else:
64+ emailaddress = u'webops+{}@canonical.com'.format(username)
65+
66+ if self.options.teams:
67+ teamnames = [unicode(t.strip())
68+ for t in self.options.teams.split(',')
69+ if t.strip()]
70+ else:
71+ teamnames = []
72+
73+ sshkey_text = unicode(self.options.sshkey) # Optional
74+
75+ person_set = getUtility(IPersonSet)
76+
77+ if openid_suffix and person_set.getByName(username) is None:
78+ # Normally the SSO has already called this method.
79+ # This codepath is really only used for testing.
80+ person_set.createPlaceholderPerson(openid_suffix, username)
81+
82+ person = person_set.getByName(username)
83+ if person is None:
84+ raise LaunchpadScriptFailure(
85+ 'Account {} does not exist'.format(username))
86+ if person.account is None:
87+ raise LaunchpadScriptFailure(
88+ 'Person {} has no Account'.format(username))
89+ if person.account.openid_identifiers.count() != 1:
90+ raise LaunchpadScriptFailure(
91+ 'Account {} has invalid OpenID identifiers'.format(username))
92+ openid_identifier = person.account.openid_identifiers.one()
93+
94+ # Create the IPerson
95+ person, _ = person_set.getOrCreateByOpenIDIdentifier(
96+ openid_identifier.identifier,
97+ emailaddress,
98+ displayname,
99+ None,
100+ 'when the create-bot-account launchpad script was run')
101+
102+ # person.name = username
103+ person.selfgenerated_bugnotifications = True
104+ person.expanded_notification_footers = True
105+ person.description = 'Canonical IS created bot'
106+ person.hide_email_addresses = True
107+
108+ # The email address must be fully validated.
109+ assert person.preferredemail is not None
110+
111+ # Add team memberships
112+ for teamname in teamnames:
113+ team = person_set.getByName(teamname)
114+ if team is None or not team.is_team:
115+ raise LaunchpadScriptFailure(
116+ '{} is not a team'.format(teamname))
117+ team.addMember(person, person)
118+
119+ # Add ssh key
120+ sshkey_set = getUtility(ISSHKeySet)
121+ if sshkey_text and (
122+ sshkey_set.getByPersonAndKeyText(person,
123+ sshkey_text).count() == 0):
124+ sshkey_set.new(person, sshkey_text, send_notification=False)
125+
126+ self.logger.info('Created or updated {}'.format(canonical_url(person)))
127+ self.txn.commit()
128
129=== added file 'lib/lp/registry/scripts/tests/test_createbotaccount.py'
130--- lib/lp/registry/scripts/tests/test_createbotaccount.py 1970-01-01 00:00:00 +0000
131+++ lib/lp/registry/scripts/tests/test_createbotaccount.py 2017-03-21 07:36:10 +0000
132@@ -0,0 +1,65 @@
133+# Copyright 2017 Canonical Ltd. This software is licensed under the
134+# GNU Affero General Public License version 3 (see the file LICENSE).
135+
136+"""Tests for PersonSet."""
137+
138+__metaclass__ = type
139+
140+
141+from mock import MagicMock
142+from zope.component import getUtility
143+from lp.registry.interfaces.person import IPersonSet
144+from lp.registry.interfaces.ssh import ISSHKeySet
145+from lp.registry.scripts.createbotaccount import CreateBotAccountScript
146+from lp.services.identity.interfaces.emailaddress import EmailAddressStatus
147+from lp.testing import TestCase
148+from lp.testing.layers import ZopelessDatabaseLayer
149+
150+
151+class CreateBotAccountTests(TestCase):
152+ """Test `IPersonSet`."""
153+ layer = ZopelessDatabaseLayer
154+
155+ def test_createbotaccount(self):
156+ script = CreateBotAccountScript()
157+
158+ class _opt:
159+ username = 'botty'
160+ openid = 'bottyid'
161+ email = ''
162+ teams = 'rosetta-admins,simple-team'
163+ sshkey = ('ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA9BC4zfVrGsve6zh'
164+ 'jOiEftyNMjqV8YMv1lLMpbWqa7Eqr0ZL+oAoJQMq2w8Dk/1hrgJ'
165+ '1pxdwxwQWogDHZTer8YDa89OSBWGenl++s6bk28h/ysZettSS82'
166+ 'BrfpoSUc8Cfz2K1SbI9kz5OhmE4nBVsJgsdiHp9WwwQiyRrjfAu'
167+ 'NhE= whatever@here.local.')
168+
169+ script.options = _opt
170+ script.logger = MagicMock()
171+ script.txn = MagicMock()
172+ script.main()
173+
174+ person_set = getUtility(IPersonSet)
175+
176+ person = person_set.getByName(u'botty')
177+ self.assertEqual(person.name, u'botty')
178+ self.assertTrue(person.hide_email_addresses)
179+ # Bots tend to flood email, so filtering is important.
180+ self.assertTrue(person.expanded_notification_footers)
181+
182+ account = person.account
183+ openid = account.openid_identifiers.one()
184+ self.assertEqual(openid.identifier, u'bottyid')
185+
186+ sshkey_set = getUtility(ISSHKeySet)
187+ self.assertIsNotNone(
188+ sshkey_set.getByPersonAndKeyText(person, _opt.sshkey))
189+
190+ email = person.preferredemail
191+ self.assertEqual(email.email, 'webops+botty@canonical.com')
192+ self.assertEqual(email.status, EmailAddressStatus.PREFERRED)
193+
194+ self.assertTrue(person.inTeam(person_set.getByName('rosetta-admins')))
195+ self.assertTrue(person.inTeam(person_set.getByName('simple-team')))
196+
197+ self.assertTrue(script.txn.commit.called)
198
199=== added file 'scripts/create-bot-account.py'
200--- scripts/create-bot-account.py 1970-01-01 00:00:00 +0000
201+++ scripts/create-bot-account.py 2017-03-21 07:36:10 +0000
202@@ -0,0 +1,13 @@
203+#!/usr/bin/python -S
204+#
205+# Copyright 2017 Canonical Ltd. This software is licensed under the
206+# GNU Affero General Public License version 3 (see the file LICENSE).
207+
208+import _pythonpath
209+
210+from lp.registry.scripts.createbotaccount import CreateBotAccountScript
211+
212+
213+if __name__ == '__main__':
214+ script = CreateBotAccountScript('create-bot-account', dbuser='launchpad')
215+ script.run()