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
=== added file 'lib/lp/registry/scripts/createbotaccount.py'
--- lib/lp/registry/scripts/createbotaccount.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/scripts/createbotaccount.py 2017-03-21 07:36:10 +0000
@@ -0,0 +1,123 @@
1# Copyright 2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Create a bot account."""
5
6from zope.component import getUtility
7
8from lp.registry.interfaces.person import IPersonSet
9from lp.registry.interfaces.ssh import ISSHKeySet
10from lp.services.scripts.base import (
11 LaunchpadScript,
12 LaunchpadScriptFailure,
13 )
14from lp.services.webapp import canonical_url
15
16
17class CreateBotAccountScript(LaunchpadScript):
18
19 description = "Create a bot account."
20 output = None
21
22 def add_my_options(self):
23 self.parser.add_option(
24 '-u', '--username', metavar='NAME', action='store',
25 type='string', dest='username', default='',
26 help='Username for the bot')
27 self.parser.add_option(
28 '--openid', metavar='SUFFIX', action='store',
29 type='string', dest='openid', default='',
30 help=('OpenID identifier suffix. Normally unnecessary because '
31 'SSO account creation handles it'))
32 self.parser.add_option(
33 '-e', '--email', metavar='ADDR', action='store',
34 type='string', dest='email', default='',
35 help='Email address. Defaults to webops+username@canonical.com')
36 self.parser.add_option(
37 '-k', '--sshkey', metavar='TXT', action='store',
38 type='string', dest='sshkey', default='',
39 help='SSH public key. Defaults to no ssh key.')
40 self.parser.add_option(
41 '-t', '--teams', metavar='TEAMS', action='store',
42 type='string', dest='teams',
43 default='canonical-is-devopsolution-bots',
44 help='Add bot to this comma separated list of teams')
45
46 def main(self):
47 username = unicode(self.options.username)
48 if not username:
49 raise LaunchpadScriptFailure('--username is required')
50 openid_suffix = unicode(self.options.openid)
51 if '/' in openid_suffix:
52 raise LaunchpadScriptFailure(
53 'Invalid OpenID suffix {}'.format(openid_suffix))
54
55 displayname = u'\U0001f916 {}'.format(username) # U+1f916==ROBOT FACE
56
57 if self.options.email:
58 emailaddress = unicode(self.options.email)
59 else:
60 emailaddress = u'webops+{}@canonical.com'.format(username)
61
62 if self.options.teams:
63 teamnames = [unicode(t.strip())
64 for t in self.options.teams.split(',')
65 if t.strip()]
66 else:
67 teamnames = []
68
69 sshkey_text = unicode(self.options.sshkey) # Optional
70
71 person_set = getUtility(IPersonSet)
72
73 if openid_suffix and person_set.getByName(username) is None:
74 # Normally the SSO has already called this method.
75 # This codepath is really only used for testing.
76 person_set.createPlaceholderPerson(openid_suffix, username)
77
78 person = person_set.getByName(username)
79 if person is None:
80 raise LaunchpadScriptFailure(
81 'Account {} does not exist'.format(username))
82 if person.account is None:
83 raise LaunchpadScriptFailure(
84 'Person {} has no Account'.format(username))
85 if person.account.openid_identifiers.count() != 1:
86 raise LaunchpadScriptFailure(
87 'Account {} has invalid OpenID identifiers'.format(username))
88 openid_identifier = person.account.openid_identifiers.one()
89
90 # Create the IPerson
91 person, _ = person_set.getOrCreateByOpenIDIdentifier(
92 openid_identifier.identifier,
93 emailaddress,
94 displayname,
95 None,
96 'when the create-bot-account launchpad script was run')
97
98 # person.name = username
99 person.selfgenerated_bugnotifications = True
100 person.expanded_notification_footers = True
101 person.description = 'Canonical IS created bot'
102 person.hide_email_addresses = True
103
104 # The email address must be fully validated.
105 assert person.preferredemail is not None
106
107 # Add team memberships
108 for teamname in teamnames:
109 team = person_set.getByName(teamname)
110 if team is None or not team.is_team:
111 raise LaunchpadScriptFailure(
112 '{} is not a team'.format(teamname))
113 team.addMember(person, person)
114
115 # Add ssh key
116 sshkey_set = getUtility(ISSHKeySet)
117 if sshkey_text and (
118 sshkey_set.getByPersonAndKeyText(person,
119 sshkey_text).count() == 0):
120 sshkey_set.new(person, sshkey_text, send_notification=False)
121
122 self.logger.info('Created or updated {}'.format(canonical_url(person)))
123 self.txn.commit()
0124
=== added file 'lib/lp/registry/scripts/tests/test_createbotaccount.py'
--- lib/lp/registry/scripts/tests/test_createbotaccount.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/scripts/tests/test_createbotaccount.py 2017-03-21 07:36:10 +0000
@@ -0,0 +1,65 @@
1# Copyright 2017 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for PersonSet."""
5
6__metaclass__ = type
7
8
9from mock import MagicMock
10from zope.component import getUtility
11from lp.registry.interfaces.person import IPersonSet
12from lp.registry.interfaces.ssh import ISSHKeySet
13from lp.registry.scripts.createbotaccount import CreateBotAccountScript
14from lp.services.identity.interfaces.emailaddress import EmailAddressStatus
15from lp.testing import TestCase
16from lp.testing.layers import ZopelessDatabaseLayer
17
18
19class CreateBotAccountTests(TestCase):
20 """Test `IPersonSet`."""
21 layer = ZopelessDatabaseLayer
22
23 def test_createbotaccount(self):
24 script = CreateBotAccountScript()
25
26 class _opt:
27 username = 'botty'
28 openid = 'bottyid'
29 email = ''
30 teams = 'rosetta-admins,simple-team'
31 sshkey = ('ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA9BC4zfVrGsve6zh'
32 'jOiEftyNMjqV8YMv1lLMpbWqa7Eqr0ZL+oAoJQMq2w8Dk/1hrgJ'
33 '1pxdwxwQWogDHZTer8YDa89OSBWGenl++s6bk28h/ysZettSS82'
34 'BrfpoSUc8Cfz2K1SbI9kz5OhmE4nBVsJgsdiHp9WwwQiyRrjfAu'
35 'NhE= whatever@here.local.')
36
37 script.options = _opt
38 script.logger = MagicMock()
39 script.txn = MagicMock()
40 script.main()
41
42 person_set = getUtility(IPersonSet)
43
44 person = person_set.getByName(u'botty')
45 self.assertEqual(person.name, u'botty')
46 self.assertTrue(person.hide_email_addresses)
47 # Bots tend to flood email, so filtering is important.
48 self.assertTrue(person.expanded_notification_footers)
49
50 account = person.account
51 openid = account.openid_identifiers.one()
52 self.assertEqual(openid.identifier, u'bottyid')
53
54 sshkey_set = getUtility(ISSHKeySet)
55 self.assertIsNotNone(
56 sshkey_set.getByPersonAndKeyText(person, _opt.sshkey))
57
58 email = person.preferredemail
59 self.assertEqual(email.email, 'webops+botty@canonical.com')
60 self.assertEqual(email.status, EmailAddressStatus.PREFERRED)
61
62 self.assertTrue(person.inTeam(person_set.getByName('rosetta-admins')))
63 self.assertTrue(person.inTeam(person_set.getByName('simple-team')))
64
65 self.assertTrue(script.txn.commit.called)
066
=== added file 'scripts/create-bot-account.py'
--- scripts/create-bot-account.py 1970-01-01 00:00:00 +0000
+++ scripts/create-bot-account.py 2017-03-21 07:36:10 +0000
@@ -0,0 +1,13 @@
1#!/usr/bin/python -S
2#
3# Copyright 2017 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6import _pythonpath
7
8from lp.registry.scripts.createbotaccount import CreateBotAccountScript
9
10
11if __name__ == '__main__':
12 script = CreateBotAccountScript('create-bot-account', dbuser='launchpad')
13 script.run()