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