Merge lp:~matsubara/launchpad/bug-628510-oops-permission into lp:launchpad

Proposed by Diogo Matsubara
Status: Merged
Approved by: Gary Poster
Approved revision: no longer in the source branch.
Merged at revision: 11700
Proposed branch: lp:~matsubara/launchpad/bug-628510-oops-permission
Merge into: lp:launchpad
Diff against target: 450 lines (+106/-45)
4 files modified
lib/canonical/launchpad/webapp/errorlog.py (+6/-0)
lib/canonical/launchpad/webapp/tests/test_errorlog.py (+53/-22)
lib/lp/services/log/tests/test_uniquefileallocator.py (+27/-9)
lib/lp/services/log/uniquefileallocator.py (+20/-14)
To merge this branch: bzr merge lp:~matsubara/launchpad/bug-628510-oops-permission
Reviewer Review Type Date Requested Status
Gary Poster (community) Approve
Review via email: mp+37991@code.launchpad.net

Commit message

Fixes bug 628510 by overriding the default permission value for the oops file and oops dir when those are created.

Description of the change

This branch fixes bug 628510 by overriding the default permission value for the oops file and oops dir when those are created.

To test:

bin/test -t test_uniquefileallocator
bin/test -t test_errorlog

Caveats:

- Documentation says to use bit OR (|) operator. I got the same result by summing the values for the stat.* constants. Same thing when I subtracted the file and dir constant (stat.S_IFREG, stat.S_IFDIR respectively) from the returned st_mode.
- I couldn't reproduce the problem described in the bug report locally, so when I wrote the test, it still passed because the default permission is exactly the same as the one I'm overriding in the fix. One thing I did to check if the overriding code worked was to make it set a different permission than the default and running the test. Tests broke because the overriding code did set the permission to a different value than the test was expecting.

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

Thank you, Diogo.

- The bit OR and AND operators are the proper ways of doing this, rather than addition and subtraction. Since the values you are manipulating are single bits, sure, addition and subtraction would work, but why not do things properly? Practically, I don't know if using addition and subtraction would ever actually bite you--that is, if these values are ever anything other than single bits--but following the docs seems like an obvious win.

- You should write a test that fails initially. Use os.umask as test set up to make the permissions different. Reinstate the umask in the test's tear down.

Revision history for this message
Diogo Matsubara (matsubara) wrote :

Hi Gary,

thanks for the review. I addressed your review comments. As we spoke on IRC, the os.umask was spot on, since it actually caught the fact that the os.makedirs() mode settings was being ignored. I added those and changed the permission setting to use bit-wise operators rather than addition and subtraction.

Let me know if this is ready to land. I'll send to ec2 meanwhile.

Revision history for this message
Gary Poster (gary) wrote :

merge-conditional

Great, thank you Diogo.

1) Please run make lint and make sure that you respond to its concerns (though note that it has one error I'm aware of: tuples with a single item such as (1,) should be rendered without a trailing space after the comma, so "(1,)" is preferred over "(1, )").

2) You have indentation that's a bit non-standard in uniquefileallocator.py and test_uniquefileallocator.py. Here's an example.

        wanted_permission = (stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
            stat.S_IROTH | stat.S_IXOTH)

I'd prefer to see that as follows:

        wanted_permission = (stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
                             stat.S_IROTH | stat.S_IXOTH)

This is another acceptable alternate:

        wanted_permission = (
            stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH |
            stat.S_IXOTH)

That's it. Thank you!

Gary

review: Approve
Revision history for this message
Robert Collins (lifeless) wrote :

A small note - cleanUp is better than tearDown.

Revision history for this message
Diogo Matsubara (matsubara) wrote :

