Merge lp:~stevenk/launchpad/auditor-for-packageupload into lp:launchpad

Proposed by Steve Kowalik on 2012-07-23
Status: Merged
Approved by: Steve Kowalik on 2012-07-26
Approved revision: no longer in the source branch.
Merged at revision: 15746
Proposed branch: lp:~stevenk/launchpad/auditor-for-packageupload
Merge into: lp:launchpad
Diff against target: 335 lines (+137/-49)
11 files modified
lib/lp/services/auditor/client.py (+43/-0)
lib/lp/services/auditor/tests/test_client.py (+27/-0)
lib/lp/services/config/schema-lazr.conf (+5/-0)
lib/lp/services/enterpriseid.py (+7/-6)
lib/lp/services/features/flags.py (+6/-0)
lib/lp/soyuz/browser/queue.py (+1/-1)
lib/lp/soyuz/interfaces/queue.py (+2/-1)
lib/lp/soyuz/model/queue.py (+6/-1)
lib/lp/testing/layers.py (+36/-38)
setup.py (+1/-0)
versions.cfg (+3/-2)
To merge this branch: bzr merge lp:~stevenk/launchpad/auditor-for-packageupload
Reviewer Review Type Date Requested Status
j.c.sackett (community) 2012-07-23 Approve on 2012-07-26
Review via email: mp+116180@code.launchpad.net

Commit Message

Add a feature flag 'auditor.enabled', write an AuditorClient class that will talk to the configured auditor instance and have PackageUpload call into it on accept.

Description of the Change

Write an AuditorClient class, which matches up to the AuditorServer test fixture. It will connect to the configured auditor instance and send or receive data. I have also added a feature flag 'auditor.enabled' which model/browser code can use to check if they should call into the auditor service.

To post a comment you must log in.
Robert Collins (lifeless) wrote :

Surely auditorclient should be in its own package, not part of LP ?
Other things than LP will want to use Auditor.

j.c.sackett (jcsackett) wrote :

> Surely auditorclient should be in its own package, not part of LP ?
> Other things than LP will want to use Auditor.

It is: see https://launchpad.net/auditorclient. The subclassing of it here seems sensible since there's no need for the enterpriseid stuff to be migrated into the auditorclient package.

I assume the import changes in enterpiseid.py are b/c of a circular import issue. May be worth it to add a comment indicating such.

Otherwise, this looks good to me. Thanks, Steve.

review: Approve
Robert Collins (lifeless) wrote :

+ for entry in logs['log-entries']:
+ entry['actor'] = enterpriseid_to_object(entry['actor'])
+ entry['object'] = enterpriseid_to_object(entry['object'])

- this is late evaluation pain waiting to happen. Depending on how
many entries you get back, of course. May need to make a multi-object
version of enterpriseid_to_object.

Colin Watson (cjwatson) wrote :

Would it be worth fixing the spelling of "receive" (not "recieve") before it gets baked into too many method names and becomes hard to change?

Colin Watson (cjwatson) wrote :

