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
1=== modified file 'lib/lp/buildmaster/model/buildbase.py'
2--- lib/lp/buildmaster/model/buildbase.py 2010-03-28 09:35:12 +0000
3+++ lib/lp/buildmaster/model/buildbase.py 2010-04-14 14:24:28 +0000
4@@ -43,7 +43,7 @@
5 package.
6
7 Note: this class does not implement IBuildBase as we currently duplicate
8- the properties defined on IBuildBase on the inheriting class tables.
9+ the properties defined on IBuildBase on the inheriting class tables.
10 BuildBase cannot therefore implement IBuildBase itself, as storm requires
11 that the corresponding __storm_table__ be defined for the class. Instead,
12 the classes using the BuildBase mixin must ensure that they implement IBuildBase.
13@@ -148,7 +148,6 @@
14 directory, store build information and push them through the
15 uploader.
16 """
17- # XXX cprov 2007-07-11 bug=129487: untested code path.
18 filemap = slave_status['filemap']
19
20 logger.info("Processing successful build %s from builder %s" % (
21@@ -182,33 +181,48 @@
22 os.makedirs(upload_path)
23
24 slave = removeSecurityProxy(self.buildqueue_record.builder.slave)
25+ successful_copy_from_slave = True
26 for filename in filemap:
27 logger.info("Grabbing file: %s" % filename)
28- slave_file = slave.getFile(filemap[filename])
29 out_file_name = os.path.join(upload_path, filename)
30+ # If the evaluated output file name is not within our
31+ # upload path, then we don't try to copy this or any
32+ # subsequent files.
33+ if not os.path.realpath(out_file_name).startswith(upload_path):
34+ successful_copy_from_slave = False
35+ logger.warning(
36+ "A slave tried to upload the file '%s' "
37+ "for the build %d." % (filename, self.id))
38+ break
39 out_file = open(out_file_name, "wb")
40+ slave_file = slave.getFile(filemap[filename])
41 copy_and_close(slave_file, out_file)
42
43- uploader_logfilename = os.path.join(upload_dir, UPLOAD_LOG_FILENAME)
44- uploader_command = self.getUploaderCommand(
45- upload_leaf, uploader_logfilename)
46- logger.debug("Saving uploader log at '%s'" % uploader_logfilename)
47-
48- logger.info("Invoking uploader on %s" % root)
49- logger.info("%s" % uploader_command)
50-
51- uploader_process = subprocess.Popen(
52- uploader_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
53-
54- # Nothing should be written to the stdout/stderr.
55- upload_stdout, upload_stderr = uploader_process.communicate()
56-
57- # XXX cprov 2007-04-17: we do not check uploader_result_code
58- # anywhere. We need to find out what will be best strategy
59- # when it failed HARD (there is a huge effort in process-upload
60- # to not return error, it only happen when the code is broken).
61- uploader_result_code = uploader_process.returncode
62- logger.info("Uploader returned %d" % uploader_result_code)
63+ # We only attempt the upload if we successfully copied all the
64+ # files from the slave.
65+ if successful_copy_from_slave:
66+ uploader_logfilename = os.path.join(
67+ upload_dir, UPLOAD_LOG_FILENAME)
68+ uploader_command = self.getUploaderCommand(
69+ upload_leaf, uploader_logfilename)
70+ logger.debug("Saving uploader log at '%s'" % uploader_logfilename)
71+
72+ logger.info("Invoking uploader on %s" % root)
73+ logger.info("%s" % uploader_command)
74+
75+ uploader_process = subprocess.Popen(
76+ uploader_command, stdout=subprocess.PIPE,
77+ stderr=subprocess.PIPE)
78+
79+ # Nothing should be written to the stdout/stderr.
80+ upload_stdout, upload_stderr = uploader_process.communicate()
81+
82+ # XXX cprov 2007-04-17: we do not check uploader_result_code
83+ # anywhere. We need to find out what will be best strategy
84+ # when it failed HARD (there is a huge effort in process-upload
85+ # to not return error, it only happen when the code is broken).
86+ uploader_result_code = uploader_process.returncode
87+ logger.info("Uploader returned %d" % uploader_result_code)
88
89 # Quick and dirty hack to carry on on process-upload failures
90 if os.path.exists(upload_dir):
91@@ -264,6 +278,7 @@
92 # also contain the information required to manually reprocess the
93 # binary upload when it was the case.
94 if (self.buildstate != BuildStatus.FULLYBUILT or
95+ not successful_copy_from_slave or
96 not self.verifySuccessfulUpload()):
97 logger.warning("Build %s upload failed." % self.id)
98 self.buildstate = BuildStatus.FAILEDTOUPLOAD
99
100=== modified file 'lib/lp/buildmaster/tests/test_buildbase.py'
101--- lib/lp/buildmaster/tests/test_buildbase.py 2010-03-03 02:40:10 +0000
102+++ lib/lp/buildmaster/tests/test_buildbase.py 2010-04-14 14:24:28 +0000
103@@ -12,10 +12,16 @@
104 import unittest
105
106 from canonical.config import config
107-from canonical.testing.layers import DatabaseFunctionalLayer
108+from canonical.database.constants import UTC_NOW
109+from canonical.testing.layers import (
110+ DatabaseFunctionalLayer, LaunchpadZopelessLayer)
111+from lp.buildmaster.interfaces.buildbase import BuildStatus
112 from lp.buildmaster.model.buildbase import BuildBase
113 from lp.registry.interfaces.pocket import pocketsuffix
114+from lp.soyuz.tests.soyuzbuilddhelpers import WaitingSlave
115+from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
116 from lp.testing import TestCase, TestCaseWithFactory
117+from lp.testing.fakemethod import FakeMethod
118
119
120 class TestBuildBase(TestCase):
121@@ -48,11 +54,11 @@
122 layer = DatabaseFunctionalLayer
123
124 def test_getUploadLogContent_nolog(self):
125- """If there is no log file there, a string explaining that is returned.
126+ """If there is no log file there, a string explanation is returned.
127 """
128 self.useTempDir()
129 build_base = BuildBase()
130- self.assertEquals('Could not find upload log file',
131+ self.assertEquals('Could not find upload log file',
132 build_base.getUploadLogContent(os.getcwd(), "myleaf"))
133
134 def test_getUploadLogContent_only_dir(self):
135@@ -61,7 +67,7 @@
136 self.useTempDir()
137 os.makedirs("accepted/myleaf")
138 build_base = BuildBase()
139- self.assertEquals('Could not find upload log file',
140+ self.assertEquals('Could not find upload log file',
141 build_base.getUploadLogContent(os.getcwd(), "myleaf"))
142
143 def test_getUploadLogContent_readsfile(self):
144@@ -99,5 +105,69 @@
145 self.assertEqual(config_args, uploader_command)
146
147
148+class TestBuildBaseHandleStatus(TestCaseWithFactory):
149+ """Tests for `IBuildBase`s handleStatus method."""
150+
151+ layer = LaunchpadZopelessLayer
152+
153+ def setUp(self):
154+ super(TestBuildBaseHandleStatus, self).setUp()
155+ test_publisher = SoyuzTestPublisher()
156+ test_publisher.prepareBreezyAutotest()
157+ binaries = test_publisher.getPubBinaries()
158+ self.build = binaries[0].binarypackagerelease.build
159+
160+ # For the moment, we require a builder for the build so that
161+ # handleStatus_OK can get a reference to the slave.
162+ builder = self.factory.makeBuilder()
163+ self.build.buildqueue_record.builder = builder
164+ self.build.buildqueue_record.setDateStarted(UTC_NOW)
165+ self.slave = WaitingSlave('BuildStatus.OK')
166+ self.slave.valid_file_hashes.append('test_file_hash')
167+ builder.setSlaveForTesting(self.slave)
168+
169+ # We overwrite the buildmaster root to use a temp directory.
170+ tmp_dir = self.makeTemporaryDirectory()
171+ tmp_builddmaster_root = """
172+ [builddmaster]
173+ root: %s
174+ """ % tmp_dir
175+ config.push('tmp_builddmaster_root', tmp_builddmaster_root)
176+
177+ # We stub out our builds getUploaderCommand() method so
178+ # we can check whether it was called.
179+ self.build.getUploaderCommand = FakeMethod(
180+ result=['echo', 'noop'])
181+
182+ def test_handleStatus_OK_normal_file(self):
183+ # A filemap with plain filenames should not cause a problem.
184+ # The call to handleStatus will attempt to get the file from
185+ # the slave resulting in a URL error in this test case.
186+ self.build.handleStatus('OK', None, {
187+ 'filemap': { 'myfile.py': 'test_file_hash'},
188+ })
189+
190+ self.assertEqual(BuildStatus.FULLYBUILT, self.build.buildstate)
191+ self.assertEqual(1, self.build.getUploaderCommand.call_count)
192+
193+ def test_handleStatus_OK_absolute_filepath(self):
194+ # A filemap that tries to write to files outside of
195+ # the upload directory will result in a failed upload.
196+ self.build.handleStatus('OK', None, {
197+ 'filemap': { '/tmp/myfile.py': 'test_file_hash'},
198+ })
199+ self.assertEqual(BuildStatus.FAILEDTOUPLOAD, self.build.buildstate)
200+ self.assertEqual(0, self.build.getUploaderCommand.call_count)
201+
202+ def test_handleStatus_OK_relative_filepath(self):
203+ # A filemap that tries to write to files outside of
204+ # the upload directory will result in a failed upload.
205+ self.build.handleStatus('OK', None, {
206+ 'filemap': { '../myfile.py': 'test_file_hash'},
207+ })
208+ self.assertEqual(BuildStatus.FAILEDTOUPLOAD, self.build.buildstate)
209+ self.assertEqual(0, self.build.getUploaderCommand.call_count)
210+
211+
212 def test_suite():
213 return unittest.TestLoader().loadTestsFromName(__name__)
214
215=== modified file 'lib/lp/soyuz/tests/soyuzbuilddhelpers.py'
216--- lib/lp/soyuz/tests/soyuzbuilddhelpers.py 2010-04-12 05:52:01 +0000
217+++ lib/lp/soyuz/tests/soyuzbuilddhelpers.py 2010-04-14 14:24:28 +0000
218@@ -173,14 +173,19 @@
219 self.dependencies = dependencies
220 self.build_id = build_id
221
222+ # By default, the slave only has a buildlog, but callsites
223+ # can update this list as needed.
224+ self.valid_file_hashes = ['buildlog']
225+
226 def status(self):
227 return ('BuilderStatus.WAITING', self.state, self.build_id, {},
228 self.dependencies)
229
230- def getFile(self, sum):
231- if sum == "buildlog":
232- s = StringIO("This is a build log")
233- s.headers = {'content-length':19}
234+ def getFile(self, hash):
235+ if hash in self.valid_file_hashes:
236+ content = "This is a %s" % hash
237+ s = StringIO(content)
238+ s.headers = {'content-length':len(content)}
239 return s
240
241