On Fri, Oct 8, 2010 at 7:07 PM, Gary Poster <email address hidden> wrote:
> Review: Approve
> merge-conditional
>
> Great, thank you Diogo.
>
> 1) Please run make lint and make sure that you respond to its concerns (though note that it has one error I'm aware of: tuples with a single item such as (1,) should be rendered without a trailing space after the comma, so "(1,)" is preferred over "(1, )").

Fixed make lint warnings.

>
> 2) You have indentation that's a bit non-standard in uniquefileallocator.py and test_uniquefileallocator.py.  Here's an example.
>
>        wanted_permission = (stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
>            stat.S_IROTH | stat.S_IXOTH)
>
> I'd prefer to see that as follows:
>
>        wanted_permission = (stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
>                             stat.S_IROTH | stat.S_IXOTH)
>
> This is another acceptable alternate:
>
>        wanted_permission = (
>            stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH |
>            stat.S_IXOTH)

Fixed the identation.

>
> That's it.  Thank you!
>

Thanks!
--
Diogo M. Matsubara

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/webapp/errorlog.py'
--- lib/canonical/launchpad/webapp/errorlog.py 2010-09-22 09:57:56 +0000
+++ lib/canonical/launchpad/webapp/errorlog.py 2010-10-13 18:46:21 +0000
@@ -11,6 +11,8 @@
11import datetime11import datetime
12from itertools import repeat12from itertools import repeat
13import logging13import logging
14import os
15import stat
14import re16import re
15import rfc82217import rfc822
16import types18import types
@@ -352,6 +354,10 @@
352 return354 return
353 filename = entry._filename355 filename = entry._filename
354 entry.write(open(filename, 'wb'))356 entry.write(open(filename, 'wb'))
357 # Set file permission to: rw-r--r--
358 wanted_permission = (
359 stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
360 os.chmod(filename, wanted_permission)
355 if request:361 if request:
356 request.oopsid = entry.id362 request.oopsid = entry.id
357 request.oops = entry363 request.oops = entry
358364
=== modified file 'lib/canonical/launchpad/webapp/tests/test_errorlog.py'
--- lib/canonical/launchpad/webapp/tests/test_errorlog.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/webapp/tests/test_errorlog.py 2010-10-13 18:46:21 +0000
@@ -10,6 +10,7 @@
10import logging10import logging
11import os11import os
12import shutil12import shutil
13import stat
13import StringIO14import StringIO
14import sys15import sys
15import tempfile16import tempfile
@@ -104,7 +105,7 @@
104 ('HTTP_REFERER', 'http://localhost:9000/'),105 ('HTTP_REFERER', 'http://localhost:9000/'),
105 ('name=foo', 'hello\nworld')],106 ('name=foo', 'hello\nworld')],
106 [(1, 5, 'store_a', 'SELECT 1'),107 [(1, 5, 'store_a', 'SELECT 1'),
107 (5, 10,'store_b', 'SELECT\n2')], False)108 (5, 10, 'store_b', 'SELECT\n2')], False)
108 fp = StringIO.StringIO()109 fp = StringIO.StringIO()
109 entry.write(fp)110 entry.write(fp)
110 self.assertEqual(fp.getvalue(), dedent("""\111 self.assertEqual(fp.getvalue(), dedent("""\
@@ -173,7 +174,6 @@
173 entry.db_statements[1],174 entry.db_statements[1],
174 (5, 10, 'store_b', 'SELECT 2'))175 (5, 10, 'store_b', 'SELECT 2'))
175176
176
177 def test_read_no_store_id(self):177 def test_read_no_store_id(self):
178 """Test ErrorReport.read() for old logs with no store_id."""178 """Test ErrorReport.read() for old logs with no store_id."""
179 fp = StringIO.StringIO(dedent("""\179 fp = StringIO.StringIO(dedent("""\
@@ -216,6 +216,7 @@
216216
217217
218class TestErrorReportingUtility(testtools.TestCase):218class TestErrorReportingUtility(testtools.TestCase):
219
219 def setUp(self):220 def setUp(self):
220 super(TestErrorReportingUtility, self).setUp()221 super(TestErrorReportingUtility, self).setUp()
221 # ErrorReportingUtility reads the global config to get the222 # ErrorReportingUtility reads the global config to get the
@@ -283,13 +284,29 @@
283 utility = ErrorReportingUtility()284 utility = ErrorReportingUtility()
284 now = datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=UTC)285 now = datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=UTC)
285286
287 # Set up default file creation mode to rwx------.
288 umask_permission = stat.S_IRWXG | stat.S_IRWXO
289 old_umask = os.umask(umask_permission)
290
286 try:291 try:
287 raise ArbitraryException('xyz')292 raise ArbitraryException('xyz')
288 except ArbitraryException:293 except ArbitraryException:
289 utility.raising(sys.exc_info(), now=now)294 utility.raising(sys.exc_info(), now=now)
290295
291 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')296 errorfile = os.path.join(
297 utility.log_namer.output_dir(now), '01800.T1')
292 self.assertTrue(os.path.exists(errorfile))298 self.assertTrue(os.path.exists(errorfile))
299
300 # Check errorfile is set with the correct permission: rw-r--r--
301 st = os.stat(errorfile)
302 wanted_permission = (
303 stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
304 # Get only the permission bits for this file.
305 file_permission = stat.S_IMODE(st.st_mode)
306 self.assertEqual(file_permission, wanted_permission)
307 # Restore the umask to the original value.
308 ignored = os.umask(old_umask)
309
293 lines = open(errorfile, 'r').readlines()310 lines = open(errorfile, 'r').readlines()
294311
295 # the header312 # the header
@@ -333,8 +350,7 @@
333 'name1': 'value3 \xa7',350 'name1': 'value3 \xa7',
334 'name2': 'value2',351 'name2': 'value2',
335 u'\N{BLACK SQUARE}': u'value4',352 u'\N{BLACK SQUARE}': u'value4',
336 }353 })
337 )
338 request.setInWSGIEnvironment('launchpad.pageid', 'IFoo:+foo-template')354 request.setInWSGIEnvironment('launchpad.pageid', 'IFoo:+foo-template')
339355
340 try:356 try:
@@ -342,7 +358,8 @@
342 except ArbitraryException:358 except ArbitraryException:
343 utility.raising(sys.exc_info(), request, now=now)359 utility.raising(sys.exc_info(), request, now=now)
344360
345 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')361 errorfile = os.path.join(
362 utility.log_namer.output_dir(now), '01800.T1')
346 self.assertTrue(os.path.exists(errorfile))363 self.assertTrue(os.path.exists(errorfile))
347 lines = open(errorfile, 'r').readlines()364 lines = open(errorfile, 'r').readlines()
348365
@@ -404,7 +421,8 @@
404 raise ArbitraryException('xyz\nabc')421 raise ArbitraryException('xyz\nabc')
405 except ArbitraryException:422 except ArbitraryException:
406 utility.raising(sys.exc_info(), request, now=now)423 utility.raising(sys.exc_info(), request, now=now)
407 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')424 errorfile = os.path.join(
425 utility.log_namer.output_dir(now), '01800.T1')
408 self.assertTrue(os.path.exists(errorfile))426 self.assertTrue(os.path.exists(errorfile))
409 lines = open(errorfile, 'r').readlines()427 lines = open(errorfile, 'r').readlines()
410 self.assertEqual(lines[16], 'xmlrpc args=(1, 2)\n')428 self.assertEqual(lines[16], 'xmlrpc args=(1, 2)\n')
@@ -459,7 +477,8 @@
459 ('name1', 'value3')], URL='https://launchpad.net/example')477 ('name1', 'value3')], URL='https://launchpad.net/example')
460 utility.raising(sys.exc_info(), request, now=now)478 utility.raising(sys.exc_info(), request, now=now)
461479
462 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')480 errorfile = os.path.join(
481 utility.log_namer.output_dir(now), '01800.T1')
463 self.assertTrue(os.path.exists(errorfile))482 self.assertTrue(os.path.exists(errorfile))
464 lines = open(errorfile, 'r').readlines()483 lines = open(errorfile, 'r').readlines()
465484
@@ -501,6 +520,7 @@
501 now = datetime.datetime(2006, 01, 01, 00, 30, 00, tzinfo=UTC)520 now = datetime.datetime(2006, 01, 01, 00, 30, 00, tzinfo=UTC)
502521
503 class UnprintableException(Exception):522 class UnprintableException(Exception):
523
504 def __str__(self):524 def __str__(self):
505 raise RuntimeError('arrgh')525 raise RuntimeError('arrgh')
506 __repr__ = __str__526 __repr__ = __str__
@@ -512,7 +532,8 @@
512 utility.raising(sys.exc_info(), now=now)532 utility.raising(sys.exc_info(), now=now)
513 log.uninstall()533 log.uninstall()
514534
515 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')535 errorfile = os.path.join(
536 utility.log_namer.output_dir(now), '01800.T1')
516 self.assertTrue(os.path.exists(errorfile))537 self.assertTrue(os.path.exists(errorfile))
517 lines = open(errorfile, 'r').readlines()538 lines = open(errorfile, 'r').readlines()
518539
@@ -555,7 +576,8 @@
555 raise Unauthorized('xyz')576 raise Unauthorized('xyz')
556 except Unauthorized:577 except Unauthorized:
557 utility.raising(sys.exc_info(), now=now)578 utility.raising(sys.exc_info(), now=now)
558 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')579 errorfile = os.path.join(
580 utility.log_namer.output_dir(now), '01800.T1')
559 self.failUnless(os.path.exists(errorfile))581 self.failUnless(os.path.exists(errorfile))
560582
561 def test_raising_unauthorized_without_principal(self):583 def test_raising_unauthorized_without_principal(self):
@@ -568,7 +590,8 @@
568 raise Unauthorized('xyz')590 raise Unauthorized('xyz')
569 except Unauthorized:591 except Unauthorized:
570 utility.raising(sys.exc_info(), request, now=now)592 utility.raising(sys.exc_info(), request, now=now)
571 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')593 errorfile = os.path.join(
594 utility.log_namer.output_dir(now), '01800.T1')
572 self.failUnless(os.path.exists(errorfile))595 self.failUnless(os.path.exists(errorfile))
573596
574 def test_raising_unauthorized_with_unauthenticated_principal(self):597 def test_raising_unauthorized_with_unauthenticated_principal(self):
@@ -581,7 +604,8 @@
581 raise Unauthorized('xyz')604 raise Unauthorized('xyz')
582 except Unauthorized:605 except Unauthorized:
583 utility.raising(sys.exc_info(), request, now=now)606 utility.raising(sys.exc_info(), request, now=now)
584 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')607 errorfile = os.path.join(
608 utility.log_namer.output_dir(now), '01800.T1')
585 self.failIf(os.path.exists(errorfile))609 self.failIf(os.path.exists(errorfile))
586610
587 def test_raising_unauthorized_with_authenticated_principal(self):611 def test_raising_unauthorized_with_authenticated_principal(self):
@@ -594,7 +618,8 @@
594 raise Unauthorized('xyz')618 raise Unauthorized('xyz')
595 except Unauthorized:619 except Unauthorized:
596 utility.raising(sys.exc_info(), request, now=now)620 utility.raising(sys.exc_info(), request, now=now)
597 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')621 errorfile = os.path.join(
622 utility.log_namer.output_dir(now), '01800.T1')
598 self.failUnless(os.path.exists(errorfile))623 self.failUnless(os.path.exists(errorfile))
599624
600 def test_raising_translation_unavailable(self):625 def test_raising_translation_unavailable(self):
@@ -612,7 +637,8 @@
612 except TranslationUnavailable:637 except TranslationUnavailable:
613 utility.raising(sys.exc_info(), now=now)638 utility.raising(sys.exc_info(), now=now)
614639
615 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')640 errorfile = os.path.join(
641 utility.log_namer.output_dir(now), '01800.T1')
616 self.assertFalse(os.path.exists(errorfile))642 self.assertFalse(os.path.exists(errorfile))
617643
618 def test_raising_no_referrer_error(self):644 def test_raising_no_referrer_error(self):
@@ -630,7 +656,8 @@
630 except NoReferrerError:656 except NoReferrerError:
631 utility.raising(sys.exc_info(), now=now)657 utility.raising(sys.exc_info(), now=now)
632658
633 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')659 errorfile = os.path.join(
660 utility.log_namer.output_dir(now), '01800.T1')
634 self.assertFalse(os.path.exists(errorfile))661 self.assertFalse(os.path.exists(errorfile))
635662
636 def test_raising_with_string_as_traceback(self):663 def test_raising_with_string_as_traceback(self):
@@ -650,7 +677,8 @@
650 exc_tb = traceback.format_exc()677 exc_tb = traceback.format_exc()
651678
652 utility.raising((exc_type, exc_value, exc_tb), now=now)679 utility.raising((exc_type, exc_value, exc_tb), now=now)
653 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')680 errorfile = os.path.join(
681 utility.log_namer.output_dir(now), '01800.T1')
654682
655 self.assertTrue(os.path.exists(errorfile))683 self.assertTrue(os.path.exists(errorfile))
656 lines = open(errorfile, 'r').readlines()684 lines = open(errorfile, 'r').readlines()
@@ -688,7 +716,8 @@
688 except ArbitraryException:716 except ArbitraryException:
689 utility.handling(sys.exc_info(), now=now)717 utility.handling(sys.exc_info(), now=now)
690718
691 errorfile = os.path.join(utility.log_namer.output_dir(now), '01800.T1')719 errorfile = os.path.join(
720 utility.log_namer.output_dir(now), '01800.T1')
692 self.assertTrue(os.path.exists(errorfile))721 self.assertTrue(os.path.exists(errorfile))
693 lines = open(errorfile, 'r').readlines()722 lines = open(errorfile, 'r').readlines()
694723
@@ -721,18 +750,18 @@
721 def test_oopsMessage(self):750 def test_oopsMessage(self):
722 """oopsMessage pushes and pops the messages."""751 """oopsMessage pushes and pops the messages."""
723 utility = ErrorReportingUtility()752 utility = ErrorReportingUtility()
724 with utility.oopsMessage({'a':'b', 'c':'d'}):753 with utility.oopsMessage({'a': 'b', 'c': 'd'}):
725 self.assertEqual(754 self.assertEqual(
726 {0: {'a':'b', 'c':'d'}}, utility._oops_messages)755 {0: {'a': 'b', 'c': 'd'}}, utility._oops_messages)
727 # An additional message doesn't supplant the original message.756 # An additional message doesn't supplant the original message.
728 with utility.oopsMessage(dict(e='f', a='z', c='d')):757 with utility.oopsMessage(dict(e='f', a='z', c='d')):
729 self.assertEqual({758 self.assertEqual({
730 0: {'a':'b', 'c':'d'},759 0: {'a': 'b', 'c': 'd'},
731 1: {'a': 'z', 'e': 'f', 'c': 'd'},760 1: {'a': 'z', 'e': 'f', 'c': 'd'},
732 }, utility._oops_messages)761 }, utility._oops_messages)
733 # Messages are removed when out of context.762 # Messages are removed when out of context.
734 self.assertEqual(763 self.assertEqual(
735 {0: {'a':'b', 'c':'d'}},764 {0: {'a': 'b', 'c': 'd'}},
736 utility._oops_messages)765 utility._oops_messages)
737766
738 def test__makeErrorReport_includes_oops_messages(self):767 def test__makeErrorReport_includes_oops_messages(self):
@@ -786,6 +815,7 @@
786815
787816
788class TestRequestWithPrincipal(TestRequest):817class TestRequestWithPrincipal(TestRequest):
818
789 def setInWSGIEnvironment(self, key, value):819 def setInWSGIEnvironment(self, key, value):
790 self._orig_env[key] = value820 self._orig_env[key] = value
791821
@@ -828,7 +858,8 @@
828 self.error_utility.log_namer._output_root = tempfile.mkdtemp()858 self.error_utility.log_namer._output_root = tempfile.mkdtemp()
829 self.logger.addHandler(859 self.logger.addHandler(
830 OopsLoggingHandler(error_utility=self.error_utility))860 OopsLoggingHandler(error_utility=self.error_utility))
831 self.addCleanup(remove_tree, self.error_utility.log_namer._output_root)861 self.addCleanup(
862 remove_tree, self.error_utility.log_namer._output_root)
832863
833 def test_exception_records_oops(self):864 def test_exception_records_oops(self):
834 # When OopsLoggingHandler is a handler for a logger, any exceptions865 # When OopsLoggingHandler is a handler for a logger, any exceptions
835866
=== modified file 'lib/lp/services/log/tests/test_uniquefileallocator.py'
--- lib/lp/services/log/tests/test_uniquefileallocator.py 2010-07-05 19:25:14 +0000
+++ lib/lp/services/log/tests/test_uniquefileallocator.py 2010-10-13 18:46:21 +0000
@@ -9,6 +9,7 @@
9import datetime9import datetime
10import os10import os
11import shutil11import shutil
12import stat
12import tempfile13import tempfile
13import unittest14import unittest
1415
@@ -58,12 +59,12 @@
58 # when combined with configuration changes causing disk scans. That59 # when combined with configuration changes causing disk scans. That
59 # would also permit using a completely stubbed out file system,60 # would also permit using a completely stubbed out file system,
60 # reducing IO in tests that use UniqueFileAllocator (such as all the61 # reducing IO in tests that use UniqueFileAllocator (such as all the
61 # pagetests in Launchpad. At that point an interface to obtain a 62 # pagetests in Launchpad. At that point an interface to obtain a
62 # factory of UniqueFileAllocator's would be useful to parameterise the63 # factory of UniqueFileAllocator's would be useful to parameterise the
63 # entire test suite.64 # entire test suite.
64 namer = UniqueFileAllocator(self._tempdir, 'OOPS', 'T')65 namer = UniqueFileAllocator(self._tempdir, 'OOPS', 'T')
65 # first name of the day66 # first name of the day
66 self.assertUniqueFileAllocator(namer, 67 self.assertUniqueFileAllocator(namer,
67 datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=UTC),68 datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=UTC),
68 'OOPS-91T1', 1, '2006-04-01/01800.T1', '2006-04-01')69 'OOPS-91T1', 1, '2006-04-01/01800.T1', '2006-04-01')
69 # second name of the day70 # second name of the day
@@ -73,7 +74,7 @@
7374
74 # first name of the following day sets a new dir and the id starts75 # first name of the following day sets a new dir and the id starts
75 # over.76 # over.
76 self.assertUniqueFileAllocator(namer, 77 self.assertUniqueFileAllocator(namer,
77 datetime.datetime(2006, 04, 02, 00, 30, 00, tzinfo=UTC),78 datetime.datetime(2006, 04, 02, 00, 30, 00, tzinfo=UTC),
78 'OOPS-92T1', 1, '2006-04-02/01800.T1', '2006-04-02')79 'OOPS-92T1', 1, '2006-04-02/01800.T1', '2006-04-02')
7980
@@ -95,16 +96,16 @@
95 self.assertRaises(ValueError, namer.newId, now)96 self.assertRaises(ValueError, namer.newId, now)
9697
97 def test_changeErrorDir(self):98 def test_changeErrorDir(self):
98 """Test changing the log output dur."""99 """Test changing the log output dir."""
99 namer = UniqueFileAllocator(self._tempdir, 'OOPS', 'T')100 namer = UniqueFileAllocator(self._tempdir, 'OOPS', 'T')
100101
101 # First an id in the original error directory.102 # First an id in the original error directory.
102 self.assertUniqueFileAllocator(namer, 103 self.assertUniqueFileAllocator(namer,
103 datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=UTC),104 datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=UTC),
104 'OOPS-91T1', 1, '2006-04-01/01800.T1', '2006-04-01')105 'OOPS-91T1', 1, '2006-04-01/01800.T1', '2006-04-01')
105106
106 # UniqueFileAllocator uses the _output_root attribute to get the current output107 # UniqueFileAllocator uses the _output_root attribute to get the
107 # directory.108 # current output directory.
108 new_output_dir = tempfile.mkdtemp()109 new_output_dir = tempfile.mkdtemp()
109 self.addCleanup(shutil.rmtree, new_output_dir, ignore_errors=True)110 self.addCleanup(shutil.rmtree, new_output_dir, ignore_errors=True)
110 namer._output_root = new_output_dir111 namer._output_root = new_output_dir
@@ -113,8 +114,8 @@
113 now = datetime.datetime(2006, 04, 01, 12, 00, 00, tzinfo=UTC)114 now = datetime.datetime(2006, 04, 01, 12, 00, 00, tzinfo=UTC)
114 log_id, filename = namer.newId(now)115 log_id, filename = namer.newId(now)
115116
116 # Since it's a new directory, with no previous logs, the id is 1 again,117 # Since it's a new directory, with no previous logs, the id is 1
117 # rather than 2.118 # again, rather than 2.
118 self.assertEqual(log_id, 'OOPS-91T1')119 self.assertEqual(log_id, 'OOPS-91T1')
119 self.assertEqual(namer._last_serial, 1)120 self.assertEqual(namer._last_serial, 1)
120 self.assertEqual(namer._last_output_dir,121 self.assertEqual(namer._last_output_dir,
@@ -134,6 +135,23 @@
134 # The namer should figure out the right highest serial.135 # The namer should figure out the right highest serial.
135 self.assertEqual(namer._findHighestSerial(output_dir), 10)136 self.assertEqual(namer._findHighestSerial(output_dir), 10)
136137
138 def test_output_dir_permission(self):
139 # Set up default dir creation mode to rwx------.
140 umask_permission = stat.S_IRWXG | stat.S_IRWXO
141 old_umask = os.umask(umask_permission)
142 namer = UniqueFileAllocator(self._tempdir, "OOPS", "T")
143 output_dir = namer.output_dir()
144 st = os.stat(output_dir)
145 # Permission we want here is: rwxr-xr-x
146 wanted_permission = (
147 stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH |
148 stat.S_IXOTH)
149 # Get only the permission bits for this directory.
150 dir_permission = stat.S_IMODE(st.st_mode)
151 self.assertEqual(dir_permission, wanted_permission)
152 # Restore the umask to the original value.
153 ignored = os.umask(old_umask)
154
137155
138def test_suite():156def test_suite():
139 return unittest.TestLoader().loadTestsFromName(__name__)157 return unittest.TestLoader().loadTestsFromName(__name__)
140158
=== modified file 'lib/lp/services/log/uniquefileallocator.py'
--- lib/lp/services/log/uniquefileallocator.py 2010-08-20 20:31:18 +0000
+++ lib/lp/services/log/uniquefileallocator.py 2010-10-13 18:46:21 +0000
@@ -12,8 +12,8 @@
12import datetime12import datetime
13import errno13import errno
14import os.path14import os.path
15import stat
15import threading16import threading
16import time
1717
18import pytz18import pytz
1919
@@ -27,7 +27,7 @@
2727
28class UniqueFileAllocator:28class UniqueFileAllocator:
29 """Assign unique file names to logs being written from an app/script.29 """Assign unique file names to logs being written from an app/script.
30 30
31 UniqueFileAllocator causes logs written from one process to be uniquely31 UniqueFileAllocator causes logs written from one process to be uniquely
32 named. It is not safe for use in multiple processes with the same output32 named. It is not safe for use in multiple processes with the same output
33 root - each process must have a unique output root.33 root - each process must have a unique output root.
@@ -90,8 +90,8 @@
90 if the logging application is restarted.90 if the logging application is restarted.
9191
92 This method is not thread safe, and only intended to be called92 This method is not thread safe, and only intended to be called
93 from the constructor (but it is called from other places in integration93 from the constructor (but it is called from other places in
94 tests).94 integration tests).
95 """95 """
96 return self._findHighestSerialFilename(directory)[0]96 return self._findHighestSerialFilename(directory)[0]
9797
@@ -105,7 +105,8 @@
105 output_dir = self.output_dir(time)105 output_dir = self.output_dir(time)
106 second_in_day = time.hour * 3600 + time.minute * 60 + time.second106 second_in_day = time.hour * 3600 + time.minute * 60 + time.second
107 return os.path.join(107 return os.path.join(
108 output_dir, '%05d.%s%s' % (second_in_day, log_subtype, log_serial))108 output_dir, '%05d.%s%s' % (
109 second_in_day, log_subtype, log_serial))
109110
110 def get_log_infix(self):111 def get_log_infix(self):
111 """Return the current log infix to use in ids and file names."""112 """Return the current log infix to use in ids and file names."""
@@ -114,8 +115,8 @@
114 def newId(self, now=None):115 def newId(self, now=None):
115 """Returns an (id, filename) pair for use by the caller.116 """Returns an (id, filename) pair for use by the caller.
116117
117 The ID is composed of a short string to identify the Launchpad instance118 The ID is composed of a short string to identify the Launchpad
118 followed by an ID that is unique for the day.119 instance followed by an ID that is unique for the day.
119120
120 The filename is composed of the zero padded second in the day121 The filename is composed of the zero padded second in the day
121 followed by the ID. This ensures that reports are in date order when122 followed by the ID. This ensures that reports are in date order when
@@ -163,12 +164,17 @@
163 except OSError, e:164 except OSError, e:
164 if e.errno != errno.EEXIST:165 if e.errno != errno.EEXIST:
165 raise166 raise
167 # Make sure the directory permission is set to: rwxr-xr-x
168 permission = (
169 stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
170 stat.S_IROTH | stat.S_IXOTH)
171 os.chmod(result, permission)
166 # TODO: Note that only one process can do this safely: its not172 # TODO: Note that only one process can do this safely: its not
167 # cross-process safe, and also not entirely threadsafe: another173 # cross-process safe, and also not entirely threadsafe:
168 # thread that has a new log and hasn't written it could then174 # another # thread that has a new log and hasn't written it
169 # use that serial number. We should either make it really safe,175 # could then use that serial number. We should either make it
170 # or remove the contention entirely and log uniquely per thread176 # really safe, or remove the contention entirely and log
171 # of execution.177 # uniquely per thread of execution.
172 self._last_serial = self._findHighestSerial(result)178 self._last_serial = self._findHighestSerial(result)
173 finally:179 finally:
174 self._lock.release()180 self._lock.release()
@@ -178,7 +184,7 @@
178 """Append a string to the log subtype in filenames and log ids.184 """Append a string to the log subtype in filenames and log ids.
179185
180 :param token: a string to append..186 :param token: a string to append..
181 Scripts that run multiple processes can use this to create a unique187 Scripts that run multiple processes can use this to create a
182 identifier for each process.188 unique identifier for each process.
183 """189 """
184 self._log_token = token190 self._log_token = token