Oh, and would it be easy to audit rejections at the same time? We occasionally do get questions of the form "who rejected this upload? they didn't tell me why", and we have no way to tell.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/lp/services/auditor/client.py'
2--- lib/lp/services/auditor/client.py 1970-01-01 00:00:00 +0000
3+++ lib/lp/services/auditor/client.py 2012-08-06 04:49:24 +0000
4@@ -0,0 +1,43 @@
5+# Copyright 2012 Canonical Ltd. This software is licensed under the
6+# GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+"""Client that will send and receive audit logs to an auditor instance."""
9+
10+__metaclass__ = type
11+__all__ = [
12+ 'AuditorClient',
13+ ]
14+
15+from auditorclient.client import Client
16+
17+from lp.services.config import config
18+from lp.services.enterpriseid import (
19+ enterpriseid_to_object,
20+ object_to_enterpriseid,
21+ )
22+
23+
24+class AuditorClient(Client):
25+
26+ def __init__(self):
27+ super(AuditorClient, self).__init__(
28+ config.auditor.host, config.auditor.port)
29+
30+ def send(self, obj, operation, actorobj, comment=None, details=None):
31+ return super(AuditorClient, self).send(
32+ object_to_enterpriseid(obj), operation,
33+ object_to_enterpriseid(actorobj), comment, details)
34+
35+
36+ def receive(self, obj=None, operation=None, actorobj=None, limit=None):
37+ if obj:
38+ obj = object_to_enterpriseid(obj)
39+ if actorobj:
40+ actorobj = object_to_enterpriseid(actorobj)
41+ logs = super(AuditorClient, self).receive(
42+ obj, operation, actorobj, limit)
43+ # Process the actors and objects back from enterprise ids.
44+ for entry in logs['log-entries']:
45+ entry['actor'] = enterpriseid_to_object(entry['actor'])
46+ entry['object'] = enterpriseid_to_object(entry['object'])
47+ return logs['log-entries']
48
49=== added file 'lib/lp/services/auditor/tests/test_client.py'
50--- lib/lp/services/auditor/tests/test_client.py 1970-01-01 00:00:00 +0000
51+++ lib/lp/services/auditor/tests/test_client.py 2012-08-06 04:49:24 +0000
52@@ -0,0 +1,27 @@
53+# Copyright 2012 Canonical Ltd. This software is licensed under the
54+# GNU Affero General Public License version 3 (see the file LICENSE).
55+
56+__metaclass__ = type
57+
58+from lp.services.auditor.client import AuditorClient
59+from lp.testing import TestCaseWithFactory
60+from lp.testing.layers import AuditorLayer
61+
62+
63+class TestAuditorClient(TestCaseWithFactory):
64+
65+ layer = AuditorLayer
66+
67+ def test_send_and_receive(self):
68+ # We can use .send() and .receive() on AuditorClient to log.
69+ actor = self.factory.makePerson()
70+ pu = self.factory.makePackageUpload()
71+ client = AuditorClient()
72+ result = client.send(pu, 'packageupload-accepted', actor)
73+ self.assertEqual('Operation recorded.', result)
74+ result = client.receive(obj=pu)
75+ del result[0]['date'] # Ignore the date.
76+ expected = [{
77+ u'comment': u'', u'details': u'', u'actor': actor,
78+ u'operation': u'packageupload-accepted', u'object': pu}]
79+ self.assertContentEqual(expected, result)
80
81=== modified file 'lib/lp/services/config/schema-lazr.conf'
82--- lib/lp/services/config/schema-lazr.conf 2012-07-26 14:31:45 +0000
83+++ lib/lp/services/config/schema-lazr.conf 2012-08-06 04:49:24 +0000
84@@ -33,6 +33,11 @@
85 run_parts_location: none
86
87
88+[auditor]
89+host: localhost
90+port: none
91+
92+
93 [binaryfile_expire]
94 dbuser: binaryfile-expire
95
96
97=== modified file 'lib/lp/services/enterpriseid.py'
98--- lib/lp/services/enterpriseid.py 2012-03-22 23:21:24 +0000
99+++ lib/lp/services/enterpriseid.py 2012-08-06 04:49:24 +0000
100@@ -11,15 +11,9 @@
101
102 import os
103
104-from lp.registry.model.person import Person
105 from lp.services.config import config
106
107
108-known_types = {
109- 'Person': Person,
110- }
111-
112-
113 def object_to_enterpriseid(obj):
114 """Given an object, convert it to SOA Enterprise ID."""
115 otype = obj.__class__.__name__
116@@ -33,8 +27,15 @@
117
118 def enterpriseid_to_object(eid):
119 """Given an SOA Enterprise ID, return the object that it references."""
120+ # Circular imports.
121+ from lp.registry.model.person import Person
122+ from lp.soyuz.model.queue import PackageUpload
123 scheme = eid.split(':')
124 if not scheme[0].startswith('lp'):
125 raise TypeError
126+ known_types = {
127+ 'PackageUpload': PackageUpload,
128+ 'Person': Person,
129+ }
130 klass = known_types[scheme[1]]
131 return klass.get(scheme[2])
132
133=== modified file 'lib/lp/services/features/flags.py'
134--- lib/lp/services/features/flags.py 2012-07-31 00:08:37 +0000
135+++ lib/lp/services/features/flags.py 2012-08-06 04:49:24 +0000
136@@ -294,6 +294,12 @@
137 '',
138 '',
139 ''),
140+ ('auditor.enabled',
141+ 'boolean',
142+ 'If true, send audit data to an auditor instance.',
143+ '',
144+ '',
145+ ''),
146 ])
147
148 # The set of all flag names that are documented.
149
150=== modified file 'lib/lp/soyuz/browser/queue.py'
151--- lib/lp/soyuz/browser/queue.py 2012-07-09 12:32:23 +0000
152+++ lib/lp/soyuz/browser/queue.py 2012-08-06 04:49:24 +0000
153@@ -443,7 +443,7 @@
154
155 def queue_action_accept(self, queue_item):
156 """Reject the queue item passed."""
157- queue_item.acceptFromQueue()
158+ queue_item.acceptFromQueue(user=self.user)
159
160 def queue_action_reject(self, queue_item):
161 """Accept the queue item passed."""
162
163=== modified file 'lib/lp/soyuz/interfaces/queue.py'
164--- lib/lp/soyuz/interfaces/queue.py 2012-07-09 12:32:23 +0000
165+++ lib/lp/soyuz/interfaces/queue.py 2012-08-06 04:49:24 +0000
166@@ -358,8 +358,9 @@
167 """
168
169 @export_write_operation()
170+ @call_with(user=REQUEST_USER)
171 @operation_for_version("devel")
172- def acceptFromQueue(logger=None, dry_run=False):
173+ def acceptFromQueue(logger=None, dry_run=False, user=None):
174 """Call setAccepted, do a syncUpdate, and send notification email.
175
176 * Grant karma to people involved with the upload.
177
178=== modified file 'lib/lp/soyuz/model/queue.py'
179--- lib/lp/soyuz/model/queue.py 2012-07-24 16:12:10 +0000
180+++ lib/lp/soyuz/model/queue.py 2012-08-06 04:49:24 +0000
181@@ -50,6 +50,7 @@
182 from lp.archiveuploader.tagfiles import parse_tagfile_content
183 from lp.registry.interfaces.pocket import PackagePublishingPocket
184 from lp.registry.model.sourcepackagename import SourcePackageName
185+from lp.services.auditor.client import AuditorClient
186 from lp.services.config import config
187 from lp.services.database.bulk import load_referencing
188 from lp.services.database.constants import UTC_NOW
189@@ -64,6 +65,7 @@
190 SQLBase,
191 sqlvalues,
192 )
193+from lp.services.features import getFeatureFlag
194 from lp.services.librarian.browser import ProxiedLibraryFileAlias
195 from lp.services.librarian.interfaces.client import DownloadFailed
196 from lp.services.librarian.model import LibraryFileAlias
197@@ -625,7 +627,7 @@
198 # Give some karma!
199 self._giveKarma()
200
201- def acceptFromQueue(self, logger=None, dry_run=False):
202+ def acceptFromQueue(self, logger=None, dry_run=False, user=None):
203 """See `IPackageUpload`."""
204 assert not self.is_delayed_copy, 'Cannot process delayed copies.'
205
206@@ -633,6 +635,9 @@
207 self._acceptNonSyncFromQueue(logger, dry_run)
208 else:
209 self._acceptSyncFromQueue()
210+ if bool(getFeatureFlag('auditor.enabled')):
211+ client = AuditorClient()
212+ client.send(self, 'packageupload-accepted', user)
213
214 def acceptFromCopy(self):
215 """See `IPackageUpload`."""
216
217=== modified file 'lib/lp/testing/layers.py'
218--- lib/lp/testing/layers.py 2012-07-05 10:38:03 +0000
219+++ lib/lp/testing/layers.py 2012-08-06 04:49:24 +0000
220@@ -707,44 +707,6 @@
221 pass
222
223
224-class AuditorLayer(BaseLayer):
225-
226- auditor = AuditorServer()
227-
228- _is_setup = False
229-
230- @classmethod
231- @profiled
232- def setUp(cls):
233- cls.auditor.setUp()
234- cls.config_fixture.add_section(
235- cls.auditor.config.service_config)
236- cls.appserver_config_fixture.add_section(
237- cls.auditor.config.service_config)
238- cls._is_setup = True
239-
240- @classmethod
241- @profiled
242- def tearDown(cls):
243- if not cls._is_setup:
244- return
245- cls.auditor.cleanUp()
246- cls._is_setup = False
247- # Can't pop the config above, so bail here and let the test runner
248- # start a sub-process.
249- raise NotImplementedError
250-
251- @classmethod
252- @profiled
253- def testSetUp(cls):
254- pass
255-
256- @classmethod
257- @profiled
258- def testTearDown(cls):
259- pass
260-
261-
262 # We store a reference to the DB-API connect method here when we
263 # put a proxy in its place.
264 _org_connect = None
265@@ -1389,6 +1351,42 @@
266 disconnect_stores()
267
268
269+class AuditorLayer(LaunchpadFunctionalLayer):
270+
271+ auditor = AuditorServer()
272+
273+ _is_setup = False
274+
275+ @classmethod
276+ @profiled
277+ def setUp(cls):
278+ cls.auditor.setUp()
279+ cls.config_fixture.add_section(cls.auditor.service_config)
280+ cls.appserver_config_fixture.add_section(cls.auditor.service_config)
281+ cls._is_setup = True
282+
283+ @classmethod
284+ @profiled
285+ def tearDown(cls):
286+ if not cls._is_setup:
287+ return
288+ cls.auditor.cleanUp()
289+ cls._is_setup = False
290+ # Can't pop the config above, so bail here and let the test runner
291+ # start a sub-process.
292+ raise NotImplementedError
293+
294+ @classmethod
295+ @profiled
296+ def testSetUp(cls):
297+ pass
298+
299+ @classmethod
300+ @profiled
301+ def testTearDown(cls):
302+ pass
303+
304+
305 class GoogleLaunchpadFunctionalLayer(LaunchpadFunctionalLayer,
306 GoogleServiceLayer):
307 """Provides Google service in addition to LaunchpadFunctionalLayer."""
308
309=== modified file 'setup.py'
310--- setup.py 2012-07-02 04:21:44 +0000
311+++ setup.py 2012-08-06 04:49:24 +0000
312@@ -27,6 +27,7 @@
313 # used in zcml.
314 install_requires=[
315 'ampoule',
316+ 'auditorclient',
317 'auditorfixture',
318 'BeautifulSoup',
319 'bzr',
320
321=== modified file 'versions.cfg'
322--- versions.cfg 2012-07-23 21:05:14 +0000
323+++ versions.cfg 2012-08-06 04:49:24 +0000
324@@ -8,8 +8,9 @@
325 amqplib = 1.0.2
326 anyjson = 0.3.1
327 argparse = 1.2.1
328-auditor = 0.0.1
329-auditorfixture = 0.0.1
330+auditor = 0.0.2
331+auditorclient = 0.0.2
332+auditorfixture = 0.0.3
333 BeautifulSoup = 3.1.0.1
334 bson = 0.3.2
335 # The source for this version of bzr is at lp:~benji/bzr/bug-998040