github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/acceptancetests/repository/trusty/haproxy/hooks/charmhelpers/fetch/__init__.py (about)

     1  # Copyright 2014-2015 Canonical Limited.
     2  #
     3  # This file is part of charm-helpers.
     4  #
     5  # charm-helpers is free software: you can redistribute it and/or modify
     6  # it under the terms of the GNU Lesser General Public License version 3 as
     7  # published by the Free Software Foundation.
     8  #
     9  # charm-helpers is distributed in the hope that it will be useful,
    10  # but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  # GNU Lesser General Public License for more details.
    13  #
    14  # You should have received a copy of the GNU Lesser General Public License
    15  # along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  import importlib
    18  from tempfile import NamedTemporaryFile
    19  import time
    20  from yaml import safe_load
    21  from charmhelpers.core.host import (
    22      lsb_release
    23  )
    24  import subprocess
    25  from charmhelpers.core.hookenv import (
    26      config,
    27      log,
    28  )
    29  import os
    30  
    31  import six
    32  if six.PY3:
    33      from urllib.parse import urlparse, urlunparse
    34  else:
    35      from urlparse import urlparse, urlunparse
    36  
    37  
    38  CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
    39  deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
    40  """
    41  PROPOSED_POCKET = """# Proposed
    42  deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
    43  """
    44  CLOUD_ARCHIVE_POCKETS = {
    45      # Folsom
    46      'folsom': 'precise-updates/folsom',
    47      'precise-folsom': 'precise-updates/folsom',
    48      'precise-folsom/updates': 'precise-updates/folsom',
    49      'precise-updates/folsom': 'precise-updates/folsom',
    50      'folsom/proposed': 'precise-proposed/folsom',
    51      'precise-folsom/proposed': 'precise-proposed/folsom',
    52      'precise-proposed/folsom': 'precise-proposed/folsom',
    53      # Grizzly
    54      'grizzly': 'precise-updates/grizzly',
    55      'precise-grizzly': 'precise-updates/grizzly',
    56      'precise-grizzly/updates': 'precise-updates/grizzly',
    57      'precise-updates/grizzly': 'precise-updates/grizzly',
    58      'grizzly/proposed': 'precise-proposed/grizzly',
    59      'precise-grizzly/proposed': 'precise-proposed/grizzly',
    60      'precise-proposed/grizzly': 'precise-proposed/grizzly',
    61      # Havana
    62      'havana': 'precise-updates/havana',
    63      'precise-havana': 'precise-updates/havana',
    64      'precise-havana/updates': 'precise-updates/havana',
    65      'precise-updates/havana': 'precise-updates/havana',
    66      'havana/proposed': 'precise-proposed/havana',
    67      'precise-havana/proposed': 'precise-proposed/havana',
    68      'precise-proposed/havana': 'precise-proposed/havana',
    69      # Icehouse
    70      'icehouse': 'precise-updates/icehouse',
    71      'precise-icehouse': 'precise-updates/icehouse',
    72      'precise-icehouse/updates': 'precise-updates/icehouse',
    73      'precise-updates/icehouse': 'precise-updates/icehouse',
    74      'icehouse/proposed': 'precise-proposed/icehouse',
    75      'precise-icehouse/proposed': 'precise-proposed/icehouse',
    76      'precise-proposed/icehouse': 'precise-proposed/icehouse',
    77      # Juno
    78      'juno': 'trusty-updates/juno',
    79      'trusty-juno': 'trusty-updates/juno',
    80      'trusty-juno/updates': 'trusty-updates/juno',
    81      'trusty-updates/juno': 'trusty-updates/juno',
    82      'juno/proposed': 'trusty-proposed/juno',
    83      'trusty-juno/proposed': 'trusty-proposed/juno',
    84      'trusty-proposed/juno': 'trusty-proposed/juno',
    85      # Kilo
    86      'kilo': 'trusty-updates/kilo',
    87      'trusty-kilo': 'trusty-updates/kilo',
    88      'trusty-kilo/updates': 'trusty-updates/kilo',
    89      'trusty-updates/kilo': 'trusty-updates/kilo',
    90      'kilo/proposed': 'trusty-proposed/kilo',
    91      'trusty-kilo/proposed': 'trusty-proposed/kilo',
    92      'trusty-proposed/kilo': 'trusty-proposed/kilo',
    93      # Liberty
    94      'liberty': 'trusty-updates/liberty',
    95      'trusty-liberty': 'trusty-updates/liberty',
    96      'trusty-liberty/updates': 'trusty-updates/liberty',
    97      'trusty-updates/liberty': 'trusty-updates/liberty',
    98      'liberty/proposed': 'trusty-proposed/liberty',
    99      'trusty-liberty/proposed': 'trusty-proposed/liberty',
   100      'trusty-proposed/liberty': 'trusty-proposed/liberty',
   101  }
   102  
   103  # The order of this list is very important. Handlers should be listed in from
   104  # least- to most-specific URL matching.
   105  FETCH_HANDLERS = (
   106      'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
   107      'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
   108      'charmhelpers.fetch.giturl.GitUrlFetchHandler',
   109  )
   110  
   111  APT_NO_LOCK = 100  # The return code for "couldn't acquire lock" in APT.
   112  APT_NO_LOCK_RETRY_DELAY = 10  # Wait 10 seconds between apt lock checks.
   113  APT_NO_LOCK_RETRY_COUNT = 30  # Retry to acquire the lock X times.
   114  
   115  
   116  class SourceConfigError(Exception):
   117      pass
   118  
   119  
   120  class UnhandledSource(Exception):
   121      pass
   122  
   123  
   124  class AptLockError(Exception):
   125      pass
   126  
   127  
   128  class BaseFetchHandler(object):
   129  
   130      """Base class for FetchHandler implementations in fetch plugins"""
   131  
   132      def can_handle(self, source):
   133          """Returns True if the source can be handled. Otherwise returns
   134          a string explaining why it cannot"""
   135          return "Wrong source type"
   136  
   137      def install(self, source):
   138          """Try to download and unpack the source. Return the path to the
   139          unpacked files or raise UnhandledSource."""
   140          raise UnhandledSource("Wrong source type {}".format(source))
   141  
   142      def parse_url(self, url):
   143          return urlparse(url)
   144  
   145      def base_url(self, url):
   146          """Return url without querystring or fragment"""
   147          parts = list(self.parse_url(url))
   148          parts[4:] = ['' for i in parts[4:]]
   149          return urlunparse(parts)
   150  
   151  
   152  def filter_installed_packages(packages):
   153      """Returns a list of packages that require installation"""
   154      cache = apt_cache()
   155      _pkgs = []
   156      for package in packages:
   157          try:
   158              p = cache[package]
   159              p.current_ver or _pkgs.append(package)
   160          except KeyError:
   161              log('Package {} has no installation candidate.'.format(package),
   162                  level='WARNING')
   163              _pkgs.append(package)
   164      return _pkgs
   165  
   166  
   167  def apt_cache(in_memory=True):
   168      """Build and return an apt cache"""
   169      from apt import apt_pkg
   170      apt_pkg.init()
   171      if in_memory:
   172          apt_pkg.config.set("Dir::Cache::pkgcache", "")
   173          apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
   174      return apt_pkg.Cache()
   175  
   176  
   177  def apt_install(packages, options=None, fatal=False):
   178      """Install one or more packages"""
   179      if options is None:
   180          options = ['--option=Dpkg::Options::=--force-confold']
   181  
   182      cmd = ['apt-get', '--assume-yes']
   183      cmd.extend(options)
   184      cmd.append('install')
   185      if isinstance(packages, six.string_types):
   186          cmd.append(packages)
   187      else:
   188          cmd.extend(packages)
   189      log("Installing {} with options: {}".format(packages,
   190                                                  options))
   191      _run_apt_command(cmd, fatal)
   192  
   193  
   194  def apt_upgrade(options=None, fatal=False, dist=False):
   195      """Upgrade all packages"""
   196      if options is None:
   197          options = ['--option=Dpkg::Options::=--force-confold']
   198  
   199      cmd = ['apt-get', '--assume-yes']
   200      cmd.extend(options)
   201      if dist:
   202          cmd.append('dist-upgrade')
   203      else:
   204          cmd.append('upgrade')
   205      log("Upgrading with options: {}".format(options))
   206      _run_apt_command(cmd, fatal)
   207  
   208  
   209  def apt_update(fatal=False):
   210      """Update local apt cache"""
   211      cmd = ['apt-get', 'update']
   212      _run_apt_command(cmd, fatal)
   213  
   214  
   215  def apt_purge(packages, fatal=False):
   216      """Purge one or more packages"""
   217      cmd = ['apt-get', '--assume-yes', 'purge']
   218      if isinstance(packages, six.string_types):
   219          cmd.append(packages)
   220      else:
   221          cmd.extend(packages)
   222      log("Purging {}".format(packages))
   223      _run_apt_command(cmd, fatal)
   224  
   225  
   226  def apt_hold(packages, fatal=False):
   227      """Hold one or more packages"""
   228      cmd = ['apt-mark', 'hold']
   229      if isinstance(packages, six.string_types):
   230          cmd.append(packages)
   231      else:
   232          cmd.extend(packages)
   233      log("Holding {}".format(packages))
   234  
   235      if fatal:
   236          subprocess.check_call(cmd)
   237      else:
   238          subprocess.call(cmd)
   239  
   240  
   241  def add_source(source, key=None):
   242      """Add a package source to this system.
   243  
   244      @param source: a URL or sources.list entry, as supported by
   245      add-apt-repository(1). Examples::
   246  
   247          ppa:charmers/example
   248          deb https://stub:key@private.example.com/ubuntu trusty main
   249  
   250      In addition:
   251          'proposed:' may be used to enable the standard 'proposed'
   252          pocket for the release.
   253          'cloud:' may be used to activate official cloud archive pockets,
   254          such as 'cloud:icehouse'
   255          'distro' may be used as a noop
   256  
   257      @param key: A key to be added to the system's APT keyring and used
   258      to verify the signatures on packages. Ideally, this should be an
   259      ASCII format GPG public key including the block headers. A GPG key
   260      id may also be used, but be aware that only insecure protocols are
   261      available to retrieve the actual public key from a public keyserver
   262      placing your Juju environment at risk. ppa and cloud archive keys
   263      are securely added automtically, so sould not be provided.
   264      """
   265      if source is None:
   266          log('Source is not present. Skipping')
   267          return
   268  
   269      if (source.startswith('ppa:') or
   270          source.startswith('http') or
   271          source.startswith('deb ') or
   272              source.startswith('cloud-archive:')):
   273          subprocess.check_call(['add-apt-repository', '--yes', source])
   274      elif source.startswith('cloud:'):
   275          apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
   276                      fatal=True)
   277          pocket = source.split(':')[-1]
   278          if pocket not in CLOUD_ARCHIVE_POCKETS:
   279              raise SourceConfigError(
   280                  'Unsupported cloud: source option %s' %
   281                  pocket)
   282          actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
   283          with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
   284              apt.write(CLOUD_ARCHIVE.format(actual_pocket))
   285      elif source == 'proposed':
   286          release = lsb_release()['DISTRIB_CODENAME']
   287          with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
   288              apt.write(PROPOSED_POCKET.format(release))
   289      elif source == 'distro':
   290          pass
   291      else:
   292          log("Unknown source: {!r}".format(source))
   293  
   294      if key:
   295          if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
   296              with NamedTemporaryFile('w+') as key_file:
   297                  key_file.write(key)
   298                  key_file.flush()
   299                  key_file.seek(0)
   300                  subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
   301          else:
   302              # Note that hkp: is in no way a secure protocol. Using a
   303              # GPG key id is pointless from a security POV unless you
   304              # absolutely trust your network and DNS.
   305              subprocess.check_call(['apt-key', 'adv', '--keyserver',
   306                                     'hkp://keyserver.ubuntu.com:80', '--recv',
   307                                     key])
   308  
   309  
   310  def configure_sources(update=False,
   311                        sources_var='install_sources',
   312                        keys_var='install_keys'):
   313      """
   314      Configure multiple sources from charm configuration.
   315  
   316      The lists are encoded as yaml fragments in the configuration.
   317      The frament needs to be included as a string. Sources and their
   318      corresponding keys are of the types supported by add_source().
   319  
   320      Example config:
   321          install_sources: |
   322            - "ppa:foo"
   323            - "http://example.com/repo precise main"
   324          install_keys: |
   325            - null
   326            - "a1b2c3d4"
   327  
   328      Note that 'null' (a.k.a. None) should not be quoted.
   329      """
   330      sources = safe_load((config(sources_var) or '').strip()) or []
   331      keys = safe_load((config(keys_var) or '').strip()) or None
   332  
   333      if isinstance(sources, six.string_types):
   334          sources = [sources]
   335  
   336      if keys is None:
   337          for source in sources:
   338              add_source(source, None)
   339      else:
   340          if isinstance(keys, six.string_types):
   341              keys = [keys]
   342  
   343          if len(sources) != len(keys):
   344              raise SourceConfigError(
   345                  'Install sources and keys lists are different lengths')
   346          for source, key in zip(sources, keys):
   347              add_source(source, key)
   348      if update:
   349          apt_update(fatal=True)
   350  
   351  
   352  def install_remote(source, *args, **kwargs):
   353      """
   354      Install a file tree from a remote source
   355  
   356      The specified source should be a url of the form:
   357          scheme://[host]/path[#[option=value][&...]]
   358  
   359      Schemes supported are based on this modules submodules.
   360      Options supported are submodule-specific.
   361      Additional arguments are passed through to the submodule.
   362  
   363      For example::
   364  
   365          dest = install_remote('http://example.com/archive.tgz',
   366                                checksum='deadbeef',
   367                                hash_type='sha1')
   368  
   369      This will download `archive.tgz`, validate it using SHA1 and, if
   370      the file is ok, extract it and return the directory in which it
   371      was extracted.  If the checksum fails, it will raise
   372      :class:`charmhelpers.core.host.ChecksumError`.
   373      """
   374      # We ONLY check for True here because can_handle may return a string
   375      # explaining why it can't handle a given source.
   376      handlers = [h for h in plugins() if h.can_handle(source) is True]
   377      installed_to = None
   378      for handler in handlers:
   379          try:
   380              installed_to = handler.install(source, *args, **kwargs)
   381          except UnhandledSource:
   382              pass
   383      if not installed_to:
   384          raise UnhandledSource("No handler found for source {}".format(source))
   385      return installed_to
   386  
   387  
   388  def install_from_config(config_var_name):
   389      charm_config = config()
   390      source = charm_config[config_var_name]
   391      return install_remote(source)
   392  
   393  
   394  def plugins(fetch_handlers=None):
   395      if not fetch_handlers:
   396          fetch_handlers = FETCH_HANDLERS
   397      plugin_list = []
   398      for handler_name in fetch_handlers:
   399          package, classname = handler_name.rsplit('.', 1)
   400          try:
   401              handler_class = getattr(
   402                  importlib.import_module(package),
   403                  classname)
   404              plugin_list.append(handler_class())
   405          except (ImportError, AttributeError):
   406              # Skip missing plugins so that they can be ommitted from
   407              # installation if desired
   408              log("FetchHandler {} not found, skipping plugin".format(
   409                  handler_name))
   410      return plugin_list
   411  
   412  
   413  def _run_apt_command(cmd, fatal=False):
   414      """
   415      Run an APT command, checking output and retrying if the fatal flag is set
   416      to True.
   417  
   418      :param: cmd: str: The apt command to run.
   419      :param: fatal: bool: Whether the command's output should be checked and
   420          retried.
   421      """
   422      env = os.environ.copy()
   423  
   424      if 'DEBIAN_FRONTEND' not in env:
   425          env['DEBIAN_FRONTEND'] = 'noninteractive'
   426  
   427      if fatal:
   428          retry_count = 0
   429          result = None
   430  
   431          # If the command is considered "fatal", we need to retry if the apt
   432          # lock was not acquired.
   433  
   434          while result is None or result == APT_NO_LOCK:
   435              try:
   436                  result = subprocess.check_call(cmd, env=env)
   437              except subprocess.CalledProcessError as e:
   438                  retry_count = retry_count + 1
   439                  if retry_count > APT_NO_LOCK_RETRY_COUNT:
   440                      raise
   441                  result = e.returncode
   442                  log("Couldn't acquire DPKG lock. Will retry in {} seconds."
   443                      "".format(APT_NO_LOCK_RETRY_DELAY))
   444                  time.sleep(APT_NO_LOCK_RETRY_DELAY)
   445  
   446      else:
   447          subprocess.call(cmd, env=env)