Merge lp:~michael.nelson/launchpad/550049-overwriting-buildd-manager-files-2 into lp:launchpad

Proposed by Michael Nelson
Status: Merged
Approved by: Michael Nelson
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~michael.nelson/launchpad/550049-overwriting-buildd-manager-files-2
Merge into: lp:launchpad
Diff against target: 239 lines (+121/-31)
3 files modified
lib/lp/buildmaster/model/buildbase.py (+38/-23)
lib/lp/buildmaster/tests/test_buildbase.py (+74/-4)
lib/lp/soyuz/tests/soyuzbuilddhelpers.py (+9/-4)
To merge this branch: bzr merge lp:~michael.nelson/launchpad/550049-overwriting-buildd-manager-files-2
Reviewer Review Type Date Requested Status
Michael Nelson (community) code Approve
Julian Edwards (community) Approve
Review via email: mp+23398@code.launchpad.net

Commit message

Adds basic test for buildStatus_OK, ensuring that paths outside the upload dir are not written.

Description of the change

This branch is just a merge of a fix that is already on production.

That (private) MP can be seen at:
https://code.edge.launchpad.net/~michael.nelson/launchpad/pd-550049-overwriting-buildd-manager-files/+merge/22932

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

rs=me, this is already landed in production

review: Approve
Revision history for this message
Michael Nelson (michael.nelson) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/buildmaster/model/buildbase.py'
--- lib/lp/buildmaster/model/buildbase.py 2010-03-28 09:35:12 +0000
+++ lib/lp/buildmaster/model/buildbase.py 2010-04-14 14:24:28 +0000
@@ -43,7 +43,7 @@
43 package.43 package.
4444
45 Note: this class does not implement IBuildBase as we currently duplicate45 Note: this class does not implement IBuildBase as we currently duplicate
46 the properties defined on IBuildBase on the inheriting class tables. 46 the properties defined on IBuildBase on the inheriting class tables.
47 BuildBase cannot therefore implement IBuildBase itself, as storm requires47 BuildBase cannot therefore implement IBuildBase itself, as storm requires
48 that the corresponding __storm_table__ be defined for the class. Instead,48 that the corresponding __storm_table__ be defined for the class. Instead,
49 the classes using the BuildBase mixin must ensure that they implement IBuildBase.49 the classes using the BuildBase mixin must ensure that they implement IBuildBase.
@@ -148,7 +148,6 @@
148 directory, store build information and push them through the148 directory, store build information and push them through the
149 uploader.149 uploader.
150 """150 """
151 # XXX cprov 2007-07-11 bug=129487: untested code path.
152 filemap = slave_status['filemap']151 filemap = slave_status['filemap']
153152
154 logger.info("Processing successful build %s from builder %s" % (153 logger.info("Processing successful build %s from builder %s" % (
@@ -182,33 +181,48 @@
182 os.makedirs(upload_path)181 os.makedirs(upload_path)
183182
184 slave = removeSecurityProxy(self.buildqueue_record.builder.slave)183 slave = removeSecurityProxy(self.buildqueue_record.builder.slave)
184 successful_copy_from_slave = True
185 for filename in filemap:185 for filename in filemap:
186 logger.info("Grabbing file: %s" % filename)186 logger.info("Grabbing file: %s" % filename)
187 slave_file = slave.getFile(filemap[filename])
188 out_file_name = os.path.join(upload_path, filename)187 out_file_name = os.path.join(upload_path, filename)
188 # If the evaluated output file name is not within our
189 # upload path, then we don't try to copy this or any
190 # subsequent files.
191 if not os.path.realpath(out_file_name).startswith(upload_path):
192 successful_copy_from_slave = False
193 logger.warning(
194 "A slave tried to upload the file '%s' "
195 "for the build %d." % (filename, self.id))
196 break
189 out_file = open(out_file_name, "wb")197 out_file = open(out_file_name, "wb")
198 slave_file = slave.getFile(filemap[filename])
190 copy_and_close(slave_file, out_file)199 copy_and_close(slave_file, out_file)
191200
192 uploader_logfilename = os.path.join(upload_dir, UPLOAD_LOG_FILENAME)201 # We only attempt the upload if we successfully copied all the
193 uploader_command = self.getUploaderCommand(202 # files from the slave.
194 upload_leaf, uploader_logfilename)203 if successful_copy_from_slave:
195 logger.debug("Saving uploader log at '%s'" % uploader_logfilename)204 uploader_logfilename = os.path.join(
196205 upload_dir, UPLOAD_LOG_FILENAME)
197 logger.info("Invoking uploader on %s" % root)206 uploader_command = self.getUploaderCommand(
198 logger.info("%s" % uploader_command)207 upload_leaf, uploader_logfilename)
199208 logger.debug("Saving uploader log at '%s'" % uploader_logfilename)
200 uploader_process = subprocess.Popen(209
201 uploader_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)210 logger.info("Invoking uploader on %s" % root)
202211 logger.info("%s" % uploader_command)
203 # Nothing should be written to the stdout/stderr.212
204 upload_stdout, upload_stderr = uploader_process.communicate()213 uploader_process = subprocess.Popen(
205214 uploader_command, stdout=subprocess.PIPE,
206 # XXX cprov 2007-04-17: we do not check uploader_result_code215 stderr=subprocess.PIPE)
207 # anywhere. We need to find out what will be best strategy216
208 # when it failed HARD (there is a huge effort in process-upload217 # Nothing should be written to the stdout/stderr.
209 # to not return error, it only happen when the code is broken).218 upload_stdout, upload_stderr = uploader_process.communicate()
210 uploader_result_code = uploader_process.returncode219
211 logger.info("Uploader returned %d" % uploader_result_code)220 # XXX cprov 2007-04-17: we do not check uploader_result_code
221 # anywhere. We need to find out what will be best strategy
222 # when it failed HARD (there is a huge effort in process-upload
223 # to not return error, it only happen when the code is broken).
224 uploader_result_code = uploader_process.returncode
225 logger.info("Uploader returned %d" % uploader_result_code)
212226
213 # Quick and dirty hack to carry on on process-upload failures227 # Quick and dirty hack to carry on on process-upload failures
214 if os.path.exists(upload_dir):228 if os.path.exists(upload_dir):
@@ -264,6 +278,7 @@
264 # also contain the information required to manually reprocess the278 # also contain the information required to manually reprocess the
265 # binary upload when it was the case.279 # binary upload when it was the case.
266 if (self.buildstate != BuildStatus.FULLYBUILT or280 if (self.buildstate != BuildStatus.FULLYBUILT or
281 not successful_copy_from_slave or
267 not self.verifySuccessfulUpload()):282 not self.verifySuccessfulUpload()):
268 logger.warning("Build %s upload failed." % self.id)283 logger.warning("Build %s upload failed." % self.id)
269 self.buildstate = BuildStatus.FAILEDTOUPLOAD284 self.buildstate = BuildStatus.FAILEDTOUPLOAD
270285
=== modified file 'lib/lp/buildmaster/tests/test_buildbase.py'
--- lib/lp/buildmaster/tests/test_buildbase.py 2010-03-03 02:40:10 +0000
+++ lib/lp/buildmaster/tests/test_buildbase.py 2010-04-14 14:24:28 +0000
@@ -12,10 +12,16 @@
12import unittest12import unittest
1313
14from canonical.config import config14from canonical.config import config
15from canonical.testing.layers import DatabaseFunctionalLayer15from canonical.database.constants import UTC_NOW
16from canonical.testing.layers import (
17 DatabaseFunctionalLayer, LaunchpadZopelessLayer)
18from lp.buildmaster.interfaces.buildbase import BuildStatus
16from lp.buildmaster.model.buildbase import BuildBase19from lp.buildmaster.model.buildbase import BuildBase
17from lp.registry.interfaces.pocket import pocketsuffix20from lp.registry.interfaces.pocket import pocketsuffix
21from lp.soyuz.tests.soyuzbuilddhelpers import WaitingSlave
22from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
18from lp.testing import TestCase, TestCaseWithFactory23from lp.testing import TestCase, TestCaseWithFactory
24from lp.testing.fakemethod import FakeMethod
1925
2026
21class TestBuildBase(TestCase):27class TestBuildBase(TestCase):
@@ -48,11 +54,11 @@
48 layer = DatabaseFunctionalLayer54 layer = DatabaseFunctionalLayer
4955
50 def test_getUploadLogContent_nolog(self):56 def test_getUploadLogContent_nolog(self):
51 """If there is no log file there, a string explaining that is returned.57 """If there is no log file there, a string explanation is returned.
52 """58 """
53 self.useTempDir()59 self.useTempDir()
54 build_base = BuildBase()60 build_base = BuildBase()
55 self.assertEquals('Could not find upload log file', 61 self.assertEquals('Could not find upload log file',
56 build_base.getUploadLogContent(os.getcwd(), "myleaf"))62 build_base.getUploadLogContent(os.getcwd(), "myleaf"))
5763
58 def test_getUploadLogContent_only_dir(self):64 def test_getUploadLogContent_only_dir(self):
@@ -61,7 +67,7 @@
61 self.useTempDir()67 self.useTempDir()
62 os.makedirs("accepted/myleaf")68 os.makedirs("accepted/myleaf")
63 build_base = BuildBase()69 build_base = BuildBase()
64 self.assertEquals('Could not find upload log file', 70 self.assertEquals('Could not find upload log file',
65 build_base.getUploadLogContent(os.getcwd(), "myleaf"))71 build_base.getUploadLogContent(os.getcwd(), "myleaf"))
6672
67 def test_getUploadLogContent_readsfile(self):73 def test_getUploadLogContent_readsfile(self):
@@ -99,5 +105,69 @@
99 self.assertEqual(config_args, uploader_command)105 self.assertEqual(config_args, uploader_command)
100106
101107
108class TestBuildBaseHandleStatus(TestCaseWithFactory):
109 """Tests for `IBuildBase`s handleStatus method."""
110
111 layer = LaunchpadZopelessLayer
112
113 def setUp(self):
114 super(TestBuildBaseHandleStatus, self).setUp()
115 test_publisher = SoyuzTestPublisher()
116 test_publisher.prepareBreezyAutotest()
117 binaries = test_publisher.getPubBinaries()
118 self.build = binaries[0].binarypackagerelease.build
119
120 # For the moment, we require a builder for the build so that
121 # handleStatus_OK can get a reference to the slave.
122 builder = self.factory.makeBuilder()
123 self.build.buildqueue_record.builder = builder
124 self.build.buildqueue_record.setDateStarted(UTC_NOW)
125 self.slave = WaitingSlave('BuildStatus.OK')
126 self.slave.valid_file_hashes.append('test_file_hash')
127 builder.setSlaveForTesting(self.slave)
128
129 # We overwrite the buildmaster root to use a temp directory.
130 tmp_dir = self.makeTemporaryDirectory()
131 tmp_builddmaster_root = """
132 [builddmaster]
133 root: %s
134 """ % tmp_dir
135 config.push('tmp_builddmaster_root', tmp_builddmaster_root)
136
137 # We stub out our builds getUploaderCommand() method so
138 # we can check whether it was called.
139 self.build.getUploaderCommand = FakeMethod(
140 result=['echo', 'noop'])
141
142 def test_handleStatus_OK_normal_file(self):
143 # A filemap with plain filenames should not cause a problem.
144 # The call to handleStatus will attempt to get the file from
145 # the slave resulting in a URL error in this test case.
146 self.build.handleStatus('OK', None, {
147 'filemap': { 'myfile.py': 'test_file_hash'},
148 })
149
150 self.assertEqual(BuildStatus.FULLYBUILT, self.build.buildstate)
151 self.assertEqual(1, self.build.getUploaderCommand.call_count)
152
153 def test_handleStatus_OK_absolute_filepath(self):
154 # A filemap that tries to write to files outside of
155 # the upload directory will result in a failed upload.
156 self.build.handleStatus('OK', None, {
157 'filemap': { '/tmp/myfile.py': 'test_file_hash'},
158 })
159 self.assertEqual(BuildStatus.FAILEDTOUPLOAD, self.build.buildstate)
160 self.assertEqual(0, self.build.getUploaderCommand.call_count)
161
162 def test_handleStatus_OK_relative_filepath(self):
163 # A filemap that tries to write to files outside of
164 # the upload directory will result in a failed upload.
165 self.build.handleStatus('OK', None, {
166 'filemap': { '../myfile.py': 'test_file_hash'},
167 })
168 self.assertEqual(BuildStatus.FAILEDTOUPLOAD, self.build.buildstate)
169 self.assertEqual(0, self.build.getUploaderCommand.call_count)
170
171
102def test_suite():172def test_suite():
103 return unittest.TestLoader().loadTestsFromName(__name__)173 return unittest.TestLoader().loadTestsFromName(__name__)
104174
=== modified file 'lib/lp/soyuz/tests/soyuzbuilddhelpers.py'
--- lib/lp/soyuz/tests/soyuzbuilddhelpers.py 2010-04-12 05:52:01 +0000
+++ lib/lp/soyuz/tests/soyuzbuilddhelpers.py 2010-04-14 14:24:28 +0000
@@ -173,14 +173,19 @@
173 self.dependencies = dependencies173 self.dependencies = dependencies
174 self.build_id = build_id174 self.build_id = build_id
175175
176 # By default, the slave only has a buildlog, but callsites
177 # can update this list as needed.
178 self.valid_file_hashes = ['buildlog']
179
176 def status(self):180 def status(self):
177 return ('BuilderStatus.WAITING', self.state, self.build_id, {},181 return ('BuilderStatus.WAITING', self.state, self.build_id, {},
178 self.dependencies)182 self.dependencies)
179183
180 def getFile(self, sum):184 def getFile(self, hash):
181 if sum == "buildlog":185 if hash in self.valid_file_hashes:
182 s = StringIO("This is a build log")186 content = "This is a %s" % hash
183 s.headers = {'content-length':19}187 s = StringIO(content)
188 s.headers = {'content-length':len(content)}
184 return s189 return s
185190
186191