# Copyright 2017, 2018 Amazon.com, Inc. or its affiliates. # This module is part of Amazon Linux Extras. # # Amazon Linux Extras is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License v2 as published # by the Free Software Foundation. # # Amazon Linux Extras is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. # # You should have received a copy of the GNU General Public License # along with Amazon Linux Extras. If not, see . from __future__ import print_function, unicode_literals from .software_catalog import VERSION_KEY import re import os import sys import shutil from tempfile import NamedTemporaryFile from string import Formatter try: import ConfigParser as configparser except ImportError: import configparser import logging as loggingmod if sys.version_info.major == 2: from gettext import gettext as gettext_yields_encoded _ = lambda *args: gettext_yields_encoded(*args).decode("UTF-8") else: from gettext import gettext as _ class UrlFormatHelper(Formatter): def convert_field(self, value, conversion): """Extend conversion symbol to support uppercasing values in the URL format. """ if conversion == "u": return str(value).upper() return super(UrlFormatHelper, self).convert_field(value, conversion) CONFIG_URL_MATCHER = re.compile(r".*?/extras/(?P[^/]+)/(?P[^/]+)/\$basearch/mirror\.list$") YUMCONFIG_FILE_NAME = os.path.join(os.environ.get("TESTROOT", "/etc/yum.repos.d/"), "amzn2-extras.repo") YUM_AMZN2_PRIORITY = 10 YUMCONFIG_SECTION_EXTRAPREFIX = "amzn2extra-" URL_FMT = os.environ.get("URL_FORMAT", "$awsproto://$amazonlinux.$awsregion.$awsdomain/$releasever/extras/{extraname}/{exactver}/{sect}/mirror.list") USE_MIRRORLIST = True if os.environ.get("USE_MIRRORLIST", "yes").lower() in ("yes", "true", "1", "on") else False logger = loggingmod.getLogger(__name__) def yum_ini_unquote(quoted_string): """ >>> yum_ini_unquote("abcdef_21__40__23__24_") 'abcdef!@#$' """ def decode(match): return chr(int(match.group(1), 16)) unquoted = re.sub("_([0-9a-fA-F]{2})_", decode, quoted_string) unnamespaced_unquoted = re.sub("^(?:"+re.escape(YUMCONFIG_SECTION_EXTRAPREFIX)+")?", r"", unquoted) return unnamespaced_unquoted def yum_ini_quote(raw_string): """ >>> yum_ini_quote("test1.1+other2") 'test1.1_2b_other2' >>> yum_ini_quote("abcdef!@#$") 'abcdef_21__40__23__24_' """ def encode(match): return "_%02x_" % (ord(match.group(1)),) quoted = re.sub("([^-A-Za-z0-9.])", encode, raw_string) # important to match _ return YUMCONFIG_SECTION_EXTRAPREFIX + quoted def read_configuration(): """Read the YUM configuration file we manage.""" config = configparser.RawConfigParser() if os.path.isfile(YUMCONFIG_FILE_NAME): config.read(YUMCONFIG_FILE_NAME) state = {} for section in config.sections(): if section.endswith(("-source", "-debuginfo")): continue quoted_section_name = yum_ini_unquote(section) state[quoted_section_name] = {} for key, value in config.items(section): if ("URL_FORMAT" not in os.environ and (key == "mirrorlist" or key == "baseurl")): # For now, we assume any overide url passed in URL_FORMAT # points to latest match = CONFIG_URL_MATCHER.match(value) if not match: logger.error("Malformed url in %s section %s %r", YUMCONFIG_FILE_NAME, section, value) raise ValueError(value) else: state[quoted_section_name][VERSION_KEY] = match.group("exactver") if key in ("enabled",): if hasattr(value, "lower") and value.lower().strip() in ("true", "1", "yes"): state[quoted_section_name][key] = 1 elif hasattr(value, "lower") and value.lower().strip() in ("false", "0", "no"): state[quoted_section_name][key] = 0 elif value in (0, 1): state[quoted_section_name][key] = value else: logger.warn("Unexpected value for %s %r is %r", section, key, value) state[quoted_section_name][key] = value else: state[quoted_section_name][key] = value if VERSION_KEY not in state[quoted_section_name] and state[quoted_section_name].get("enabled"): state[quoted_section_name][VERSION_KEY] = "latest" return state def write_configuration(state): """Write the probably-mutated YUM configuration file we manage.""" src_suffix = "-source" dbg_suffix = "-debuginfo" config = configparser.RawConfigParser() config.read(YUMCONFIG_FILE_NAME) for key in state: ini_section = yum_ini_quote(key) ini_section_dbg = ini_section + dbg_suffix ini_section_src = ini_section + src_suffix if not config.has_section(ini_section_src): config.add_section(ini_section_src) if not config.has_section(ini_section_dbg): config.add_section(ini_section_dbg) if not config.has_section(ini_section): config.add_section(ini_section) # defaults config.set(ini_section, "enabled", state[key]["enabled"]) config.set(ini_section_dbg, "enabled", config.get(ini_section_dbg, "enabled") if config.has_option(ini_section_dbg, "enabled") else "0") config.set(ini_section_src, "enabled", config.get(ini_section_src, "enabled") if config.has_option(ini_section_src, "enabled") else "0") # user settings for k, v in sorted(state[key].items()): if k == VERSION_KEY: continue config.set(ini_section, k, v) # overrides of user settings config.set(ini_section, "name", "Amazon Extras repo for " + key) config.set(ini_section_src, "name", "Amazon Extras source repo for " + key) config.set(ini_section_dbg, "name", "Amazon Extras debuginfo repo for " + key) if USE_MIRRORLIST: url_key = "mirrorlist" else: url_key = "baseurl" # Get alternates suffixes for srpms and debuginfo if needed srpm_suffix = os.environ.get("SRC_SUFFIX", "SRPMS") debuginfo_suffix = os.environ.get("DEBUGINFO_SUFFIX", "debuginfo/$basearch") url_format_helper = UrlFormatHelper() config.set(ini_section, url_key, url_format_helper.format(URL_FMT, extraname=key, exactver=state[key].get(VERSION_KEY, "latest"), sect="$basearch")) config.set(ini_section_src, url_key, url_format_helper.format(URL_FMT, extraname=key, exactver=state[key].get(VERSION_KEY, "latest"), sect=srpm_suffix)) config.set(ini_section_dbg, url_key, url_format_helper.format(URL_FMT, extraname=key, exactver=state[key].get(VERSION_KEY, "latest"), sect=debuginfo_suffix)) if "URL_FORMAT" not in os.environ: assert CONFIG_URL_MATCHER.match(config.get(ini_section, url_key)) config.set(ini_section, "gpgcheck", 1) config.set(ini_section_src, "gpgcheck", 1) config.set(ini_section_dbg, "gpgcheck", 1) config.set(ini_section, "gpgkey", "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-amazon-linux-2") config.set(ini_section_src, "gpgkey", "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-amazon-linux-2") config.set(ini_section_dbg, "gpgkey", "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-amazon-linux-2") config.set(ini_section, "priority", YUM_AMZN2_PRIORITY) config.set(ini_section_src, "priority", YUM_AMZN2_PRIORITY) config.set(ini_section_dbg, "priority", YUM_AMZN2_PRIORITY) config.set(ini_section, "skip_if_unavailable", 1) config.set(ini_section_src, "skip_if_unavailable", 1) config.set(ini_section_dbg, "skip_if_unavailable", 1) config.set(ini_section, "report_instanceid", "yes") config.set(ini_section_src, "report_instanceid", "yes") config.set(ini_section_dbg, "report_instanceid", "yes") with NamedTemporaryFile(mode="w+t", delete=False) as ntf: written_file_name = ntf.name ntf.write("\n\n\n\n### This file is managed with amazon-linux-extras. Please manage with that tool.\n") ntf.write("\n"*20) # Scroll the good stuff below impulsive attentions. config.write(ntf) os.chmod(written_file_name, 0o644) # Make world-readable, owner writable try: shutil.move(written_file_name, YUMCONFIG_FILE_NAME) except IOError as exc: logger.error(_("You lack permissions to write to system configuration.") + " " + YUMCONFIG_FILE_NAME) raise