# 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