Merge lp:~danilo/linaro-license-protection/staging-publish into lp:~linaro-automation/linaro-license-protection/trunk
- staging-publish
- Merge into trunk
Proposed by
Данило Шеган
Status: | Merged |
---|---|
Approved by: | Stevan Radaković |
Approved revision: | 114 |
Merged at revision: | 110 |
Proposed branch: | lp:~danilo/linaro-license-protection/staging-publish |
Merge into: | lp:~linaro-automation/linaro-license-protection/trunk |
Diff against target: |
392 lines (+228/-61) 2 files modified
scripts/publish_to_snapshots.py (+90/-27) tests/test_publish_to_snapshots.py (+138/-34) |
To merge this branch: | bzr merge lp:~danilo/linaro-license-protection/staging-publish |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stevan Radaković | code | Approve | |
Review via email: mp+120861@code.launchpad.net |
Commit message
Description of the change
This adds an option to do publishing to staging as well, which includes
a trivial sanitization: the file is truncated and the base of the filename
is written as the contents instead.
Some side-refactoring, and mostly all things are tested.
To post a comment you must log in.
- 115. By Данило Шеган
-
Lint fixes.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'scripts/publish_to_snapshots.py' |
2 | --- scripts/publish_to_snapshots.py 2012-08-03 10:38:06 +0000 |
3 | +++ scripts/publish_to_snapshots.py 2012-08-22 19:44:18 +0000 |
4 | @@ -1,26 +1,12 @@ |
5 | #!/usr/bin/env python |
6 | # Move artifacts from a temporary location to a permanent location on s.l.o |
7 | |
8 | +import argparse |
9 | +import fnmatch |
10 | import os |
11 | +import os.path |
12 | +import shutil |
13 | import sys |
14 | -import shutil |
15 | -import argparse |
16 | - |
17 | -parser = argparse.ArgumentParser() |
18 | -parser.add_argument("-t", "--job-type", dest="job_type", |
19 | - help="Specify the job type (Ex: android/kernel-hwpack)") |
20 | -parser.add_argument( |
21 | - "-j", "--job-name", dest="job_name", |
22 | - help=("Specify the job name which resulted the archive to " |
23 | - "be stored. Ex: ${JOB_NAME} should be specified for " |
24 | - "android/ubuntu-{hwpacks,images,sysroots}/binaries and for" |
25 | - "kernel-hwpack ${KERNEL_JOB_NAME}")) |
26 | -parser.add_argument( |
27 | - "-n", "--build-num", dest="build_num", type=int, |
28 | - help=("Specify the job build number for android/" |
29 | - "ubuntu-{hwpacks,images,sysroots}/binaries")) |
30 | -parser.add_argument("-m", "--manifest", dest="manifest", action='store_true', |
31 | - help="Optional parameter to generate MANIFEST file") |
32 | |
33 | uploads_path = '/srv/snapshots.linaro.org/uploads/' |
34 | target_path = '/srv/snapshots.linaro.org/www/' |
35 | @@ -39,20 +25,87 @@ |
36 | ] |
37 | |
38 | |
39 | +def setup_parser(): |
40 | + """Set up the argument parser for publish_to_snapshots script.""" |
41 | + parser = argparse.ArgumentParser() |
42 | + parser.add_argument( |
43 | + "-s", "--staging", dest="staging", default=False, |
44 | + action='store_true', |
45 | + help=("Perform sanitization on the file to not expose any" |
46 | + "potentially sensitive data. Used for staging deployment.")) |
47 | + parser.add_argument( |
48 | + "-t", "--job-type", dest="job_type", |
49 | + help="Specify the job type (Ex: android/kernel-hwpack)") |
50 | + parser.add_argument( |
51 | + "-j", "--job-name", dest="job_name", |
52 | + help=("Specify the job name which resulted the archive to " |
53 | + "be stored. Ex: ${JOB_NAME} should be specified for " |
54 | + "android/ubuntu-{hwpacks,images,sysroots}/binaries and for" |
55 | + "kernel-hwpack ${KERNEL_JOB_NAME}")) |
56 | + parser.add_argument( |
57 | + "-n", "--build-num", dest="build_num", type=int, |
58 | + help=("Specify the job build number for android/" |
59 | + "ubuntu-{hwpacks,images,sysroots}/binaries")) |
60 | + parser.add_argument( |
61 | + "-m", "--manifest", dest="manifest", |
62 | + action='store_true', |
63 | + help="Optional parameter to generate MANIFEST file") |
64 | + return parser |
65 | + |
66 | + |
67 | +class PublisherArgumentException(Exception): |
68 | + """There was a problem with one of the publisher arguments.""" |
69 | + pass |
70 | + |
71 | + |
72 | class SnapshotsPublisher(object): |
73 | |
74 | + # Files that need no sanitization even when publishing to staging. |
75 | + STAGING_ACCEPTED_FILES = [ |
76 | + 'BUILD-INFO.txt', |
77 | + 'EULA.txt', |
78 | + 'OPEN-EULA.txt', |
79 | + '*.EULA.txt.*', |
80 | + ] |
81 | + |
82 | + def __init__(self, argument_parser=None): |
83 | + """Allow moving files around for publishing on snapshots.l.o.""" |
84 | + self.argument_parser = argument_parser |
85 | + |
86 | + @classmethod |
87 | + def is_accepted_for_staging(cls, filename): |
88 | + """Is filename is in a list of globs in STAGING_ACCEPTED_FILES?""" |
89 | + filename = os.path.basename(filename) |
90 | + for accepted_names in cls.STAGING_ACCEPTED_FILES: |
91 | + if fnmatch.fnmatch(filename, accepted_names): |
92 | + return True |
93 | + return False |
94 | + |
95 | + @classmethod |
96 | + def sanitize_file(cls, file_path): |
97 | + """This truncates the file and fills it with its own filename.""" |
98 | + assert not cls.is_accepted_for_staging(file_path) |
99 | + base_file_name = os.path.basename(file_path) |
100 | + protected_file = open(file_path, "w") |
101 | + # Nice property of this is that we are also saving on disk space |
102 | + # needed. |
103 | + protected_file.truncate() |
104 | + # To help distinguish files more easily when they are downloaded, |
105 | + # we write out the base file name as the contents. |
106 | + protected_file.write(base_file_name) |
107 | + protected_file.close() |
108 | + |
109 | def validate_args(self, args): |
110 | # Validate that all the required information |
111 | # is passed on the command line |
112 | if (args.job_type == None or args.job_name == None or |
113 | args.build_num == None): |
114 | - parser.error( |
115 | + raise PublisherArgumentException( |
116 | "\nYou must specify job-type, job-name and build-num") |
117 | - return FAIL |
118 | |
119 | if (args.job_type not in acceptable_job_types): |
120 | - parser.error("Invalid job type") |
121 | - return FAIL |
122 | + raise PublisherArgumentException("Invalid job type") |
123 | + return True |
124 | |
125 | def jobname_to_target_subdir(self, args, jobname): |
126 | ret_val = None |
127 | @@ -202,7 +255,7 @@ |
128 | os.chdir(orig_dir) |
129 | return FAIL |
130 | |
131 | - def move_dir_content(self, src_dir, dest_dir): |
132 | + def move_dir_content(self, src_dir, dest_dir, sanitize=False): |
133 | filelist = os.listdir(src_dir) |
134 | try: |
135 | for file in filelist: |
136 | @@ -214,6 +267,11 @@ |
137 | continue |
138 | else: |
139 | os.remove(dest) |
140 | + if sanitize and not self.is_accepted_for_staging(src): |
141 | + # Perform the sanitization before moving the file |
142 | + # into place. |
143 | + print "Sanitizing contents of '%s'." % src |
144 | + self.sanitize_file(src) |
145 | print "Moving the src '", src, "'to dest'", dest, "'" |
146 | shutil.move(src, dest) |
147 | except shutil.Error: |
148 | @@ -231,7 +289,8 @@ |
149 | if not os.path.isdir(target_dir_path): |
150 | raise OSError |
151 | |
152 | - self.move_dir_content(build_dir_path, target_dir_path) |
153 | + self.move_dir_content(build_dir_path, target_dir_path, |
154 | + sanitize=args.staging) |
155 | |
156 | if (args.job_type == "android" or |
157 | args.job_type == "ubuntu-hwpacks" or |
158 | @@ -265,9 +324,13 @@ |
159 | |
160 | |
161 | def main(): |
162 | - publisher = SnapshotsPublisher() |
163 | - args = parser.parse_args() |
164 | - publisher.validate_args(args) |
165 | + argument_parser = setup_parser() |
166 | + publisher = SnapshotsPublisher(argument_parser) |
167 | + args = argument_parser.parse_args() |
168 | + try: |
169 | + publisher.validate_args(args) |
170 | + except PublisherArgumentException as exception: |
171 | + argument_parser.error(exception.message) |
172 | try: |
173 | build_dir_path, target_dir_path = publisher.validate_paths( |
174 | args, uploads_path, target_path) |
175 | |
176 | === modified file 'tests/test_publish_to_snapshots.py' |
177 | --- tests/test_publish_to_snapshots.py 2012-08-03 10:38:06 +0000 |
178 | +++ tests/test_publish_to_snapshots.py 2012-08-22 19:44:18 +0000 |
179 | @@ -4,10 +4,13 @@ |
180 | import sys |
181 | import shutil |
182 | import tempfile |
183 | -import argparse |
184 | from StringIO import StringIO |
185 | from testtools import TestCase |
186 | -from scripts.publish_to_snapshots import SnapshotsPublisher |
187 | +from scripts.publish_to_snapshots import ( |
188 | + PublisherArgumentException, |
189 | + SnapshotsPublisher, |
190 | + setup_parser, |
191 | + ) |
192 | |
193 | |
194 | class TestSnapshotsPublisher(TestCase): |
195 | @@ -18,13 +21,8 @@ |
196 | orig_dir = os.getcwd() |
197 | |
198 | def setUp(self): |
199 | - self.parser = argparse.ArgumentParser() |
200 | - self.parser.add_argument("-t", "--job-type", dest="job_type") |
201 | - self.parser.add_argument("-j", "--job-name", dest="job_name") |
202 | - self.parser.add_argument("-n", "--build-num", dest="build_num", |
203 | - type=int) |
204 | - self.parser.add_argument("-m", "--manifest", dest="manifest", |
205 | - action='store_true') |
206 | + self.parser = setup_parser() |
207 | + |
208 | if not os.path.isdir(self.uploads_path): |
209 | os.mkdir(self.uploads_path) |
210 | |
211 | @@ -72,21 +70,18 @@ |
212 | ['-t', 'binaries', '-j', 'dummy_job_name', '-n', '1']) |
213 | self.publisher.validate_args(param) |
214 | |
215 | + # Staging parameter is accepted as well. |
216 | + param = self.parser.parse_args( |
217 | + ['--staging', '-t', 'binaries', '-j', 'dummy_job_name', '-n', '1']) |
218 | + self.publisher.validate_args(param) |
219 | + |
220 | def test_validate_args_invalid_job_type(self): |
221 | - orig_stderr = sys.stderr |
222 | - stderr = sys.stderr = StringIO() |
223 | self.publisher = SnapshotsPublisher() |
224 | param = self.parser.parse_args( |
225 | ['-t', 'invalid_job_type', '-j', 'dummy_job_name', '-n', '1']) |
226 | - try: |
227 | - self.publisher.validate_args(param) |
228 | - except SystemExit, err: |
229 | - self.assertEqual(err.code, 2, "Expected result") |
230 | - finally: |
231 | - sys.stderr = orig_stderr |
232 | - |
233 | - stderr.seek(0) |
234 | - self.assertIn("Invalid job type", stderr.read()) |
235 | + self.assertRaisesRegexp( |
236 | + PublisherArgumentException, "Invalid job type", |
237 | + self.publisher.validate_args, param) |
238 | |
239 | def test_validate_args_run_invalid_argument(self): |
240 | orig_stderr = sys.stderr |
241 | @@ -120,21 +115,13 @@ |
242 | stderr.read()) |
243 | |
244 | def test_validate_args_run_none_values(self): |
245 | - orig_stderr = sys.stderr |
246 | - stderr = sys.stderr = StringIO() |
247 | self.publisher = SnapshotsPublisher() |
248 | - try: |
249 | - param = self.parser.parse_args( |
250 | - ['-t', None, '-j', None, '-n', 0]) |
251 | - self.publisher.validate_args(param) |
252 | - except SystemExit, err: |
253 | - self.assertEqual(err.code, 2, "None values are not acceptable") |
254 | - finally: |
255 | - sys.stderr = orig_stderr |
256 | - |
257 | - stderr.seek(0) |
258 | - self.assertIn("You must specify job-type, job-name and build-num", |
259 | - stderr.read()) |
260 | + param = self.parser.parse_args( |
261 | + ['-t', None, '-j', None, '-n', 0]) |
262 | + self.assertRaisesRegexp( |
263 | + PublisherArgumentException, |
264 | + "You must specify job-type, job-name and build-num", |
265 | + self.publisher.validate_args, param) |
266 | |
267 | def test_validate_paths_invalid_uploads_path(self): |
268 | orig_stdout = sys.stdout |
269 | @@ -176,6 +163,123 @@ |
270 | stdout.seek(0) |
271 | self.assertIn("Missing target path", stdout.read()) |
272 | |
273 | + def test_is_accepted_for_staging_EULA_txt(self): |
274 | + self.assertTrue( |
275 | + SnapshotsPublisher.is_accepted_for_staging("EULA.txt")) |
276 | + self.assertTrue( |
277 | + SnapshotsPublisher.is_accepted_for_staging("/path/to/EULA.txt")) |
278 | + # Full filename should be EULA.txt and nothing should be added to it. |
279 | + self.assertFalse( |
280 | + SnapshotsPublisher.is_accepted_for_staging( |
281 | + "/path/to/EULA.txt.something")) |
282 | + |
283 | + def test_is_accepted_for_staging_OPEN_EULA_txt(self): |
284 | + self.assertTrue( |
285 | + SnapshotsPublisher.is_accepted_for_staging( |
286 | + "OPEN-EULA.txt")) |
287 | + self.assertTrue( |
288 | + SnapshotsPublisher.is_accepted_for_staging( |
289 | + "/path/to/OPEN-EULA.txt")) |
290 | + |
291 | + def test_is_accepted_for_staging_per_file_EULA(self): |
292 | + self.assertTrue( |
293 | + SnapshotsPublisher.is_accepted_for_staging( |
294 | + "something.tar.gz.EULA.txt.ste")) |
295 | + self.assertTrue( |
296 | + SnapshotsPublisher.is_accepted_for_staging( |
297 | + "/path/to/something.tar.gz.EULA.txt.ste")) |
298 | + # We must have a "theme" for per-file license files in the |
299 | + # EULA-model. |
300 | + self.assertFalse( |
301 | + SnapshotsPublisher.is_accepted_for_staging( |
302 | + "something.tar.gz.EULA.txt")) |
303 | + |
304 | + def test_is_accepted_for_staging_build_info(self): |
305 | + self.assertTrue( |
306 | + SnapshotsPublisher.is_accepted_for_staging( |
307 | + "BUILD-INFO.txt")) |
308 | + self.assertTrue( |
309 | + SnapshotsPublisher.is_accepted_for_staging( |
310 | + "/path/to/BUILD-INFO.txt")) |
311 | + |
312 | + def test_sanitize_file_assert_on_accepted_files(self): |
313 | + # Since sanitize_file explicitely sanitizes a file, |
314 | + # one needs to ensure outside the function that it's |
315 | + # not being called on one of accepted file names. |
316 | + filename = '/path/to/EULA.txt' |
317 | + self.assertTrue(SnapshotsPublisher.is_accepted_for_staging(filename)) |
318 | + self.assertRaises( |
319 | + AssertionError, SnapshotsPublisher.sanitize_file, filename) |
320 | + |
321 | + def make_temporary_file(self, data, root=None): |
322 | + """Creates a temporary file and fills it with data. |
323 | + |
324 | + Returns the full file path of the new temporary file. |
325 | + """ |
326 | + tmp_file_handle, tmp_filename = tempfile.mkstemp(dir=root) |
327 | + tmp_file = os.fdopen(tmp_file_handle, "w") |
328 | + tmp_file.write(data) |
329 | + tmp_file.close() |
330 | + return tmp_filename |
331 | + |
332 | + def test_sanitize_file_loses_original_contents(self): |
333 | + original_text = "Some garbage" * 100 |
334 | + protected_filename = self.make_temporary_file(original_text) |
335 | + |
336 | + SnapshotsPublisher.sanitize_file(protected_filename) |
337 | + new_contents = open(protected_filename).read() |
338 | + self.assertNotEqual(original_text, new_contents) |
339 | + # Clean-up. |
340 | + os.remove(protected_filename) |
341 | + |
342 | + def test_sanitize_file_basename_as_contents(self): |
343 | + # It's useful to have an easy way to distinguish files by the content |
344 | + # as well, so we put the basename (filename without a path) in. |
345 | + protected_filename = self.make_temporary_file("Some contents") |
346 | + SnapshotsPublisher.sanitize_file(protected_filename) |
347 | + new_contents = open(protected_filename).read() |
348 | + # Incidentally, the contents are actually the file base name. |
349 | + self.assertEqual(os.path.basename(protected_filename), new_contents) |
350 | + # Clean-up. |
351 | + os.remove(protected_filename) |
352 | + |
353 | + def test_move_dir_content_sanitize(self): |
354 | + # A directory containing a file to sanitize is moved with the |
355 | + # data being sanitized first. |
356 | + source_dir = tempfile.mkdtemp() |
357 | + destination_dir = tempfile.mkdtemp() |
358 | + protected_content = "Something secret" * 10 |
359 | + protected_file = self.make_temporary_file(protected_content, |
360 | + root=source_dir) |
361 | + publisher = SnapshotsPublisher() |
362 | + publisher.move_dir_content(source_dir, destination_dir, sanitize=True) |
363 | + resulting_file = os.path.join(destination_dir, |
364 | + os.path.basename(protected_file)) |
365 | + self.assertNotEqual(protected_content, |
366 | + open(resulting_file).read()) |
367 | + shutil.rmtree(source_dir) |
368 | + shutil.rmtree(destination_dir) |
369 | + |
370 | + def test_move_dir_content_no_sanitize(self): |
371 | + # A directory containing one of accepted files has it moved |
372 | + # without changes even with sanitization option on. |
373 | + source_dir = tempfile.mkdtemp() |
374 | + destination_dir = tempfile.mkdtemp() |
375 | + allowed_content = "Something public" * 10 |
376 | + allowed_file_name = os.path.join(source_dir, "EULA.txt") |
377 | + allowed_file = open(allowed_file_name, "w") |
378 | + allowed_file.write(allowed_content) |
379 | + allowed_file.close() |
380 | + |
381 | + publisher = SnapshotsPublisher() |
382 | + publisher.move_dir_content(source_dir, destination_dir, sanitize=True) |
383 | + resulting_file = os.path.join(destination_dir, |
384 | + os.path.basename(allowed_file_name)) |
385 | + self.assertEqual(allowed_content, |
386 | + open(resulting_file).read()) |
387 | + shutil.rmtree(source_dir) |
388 | + shutil.rmtree(destination_dir) |
389 | + |
390 | def test_move_artifacts_kernel_successful_move(self): |
391 | orig_stdout = sys.stdout |
392 | stdout = sys.stdout = StringIO() |
Looks good.
As I've mentioned, we should probably increase the list of accepted files but we can do it 'nother time.
Approve +1.