blob: a4ae07c2bb5347c9c6aea6a95302d8b1284ee3bc [file] [log] [blame]
Allen Libf0c4412017-02-03 16:49:53 -08001"""
2This module defines the PackageManager class which provides an
3implementation of the packaging system API providing methods to fetch,
4upload and remove packages.
5"""
6
7#pylint: disable=missing-docstring
8
9import fcntl
10import logging
11import os
12import re
13import shutil
14
15import common
16from autotest_lib.client.bin import os_dep
17from autotest_lib.client.common_lib import error
18from autotest_lib.client.common_lib import global_config
19from autotest_lib.client.common_lib import utils
showard9d02fb52008-08-08 18:20:37 +000020
21
Allen Libf0c4412017-02-03 16:49:53 -080022# the name of the checksum file that stores the packages' checksums
23CHECKSUM_FILE = "packages.checksum"
jadmanskic27c2312009-08-05 20:58:51 +000024
showard9d02fb52008-08-08 18:20:37 +000025
Allen Libf0c4412017-02-03 16:49:53 -080026def has_pbzip2():
27 '''Check if parallel bzip2 is available on this system.'''
28 try:
29 os_dep.command('pbzip2')
30 except ValueError:
31 return False
32 return True
33
34
35# is parallel bzip2 available for use?
36_PBZIP2_AVAILABLE = has_pbzip2()
37
38
39def parse_ssh_path(repo):
40 '''
41 Parse ssh://xx@xx/path/to/ and return a tuple with host_line and
42 remote path
43 '''
44
45 match = re.search('^ssh://(.*?)(/.*)$', repo)
46 if match:
47 return match.groups()
48 else:
49 raise error.PackageUploadError(
50 "Incorrect SSH path in global_config: %s" % repo)
51
52
53def repo_run_command(repo, cmd, ignore_status=False, cd=True):
54 """Run a command relative to the repos path"""
55 repo = repo.strip()
56 run_cmd = None
57 cd_str = ''
58 if repo.startswith('ssh://'):
59 username = None
60 hostline, remote_path = parse_ssh_path(repo)
61 if cd:
62 cd_str = 'cd %s && ' % remote_path
63 if '@' in hostline:
64 username, host = hostline.split('@')
65 run_cmd = 'ssh %s@%s "%s%s"' % (username, host, cd_str, cmd)
66 else:
67 run_cmd = 'ssh %s "%s%s"' % (host, cd_str, cmd)
68
69 else:
70 if cd:
71 cd_str = 'cd %s && ' % repo
72 run_cmd = "%s%s" % (cd_str, cmd)
73
74 if run_cmd:
75 return utils.run(run_cmd, ignore_status=ignore_status)
76
77
78def create_directory(repo):
79 remote_path = repo
80 if repo.startswith('ssh://'):
81 _, remote_path = parse_ssh_path(repo)
82 repo_run_command(repo, 'mkdir -p %s' % remote_path, cd=False)
83
84
85def check_diskspace(repo, min_free=None):
86 # Note: 1 GB = 10**9 bytes (SI unit).
87 if min_free is None:
88 min_free = global_config.global_config.get_config_value('PACKAGES',
89 'minimum_free_space',
90 type=int, default=1)
91 try:
92 df = repo_run_command(repo,
93 'df -PB %d . | tail -1' % 10 ** 9).stdout.split()
94 free_space_gb = int(df[3])
95 except Exception, e:
96 raise error.RepoUnknownError('Unknown Repo Error: %s' % e)
97 if free_space_gb < min_free:
98 raise error.RepoDiskFullError('Not enough disk space available '
99 '%sg < %sg' % (free_space_gb, min_free))
100
101
102def check_write(repo):
103 try:
104 repo_testfile = '.repo_test_file'
105 repo_run_command(repo, 'touch %s' % repo_testfile).stdout.strip()
106 repo_run_command(repo, 'rm ' + repo_testfile)
107 except error.CmdError:
108 raise error.RepoWriteError('Unable to write to ' + repo)
109
110
111def trim_custom_directories(repo, older_than_days=None):
112 if not repo:
113 return
114
115 if older_than_days is None:
116 older_than_days = global_config.global_config.get_config_value(
117 'PACKAGES', 'custom_max_age', type=int, default=40)
118 cmd = 'find . -type f -atime +%s -exec rm -f {} \;' % older_than_days
119 repo_run_command(repo, cmd, ignore_status=True)
120
121
122class RepositoryFetcher(object):
123 url = None
124
125
126 def fetch_pkg_file(self, filename, dest_path):
127 """ Fetch a package file from a package repository.
128
129 @param filename: The filename of the package file to fetch.
130 @param dest_path: Destination path to download the file to.
131
132 @raises PackageFetchError if the fetch failed
133 """
134 raise NotImplementedError()
135
136
137class HttpFetcher(RepositoryFetcher):
138 wget_cmd_pattern = 'wget --connect-timeout=15 -nv %s -O %s'
139
140
141 def __init__(self, package_manager, repository_url):
142 """
143 @param repository_url: The base URL of the http repository
144 """
145 self.run_command = package_manager._run_command
146 self.url = repository_url
147
148 def exists(self, destpath, target='file'):
149 """Check if a file or directory exists using `test`.
150
151 This is a wrapper for run_command.
152
153 Args:
154 target: Optional string that should either be 'file' or 'dir'
155 indicating what should exist.
156 """
157 if target == 'dir':
158 test_cmd = 'test -d %s'
159 else:
160 test_cmd = 'test -e %s'
161
162 try:
163 self.run_command(test_cmd % destpath)
164 return True
165 except (error.CmdError, error.AutoservRunError):
166 return False
167
168 def _quick_http_test(self):
169 """ Run a simple 30 second wget on the repository to see if it is
170 reachable. This avoids the need to wait for a full 10min timeout.
171 """
172 # just make a temp file to write a test fetch into
173 mktemp = 'mktemp -u /tmp/tmp.XXXXXX'
174 dest_file_path = self.run_command(mktemp).stdout.strip()
175
176 try:
177 # build up a wget command
178 http_cmd = self.wget_cmd_pattern % (self.url, dest_file_path)
179 try:
180 self.run_command(http_cmd, _run_command_dargs={'timeout': 30})
181 except Exception, e:
182 msg = 'HTTP test failed, unable to contact %s: %s'
183 raise error.PackageFetchError(msg % (self.url, e))
184 finally:
185 self.run_command('rm -rf %s' % dest_file_path)
186
187
188 def fetch_pkg_file(self, filename, dest_path):
189 logging.info('Fetching %s from %s to %s', filename, self.url,
190 dest_path)
191
192 # do a quick test to verify the repo is reachable
193 self._quick_http_test()
194
195 # try to retrieve the package via http
196 package_url = os.path.join(self.url, filename)
197 try:
198 cmd = self.wget_cmd_pattern % (package_url, dest_path)
199 result = self.run_command(cmd,
200 _run_command_dargs={'timeout': 1200})
201
202 if not self.exists(dest_path):
203 logging.error('wget failed: %s', result)
204 raise error.CmdError(cmd, result)
205
206 logging.info('Successfully fetched %s from %s', filename,
207 package_url)
208 except error.CmdError as e:
209 # remove whatever junk was retrieved when the get failed
210 self.run_command('rm -f %s' % dest_path)
211
212 raise error.PackageFetchError('%s not found in %s\n%s'
213 'wget error code: %d' % (filename, package_url,
214 e.result_obj.stderr, e.result_obj.exit_status))
215
216
217class LocalFilesystemFetcher(RepositoryFetcher):
218 def __init__(self, package_manager, local_dir):
219 self.run_command = package_manager._run_command
220 self.url = local_dir
221
222
223 def fetch_pkg_file(self, filename, dest_path):
224 logging.info('Fetching %s from %s to %s', filename, self.url,
225 dest_path)
226 local_path = os.path.join(self.url, filename)
227 try:
228 self.run_command('cp %s %s' % (local_path, dest_path))
229 logging.debug('Successfully fetched %s from %s', filename,
230 local_path)
231 except error.CmdError, e:
232 raise error.PackageFetchError(
233 'Package %s could not be fetched from %s'
234 % (filename, self.url), e)
235
236
237class BasePackageManager(object):
238 def __init__(self, pkgmgr_dir, hostname=None, repo_urls=None,
239 upload_paths=None, do_locking=True, run_function=utils.run,
240 run_function_args=[], run_function_dargs={}):
241 '''
242 repo_urls: The list of the repository urls which is consulted
243 whilst fetching the package
244 upload_paths: The list of the upload of repositories to which
245 the package is uploaded to
246 pkgmgr_dir : A directory that can be used by the package manager
247 to dump stuff (like checksum files of the repositories
248 etc.).
249 do_locking : Enable locking when the packages are installed.
250
251 run_function is used to execute the commands throughout this file.
252 It defaults to utils.run() but a custom method (if provided) should
253 be of the same schema as utils.run. It should return a CmdResult
254 object and throw a CmdError exception. The reason for using a separate
255 function to run the commands is that the same code can be run to fetch
256 a package on the local machine or on a remote machine (in which case
257 ssh_host's run function is passed in for run_function).
258 '''
259 # In memory dictionary that stores the checksum's of packages
260 self._checksum_dict = {}
261
262 self.pkgmgr_dir = pkgmgr_dir
263 self.do_locking = do_locking
264 self.hostname = hostname
265 self.repositories = []
266
267 # Create an internal function that is a simple wrapper of
268 # run_function and takes in the args and dargs as arguments
269 def _run_command(command, _run_command_args=run_function_args,
270 _run_command_dargs={}):
271 '''
272 Special internal function that takes in a command as
273 argument and passes it on to run_function (if specified).
274 The _run_command_dargs are merged into run_function_dargs
275 with the former having more precedence than the latter.
276 '''
277 new_dargs = dict(run_function_dargs)
278 new_dargs.update(_run_command_dargs)
279 # avoid polluting logs with extremely verbose packaging output
280 new_dargs.update({'stdout_tee' : None})
281
282 return run_function(command, *_run_command_args,
283 **new_dargs)
284
285 self._run_command = _run_command
286
287 # Process the repository URLs
288 if not repo_urls:
289 repo_urls = []
290 elif hostname:
291 repo_urls = self.get_mirror_list(repo_urls)
292 for url in repo_urls:
293 self.add_repository(url)
294
295 # Process the upload URLs
296 if not upload_paths:
297 self.upload_paths = []
298 else:
299 self.upload_paths = list(upload_paths)
300
301
302 def add_repository(self, repo):
303 if isinstance(repo, basestring):
304 self.repositories.append(self.get_fetcher(repo))
305 elif isinstance(repo, RepositoryFetcher):
306 self.repositories.append(repo)
307 else:
308 raise TypeError("repo must be RepositoryFetcher or url string")
309
310 def exists(self, destpath, target='file'):
311 """Check if a file or directory exists using `test`.
312
313 This is a wrapper for _run_command.
314
315 Args:
316 target: Optional string that should either be 'file' or 'dir'
317 indicating what should exist.
318 """
319 if target == 'dir':
320 test_cmd = 'test -d %s'
321 else:
322 test_cmd = 'test -e %s'
323
324 try:
325 self._run_command(test_cmd % destpath)
326 return True
327 except (error.CmdError, error.AutoservRunError):
328 return False
329
330 def get_fetcher(self, url):
331 if url.startswith('http://'):
332 return HttpFetcher(self, url)
333 else:
334 return LocalFilesystemFetcher(self, url)
335
336
337 def repo_check(self, repo):
338 '''
339 Check to make sure the repo is in a sane state:
340 ensure we have at least XX amount of free space
341 Make sure we can write to the repo
342 '''
343 if not repo.startswith('/') and not repo.startswith('ssh:'):
344 return
345 try:
346 create_directory(repo)
347 check_diskspace(repo)
348 check_write(repo)
349 except (error.RepoWriteError, error.RepoUnknownError,
350 error.RepoDiskFullError), e:
351 raise error.RepoError("ERROR: Repo %s: %s" % (repo, e))
352
353
354 def upkeep(self, custom_repos=None):
355 '''
356 Clean up custom upload/download areas
357 '''
358 from autotest_lib.server import subcommand
359 if not custom_repos:
360 # Not all package types necessarily require or allow custom repos
361 try:
362 custom_repos = global_config.global_config.get_config_value(
363 'PACKAGES', 'custom_upload_location').split(',')
364 except global_config.ConfigError:
365 custom_repos = []
366 try:
367 custom_download = global_config.global_config.get_config_value(
368 'PACKAGES', 'custom_download_location')
369 custom_repos += [custom_download]
370 except global_config.ConfigError:
371 pass
372
373 if not custom_repos:
374 return
375
376 subcommand.parallel_simple(trim_custom_directories, custom_repos,
377 log=False)
378
379
380 def install_pkg(self, name, pkg_type, fetch_dir, install_dir,
381 preserve_install_dir=False, repo_url=None):
382 '''
383 Remove install_dir if it already exists and then recreate it unless
384 preserve_install_dir is specified as True.
385 Fetch the package into the pkg_dir. Untar the package into install_dir
386 The assumption is that packages are of the form :
387 <pkg_type>.<pkg_name>.tar.bz2
388 name : name of the package
389 type : type of the package
390 fetch_dir : The directory into which the package tarball will be
391 fetched to.
392 install_dir : the directory where the package files will be untarred to
393 repo_url : the url of the repository to fetch the package from.
394 '''
395
396 # do_locking flag is on by default unless you disable it (typically
397 # in the cases where packages are directly installed from the server
398 # onto the client in which case fcntl stuff wont work as the code
399 # will run on the server in that case..
400 if self.do_locking:
401 lockfile_name = '.%s-%s-lock' % (name, pkg_type)
402 lockfile = open(os.path.join(self.pkgmgr_dir, lockfile_name), 'w')
403
404 try:
405 if self.do_locking:
406 fcntl.flock(lockfile, fcntl.LOCK_EX)
407
408 self._run_command('mkdir -p %s' % fetch_dir)
409
410 pkg_name = self.get_tarball_name(name, pkg_type)
411 fetch_path = os.path.join(fetch_dir, pkg_name)
412 try:
413 # Fetch the package into fetch_dir
414 self.fetch_pkg(pkg_name, fetch_path, use_checksum=True)
415
416 # check to see if the install_dir exists and if it does
417 # then check to see if the .checksum file is the latest
418 if (self.exists(install_dir, target='dir') and
419 not self.untar_required(fetch_path, install_dir)):
420 return
421
422 # untar the package into install_dir and
423 # update the checksum in that directory
424 if not preserve_install_dir:
425 # Make sure we clean up the install_dir
426 self._run_command('rm -rf %s' % install_dir)
427 self._run_command('mkdir -p %s' % install_dir)
428
429 self.untar_pkg(fetch_path, install_dir)
430
431 except error.PackageFetchError, why:
432 raise error.PackageInstallError(
433 'Installation of %s(type:%s) failed : %s'
434 % (name, pkg_type, why))
435 finally:
436 if self.do_locking:
437 fcntl.flock(lockfile, fcntl.LOCK_UN)
438 lockfile.close()
439
440
441 def fetch_pkg(self, pkg_name, dest_path, repo_url=None, use_checksum=False):
442 '''
443 Fetch the package into dest_dir from repo_url. By default repo_url
444 is None and the package is looked in all the repositories specified.
445 Otherwise it fetches it from the specific repo_url.
446 pkg_name : name of the package (ex: test-sleeptest.tar.bz2,
447 dep-gcc.tar.bz2, kernel.1-1.rpm)
448 repo_url : the URL of the repository where the package is located.
449 dest_path : complete path of where the package will be fetched to.
450 use_checksum : This is set to False to fetch the packages.checksum file
451 so that the checksum comparison is bypassed for the
452 checksum file itself. This is used internally by the
453 packaging system. It should be ignored by externals
454 callers of this method who use it fetch custom packages.
455 '''
456 # Check if the destination dir exists.
457 if not self.exists(os.path.dirname(dest_path), target='dir'):
458 raise error.PackageFetchError("Please provide a valid "
459 "destination: %s " % dest_path)
460
461 # See if the package was already fetched earlier, if so
462 # the checksums need to be compared and the package is now
463 # fetched only if they differ.
464 pkg_exists = self.exists(dest_path)
465
466 # if a repository location is explicitly provided, fetch the package
467 # from there and return
468 if repo_url:
469 repositories = [self.get_fetcher(repo_url)]
470 elif self.repositories:
471 repositories = self.repositories
472 else:
473 raise error.PackageFetchError("No repository urls specified")
474
475 # install the package from the package repos, try the repos in
476 # reverse order, assuming that the 'newest' repos are most desirable
477 for fetcher in reversed(repositories):
478 try:
479 # Fetch the package if it is not there, the checksum does
480 # not match, or checksums are disabled entirely
481 need_to_fetch = (
482 not use_checksum or not pkg_exists
483 or not self.compare_checksum(dest_path))
484 if need_to_fetch:
485 fetcher.fetch_pkg_file(pkg_name, dest_path)
486 # update checksum so we won't refetch next time.
487 if use_checksum:
488 self.update_checksum(dest_path)
489 return
490 except (error.PackageFetchError, error.AutoservRunError) as e:
491 # The package could not be found in this repo, continue looking
492 logging.debug(e)
493
494 repo_url_list = [repo.url for repo in repositories]
495 message = ('%s could not be fetched from any of the repos %s' %
496 (pkg_name, repo_url_list))
497 logging.error(message)
498 # if we got here then that means the package is not found
499 # in any of the repositories.
500 raise error.PackageFetchError(message)
501
502
503 def upload_pkg(self, pkg_path, upload_path=None, update_checksum=False,
504 timeout=300):
505 from autotest_lib.server import subcommand
506 if upload_path:
507 upload_path_list = [upload_path]
508 self.upkeep(upload_path_list)
509 elif len(self.upload_paths) > 0:
510 self.upkeep()
511 upload_path_list = self.upload_paths
512 else:
513 raise error.PackageUploadError("Invalid Upload Path specified")
514
515 if update_checksum:
516 # get the packages' checksum file and update it with the current
517 # package's checksum
518 self.update_checksum(pkg_path)
519
520 commands = []
521 for path in upload_path_list:
522 commands.append(subcommand.subcommand(self.upload_pkg_parallel,
523 (pkg_path, path,
524 update_checksum)))
525
526 results = subcommand.parallel(commands, timeout, return_results=True)
527 for result in results:
528 if result:
529 print str(result)
530
531
532 # TODO(aganti): Fix the bug with the current checksum logic where
533 # packages' checksums that are not present consistently in all the
534 # repositories are not handled properly. This is a corner case though
535 # but the ideal solution is to make the checksum file repository specific
536 # and then maintain it.
537 def upload_pkg_parallel(self, pkg_path, upload_path, update_checksum=False):
538 '''
539 Uploads to a specified upload_path or to all the repos.
540 Also uploads the checksum file to all the repos.
541 pkg_path : The complete path to the package file
542 upload_path : the absolute path where the files are copied to.
543 if set to 'None' assumes 'all' repos
544 update_checksum : If set to False, the checksum file is not
545 going to be updated which happens by default.
546 This is necessary for custom
547 packages (like custom kernels and custom tests)
548 that get uploaded which do not need to be part of
549 the checksum file and bloat it.
550 '''
551 self.repo_check(upload_path)
552 # upload the package
553 if os.path.isdir(pkg_path):
554 self.upload_pkg_dir(pkg_path, upload_path)
555 else:
556 self.upload_pkg_file(pkg_path, upload_path)
557 if update_checksum:
558 self.upload_pkg_file(self._get_checksum_file_path(),
559 upload_path)
560
561
562 def upload_pkg_file(self, file_path, upload_path):
563 '''
564 Upload a single file. Depending on the upload path, the appropriate
565 method for that protocol is called. Currently this simply copies the
566 file to the target directory (but can be extended for other protocols)
567 This assumes that the web server is running on the same machine where
568 the method is being called from. The upload_path's files are
569 basically served by that web server.
570 '''
571 try:
572 if upload_path.startswith('ssh://'):
573 # parse ssh://user@host/usr/local/autotest/packages
574 hostline, remote_path = parse_ssh_path(upload_path)
575 try:
576 utils.run('scp %s %s:%s' % (file_path, hostline,
577 remote_path))
578 r_path = os.path.join(remote_path,
579 os.path.basename(file_path))
580 utils.run("ssh %s 'chmod 644 %s'" % (hostline, r_path))
581 except error.CmdError:
582 logging.error("Error uploading to repository %s",
583 upload_path)
584 else:
585 shutil.copy(file_path, upload_path)
586 os.chmod(os.path.join(upload_path,
587 os.path.basename(file_path)), 0644)
588 except (IOError, os.error), why:
589 logging.error("Upload of %s to %s failed: %s", file_path,
590 upload_path, why)
591
592
593 def upload_pkg_dir(self, dir_path, upload_path):
594 '''
595 Upload a full directory. Depending on the upload path, the appropriate
596 method for that protocol is called. Currently this copies the whole
597 tmp package directory to the target directory.
598 This assumes that the web server is running on the same machine where
599 the method is being called from. The upload_path's files are
600 basically served by that web server.
601 '''
602 local_path = os.path.join(dir_path, "*")
603 try:
604 if upload_path.startswith('ssh://'):
605 hostline, remote_path = parse_ssh_path(upload_path)
606 try:
607 utils.run('scp %s %s:%s' % (local_path, hostline,
608 remote_path))
609 ssh_path = os.path.join(remote_path, "*")
610 utils.run("ssh %s 'chmod 644 %s'" % (hostline, ssh_path))
611 except error.CmdError:
612 logging.error("Error uploading to repository: %s",
613 upload_path)
614 else:
615 utils.run("cp %s %s " % (local_path, upload_path))
616 up_path = os.path.join(upload_path, "*")
617 utils.run("chmod 644 %s" % up_path)
618 except (IOError, os.error), why:
619 raise error.PackageUploadError("Upload of %s to %s failed: %s"
620 % (dir_path, upload_path, why))
621
622
623 def remove_pkg(self, pkg_name, remove_path=None, remove_checksum=False):
624 '''
625 Remove the package from the specified remove_path
626 pkg_name : name of the package (ex: test-sleeptest.tar.bz2,
627 dep-gcc.tar.bz2)
628 remove_path : the location to remove the package from.
629
630 '''
631 if remove_path:
632 remove_path_list = [remove_path]
633 elif len(self.upload_paths) > 0:
634 remove_path_list = self.upload_paths
635 else:
636 raise error.PackageRemoveError(
637 "Invalid path to remove the pkg from")
638
639 checksum_path = self._get_checksum_file_path()
640
641 if remove_checksum:
642 self.remove_checksum(pkg_name)
643
644 # remove the package and upload the checksum file to the repos
645 for path in remove_path_list:
646 self.remove_pkg_file(pkg_name, path)
647 self.upload_pkg_file(checksum_path, path)
648
649
650 def remove_pkg_file(self, filename, pkg_dir):
651 '''
652 Remove the file named filename from pkg_dir
653 '''
654 try:
655 # Remove the file
656 if pkg_dir.startswith('ssh://'):
657 hostline, remote_path = parse_ssh_path(pkg_dir)
658 path = os.path.join(remote_path, filename)
659 utils.run("ssh %s 'rm -rf %s/%s'" % (hostline, remote_path,
660 path))
661 else:
662 os.remove(os.path.join(pkg_dir, filename))
663 except (IOError, os.error), why:
664 raise error.PackageRemoveError("Could not remove %s from %s: %s "
665 % (filename, pkg_dir, why))
666
667
668 def get_mirror_list(self, repo_urls):
669 '''
670 Stub function for site specific mirrors.
671
672 Returns:
673 Priority ordered list
674 '''
675 return repo_urls
676
677
678 def _get_checksum_file_path(self):
679 '''
680 Return the complete path of the checksum file (assumed to be stored
681 in self.pkgmgr_dir
682 '''
683 return os.path.join(self.pkgmgr_dir, CHECKSUM_FILE)
684
685
686 def _get_checksum_dict(self):
687 '''
688 Fetch the checksum file if not already fetched. If the checksum file
689 cannot be fetched from the repos then a new file is created with
690 the current package's (specified in pkg_path) checksum value in it.
691 Populate the local checksum dictionary with the values read from
692 the checksum file.
693 The checksum file is assumed to be present in self.pkgmgr_dir
694 '''
695 checksum_path = self._get_checksum_file_path()
696 if not self._checksum_dict:
697 # Fetch the checksum file
698 try:
699 if not self.exists(checksum_path):
700 # The packages checksum file does not exist locally.
701 # See if it is present in the repositories.
702 self.fetch_pkg(CHECKSUM_FILE, checksum_path)
703 except error.PackageFetchError:
704 # This should not happen whilst fetching a package..if a
705 # package is present in the repository, the corresponding
706 # checksum file should also be automatically present. This
707 # case happens only when a package
708 # is being uploaded and if it is the first package to be
709 # uploaded to the repos (hence no checksum file created yet)
710 # Return an empty dictionary in that case
711 return {}
712
713 # Read the checksum file into memory
714 checksum_file_contents = self._run_command('cat '
715 + checksum_path).stdout
716
717 # Return {} if we have an empty checksum file present
718 if not checksum_file_contents.strip():
719 return {}
720
721 # Parse the checksum file contents into self._checksum_dict
722 for line in checksum_file_contents.splitlines():
723 checksum, package_name = line.split(None, 1)
724 self._checksum_dict[package_name] = checksum
725
726 return self._checksum_dict
727
728
729 def _save_checksum_dict(self, checksum_dict):
730 '''
731 Save the checksum dictionary onto the checksum file. Update the
732 local _checksum_dict variable with this new set of values.
733 checksum_dict : New checksum dictionary
734 checksum_dir : The directory in which to store the checksum file to.
735 '''
736 checksum_path = self._get_checksum_file_path()
737 self._checksum_dict = checksum_dict.copy()
738 checksum_contents = '\n'.join(checksum + ' ' + pkg_name
739 for pkg_name, checksum in
740 checksum_dict.iteritems())
741 # Write the checksum file back to disk
742 self._run_command('echo "%s" > %s' % (checksum_contents,
743 checksum_path),
744 _run_command_dargs={'verbose': False})
745
746
747 def compute_checksum(self, pkg_path):
748 '''
749 Compute the MD5 checksum for the package file and return it.
750 pkg_path : The complete path for the package file
751 '''
752 md5sum_output = self._run_command("md5sum %s " % pkg_path).stdout
753 return md5sum_output.split()[0]
754
755
756 def update_checksum(self, pkg_path):
757 '''
758 Update the checksum of the package in the packages' checksum
759 file. This method is called whenever a package is fetched just
760 to be sure that the checksums in the local file are the latest.
761 pkg_path : The complete path to the package file.
762 '''
763 # Compute the new checksum
764 new_checksum = self.compute_checksum(pkg_path)
765 checksum_dict = self._get_checksum_dict()
766 checksum_dict[os.path.basename(pkg_path)] = new_checksum
767 self._save_checksum_dict(checksum_dict)
768
769
770 def remove_checksum(self, pkg_name):
771 '''
772 Remove the checksum of the package from the packages checksum file.
773 This method is called whenever a package is removed from the
774 repositories in order clean its corresponding checksum.
775 pkg_name : The name of the package to be removed
776 '''
777 checksum_dict = self._get_checksum_dict()
778 if pkg_name in checksum_dict:
779 del checksum_dict[pkg_name]
780 self._save_checksum_dict(checksum_dict)
781
782
783 def compare_checksum(self, pkg_path):
784 '''
785 Calculate the checksum of the file specified in pkg_path and
786 compare it with the checksum in the checksum file
787 Return True if both match else return False.
788 pkg_path : The full path to the package file for which the
789 checksum is being compared
790 '''
791 checksum_dict = self._get_checksum_dict()
792 package_name = os.path.basename(pkg_path)
793 if not checksum_dict or package_name not in checksum_dict:
794 return False
795
796 repository_checksum = checksum_dict[package_name]
797 local_checksum = self.compute_checksum(pkg_path)
798 return (local_checksum == repository_checksum)
799
800
801 def tar_package(self, pkg_name, src_dir, dest_dir, exclude_string=None):
802 '''
803 Create a tar.bz2 file with the name 'pkg_name' say test-blah.tar.bz2.
804 Excludes the directories specified in exclude_string while tarring
805 the source. Returns the tarball path.
806 '''
807 tarball_path = os.path.join(dest_dir, pkg_name)
808 temp_path = tarball_path + '.tmp'
809 cmd_list = ['tar', '-cf', temp_path, '-C', src_dir]
810 if _PBZIP2_AVAILABLE:
811 cmd_list.append('--use-compress-prog=pbzip2')
812 else:
813 cmd_list.append('-j')
814 if exclude_string is not None:
815 cmd_list.append(exclude_string)
816
817 try:
818 utils.system(' '.join(cmd_list))
819 except:
820 os.unlink(temp_path)
821 raise
822
823 os.rename(temp_path, tarball_path)
824 return tarball_path
825
826
827 def untar_required(self, tarball_path, dest_dir):
828 '''
829 Compare the checksum of the tarball_path with the .checksum file
830 in the dest_dir and return False if it matches. The untar
831 of the package happens only if the checksums do not match.
832 '''
833 checksum_path = os.path.join(dest_dir, '.checksum')
834 try:
835 existing_checksum = self._run_command('cat ' + checksum_path).stdout
836 except (error.CmdError, error.AutoservRunError):
837 # If the .checksum file is not present (generally, this should
838 # not be the case) then return True so that the untar happens
839 return True
840
841 new_checksum = self.compute_checksum(tarball_path)
842 return (new_checksum.strip() != existing_checksum.strip())
843
844
845 def untar_pkg(self, tarball_path, dest_dir):
846 '''
847 Untar the package present in the tarball_path and put a
848 ".checksum" file in the dest_dir containing the checksum
849 of the tarball. This method
850 assumes that the package to be untarred is of the form
851 <name>.tar.bz2
852 '''
853 self._run_command('tar --no-same-owner -xjf %s -C %s' %
854 (tarball_path, dest_dir))
855 # Put the .checksum file in the install_dir to note
856 # where the package came from
857 pkg_checksum = self.compute_checksum(tarball_path)
858 pkg_checksum_path = os.path.join(dest_dir,
859 '.checksum')
860 self._run_command('echo "%s" > %s '
861 % (pkg_checksum, pkg_checksum_path))
862
863
864 @staticmethod
865 def get_tarball_name(name, pkg_type):
866 """Converts a package name and type into a tarball name.
867
868 @param name: The name of the package
869 @param pkg_type: The type of the package
870
871 @returns A tarball filename for that specific type of package
872 """
873 assert '-' not in pkg_type
874 return '%s-%s.tar.bz2' % (pkg_type, name)
875
876
877 @staticmethod
878 def parse_tarball_name(tarball_name):
879 """Coverts a package tarball name into a package name and type.
880
881 @param tarball_name: The filename of the tarball
882
883 @returns (name, pkg_type) where name is the package name and pkg_type
884 is the package type.
885 """
886 match = re.search(r'^([^-]*)-(.*)\.tar\.bz2$', tarball_name)
887 pkg_type, name = match.groups()
888 return name, pkg_type
889
890
891 def is_url(self, url):
892 """Return true if path looks like a URL"""
893 return url.startswith('http://')
894
895
896 def get_package_name(self, url, pkg_type):
897 '''
898 Extract the group and test name for the url. This method is currently
899 used only for tests.
900 '''
901 if pkg_type == 'test':
902 regex = '[^:]+://(.*)/([^/]*)$'
903 return self._get_package_name(url, regex)
904 else:
905 return ('', url)
906
907
908 def _get_package_name(self, url, regex):
909 if not self.is_url(url):
910 if url.endswith('.tar.bz2'):
911 testname = url.replace('.tar.bz2', '')
912 testname = re.sub(r'(\d*)\.', '', testname)
913 return (testname, testname)
914 else:
915 return ('', url)
916
917 match = re.match(regex, url)
918 if not match:
919 return ('', url)
920 group, filename = match.groups()
921 # Generate the group prefix.
922 group = re.sub(r'\W', '_', group)
923 # Drop the extension to get the raw test name.
924 testname = re.sub(r'\.tar\.bz2', '', filename)
925 # Drop any random numbers at the end of the test name if any
926 testname = re.sub(r'\.(\d*)', '', testname)
927 return (group, testname)
928
929
930class SiteHttpFetcher(HttpFetcher):
931 wget_cmd_pattern = ('wget --connect-timeout=15 --retry-connrefused '
932 '--wait=5 -nv %s -O %s')
933
934 # shortcut quick http test for now since our dev server does not support
935 # this operation.
936 def _quick_http_test(self):
937 return
938
939
940class PackageManager(BasePackageManager):
941 def get_fetcher(self, url):
942 if url.startswith('http://'):
943 return SiteHttpFetcher(self, url)
944 else:
945 return super(PackageManager, self).get_fetcher(url)