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)