diff options
| author | Zuul <zuul@review.opendev.org> | 2026-07-08 16:33:45 +0000 |
|---|---|---|
| committer | Gerrit Code Review <review@openstack.org> | 2026-07-08 16:33:45 +0000 |
| commit | 44b4d81399792f539716618953f2cafa64086a5c (patch) | |
| tree | 4699d7e068e6dd842a9f1f247ca2f150d60d5e27 /kolla/docker/base | |
| parent | b3803eb9f951acd63018ab8ba69bcbd1a2de5c93 (diff) | |
| parent | db35ac8d793506bc43b62e7e39d27a75de1043ed (diff) | |
| download | kolla-44b4d81399792f539716618953f2cafa64086a5c.tar.gz kolla-44b4d81399792f539716618953f2cafa64086a5c.zip | |
Merge "Migrate to pyproject.toml"
Diffstat (limited to 'kolla/docker/base')
24 files changed, 1507 insertions, 0 deletions
diff --git a/kolla/docker/base/Dockerfile.j2 b/kolla/docker/base/Dockerfile.j2 new file mode 100644 index 000000000..0e2f3c1ab --- /dev/null +++ b/kolla/docker/base/Dockerfile.j2 @@ -0,0 +1,346 @@ +FROM {{ base_image }}:{{ base_distro_tag }} +{% block labels %} +LABEL maintainer="{{ maintainer }}" name="{{ image_name }}" +{% endblock %} + +RUN . /etc/os-release;\ + if [ "${PRETTY_NAME#{{ supported_distro_name }}}" = "$PRETTY_NAME" ]; then \ + echo "Only releases \"{{ supported_distro_name }}\" are supported on {{ base_distro }}"; false; \ + fi + +# We use curl in this dockerfile so let configure it before first use +COPY curlrc /root/.curlrc + +{% block base_lang %} +# NOTE(yoctozepto): use a UTF-8 (Unicode) locale like standard image installs do +# fixes issues arising from ascii fallback usage +ENV LANG en_US.UTF-8 +{% endblock %} + +LABEL kolla_version="{{ kolla_version }}" + +{% import "macros.j2" as macros with context %} +{% block base_header %}{% endblock %} + +ENV KOLLA_BASE_DISTRO={{ base_distro }} \ + KOLLA_BASE_ARCH={{ base_arch }} + +#### Customize PS1 to be used with bash shell +COPY kolla_bashrc /tmp/ +RUN cat /tmp/kolla_bashrc >> /etc/skel/.bashrc \ + && cat /tmp/kolla_bashrc >> /root/.bashrc + +# PS1 var when used /bin/sh shell +ENV PS1="$(tput bold)($(printenv KOLLA_SERVICE_NAME))$(tput sgr0)[$(id -un)@$(hostname -s) $(pwd)]$ " + +{% if base_package_type == 'rpm' %} +# For RPM Variants, enable the correct repositories - this should all be done +# in the base image so repos are consistent throughout the system. This also +# enables to provide repo overrides at a later date in a simple fashion if we +# desire such functionality. I think we will :) + +ENV KOLLA_RPM_OVS_VERSION=3.5 \ + KOLLA_RPM_OVN_VERSION=26.03 + +RUN cat /tmp/kolla_bashrc >> /etc/bashrc \ + && sed -i 's|^\(override_install_langs=.*\)|# \1|' /etc/dnf/dnf.conf + +{% block base_dnf_conf %} +{% block base_yum_conf %} + +COPY dnf.conf /etc/dnf/dnf.conf + +{% endblock %} +{% endblock %} + +#### BEGIN REPO ENABLEMENT +{% set base_yum_url_packages = [ +] %} + +{% set base_yum_repo_keys = [ +] %} + +{% block base_centos_repo_overrides_post_copy %}{% endblock %} + +# Install what is needed for en_US.UTF-8 +{% block base_centos_distro_sync_and_languages %} + +{% set base_centos_language_packages = [ + 'langpacks-en', + 'glibc-all-langpacks' +] %} + +# NOTE(hrw): this macro file drops all languages other than C.UTF-8 so horizon fails +# https://bugzilla.redhat.com/show_bug.cgi?id=1729770 +RUN rm -f /etc/rpm/macros.image-language-conf \ + && {{ macros.install_packages(base_centos_language_packages | customizable("centos_language_packages"), chain=True, clean=False) }} \ + && {{ macros.rpm_security_update(clean_package_cache) }} + +{% endblock %} + +{{ macros.install_packages(base_yum_url_packages | customizable("yum_url_packages")) }} + +{% for key in base_yum_repo_keys | customizable('yum_repo_keys') %} +{%- if loop.first %}RUN {% else %} && {% endif -%} + rpm --import {{ key }} +{%- if not loop.last %} \{% endif %} +{% endfor -%} + +{% for cmd in rpm_setup %} +{{ cmd }} +{% endfor %} + +{% block base_centos_repo_overrides_post_rpm %}{% endblock %} + +{% block base_centos_gpg_key_import %} +{% endblock %} + +{% set base_centos_yum_repo_keys = [ +] %} + +{% set base_centos_yum_repo_packages = [ + 'centos-release-ceph-squid', + 'centos-release-nfv-openvswitch', + 'epel-release', +] %} + +# We need 'dnf-plugins-core' for 'dnf config-manager' +{% set base_centos_yum_repo_packages = base_centos_yum_repo_packages + [ + 'dnf-plugins-core' +] %} + +RUN {{ macros.install_packages(base_centos_yum_repo_packages | customizable("centos_yum_repo_packages"), chain=True, clean=False) }} + +{%- for key in base_centos_yum_repo_keys | customizable('centos_yum_repo_keys') %} && rpm --import {{ key }} {% endfor %} \ +{% block base_centos_repo_overrides_post_yum %}{% endblock -%} + && {{ macros.rpm_security_update(clean_package_cache) }} + +#### END REPO ENABLEMENT + +{# We are back to the basic if conditional here which is: + if base_package_type == 'rpm' #} + +{% block base_redhat_binary_versionlock %}{% endblock %} + +{# NOTE(hrw): CentOS Stream 9 has curl-minimal, Rocky Linux 9 has curl so we do not install any #} +{% set base_centos_packages = [ + 'ca-certificates', + 'crypto-policies-scripts', + 'dumb-init', + 'findutils', + 'hostname', + 'iproute', + 'iscsi-initiator-utils', + 'lsof', + 'lvm2', + 'ncurses', + 'openssl', + 'procps-ng', + 'python3', + 'python3-pip', + 'python3-pyyaml', + 'socat', + 'sudo', + 'tar', + 'util-linux', + 'util-linux-user', + 'which', + 'patch' +] %} + +# Install base packages +{{ macros.enable_extra_repos(['epel']) }} +{{ macros.install_packages(base_centos_packages | customizable("centos_packages") | customizable("centos_binary_packages") | customizable("centos_source_packages")) }} + +{# endif for base_package_type rpm #} +{% elif base_package_type == 'deb' %} +# This will prevent questions from being asked during the install +ENV DEBIAN_FRONTEND noninteractive + +# Reducing disk footprint +COPY dpkg_reducing_disk_footprint /etc/dpkg/dpkg.cfg.d/dpkg_reducing_disk_footprint + +{% block base_ubuntu_package_pre %} +# curl and ca-certificates to fetch remote keys via http +# gnupg to fetch keys directly from keyserver +{% set base_ubuntu_package_pre_packages = [ + 'adduser', + 'ca-certificates', + 'curl', + 'gnupg' +] %} +# ubuntu-cloud-keyring to install UCA packages +{% if base_distro == 'ubuntu' %} +{% set base_ubuntu_package_pre_packages = base_ubuntu_package_pre_packages + [ + 'ubuntu-cloud-keyring' +] %} +{% endif %} +{{ macros.install_packages(base_ubuntu_package_pre_packages | customizable("base_ubuntu_package_pre_packages")) }} +{% endblock %} + +# Customize PS1 bash shell +# - enlarge 'system users' range so 'haproxy' package will not complain +# see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=939470 +# - enlarge 'system groups' range so 'hacluster' user added in +# https://review.opendev.org/c/openstack/kolla/+/802671 +# can be in 'haclient' group with same high uid +RUN cat /tmp/kolla_bashrc >> /etc/bash.bashrc \ + && sed -i -e s/#*LAST_SYSTEM_UID=999/LAST_SYSTEM_UID=59999/g \ + -e s/#*LAST_SYSTEM_GID=999/LAST_SYSTEM_GID=59999/g /etc/adduser.conf + +{# NOTE(mnasiadka): Ubuntu arm64 uses ports.ubuntu.com #} +{% block base_ubuntu_package_sources_list %} +{% if base_distro == "ubuntu" and base_arch == 'aarch64' %} +COPY {{ base_distro }}.sources.arm64 /etc/apt/sources.list.d/{{ base_distro }}.sources +{% else %} +COPY {{ base_distro }}.sources /etc/apt/sources.list.d/ +{% endif %} +COPY sources.list /etc/apt/sources.list.d/kolla-custom.list +{% endblock %} + +{% block base_debian_after_sources_list %}{% endblock %} + +{# install Debian Openstack repos - they are not mirrored on CI #} +{% if base_distro == 'debian' %} + +RUN apt update \ + && apt install -y --no-install-recommends extrepo \ + && extrepo enable openstack_{{ openstack_release_codename | lower }} \ + && apt purge -y extrepo \ + && apt --purge autoremove -y \ + && apt clean + +{% endif %} + +{% block base_ubuntu_package_apt_preferences %} +COPY apt_preferences.{{ base_distro }} /etc/apt/preferences +COPY apt_preferences /etc/apt/preferences.d/kolla-custom +{% endblock %} + +{% set base_apt_packages = [ + 'apt-utils', + 'dumb-init', + 'systemd-standalone-sysusers', + 'gawk', + 'iproute2', + 'kmod', + 'lsof', + 'lvm2', + 'netbase', + 'open-iscsi', + 'procps', + 'python3', + 'python3-pip', + 'python3-yaml', + 'socat', + 'sudo', + 'tgt', + 'patch' +] %} + +{% set base_apt_keys = [ + {'name': 'erlang-ppa', 'keyid': 'F77F1EDA57EBB1CC'}, + {'name': 'rabbitmq', 'keyid': '6B73A36E6026DFCA'}, + {'name': 'haproxy', 'keyid': 'CFFB779AADC995E4F350A060505D97A41C61B9CD'}, +] %} + +{# NOTE(hrw): type field defaults to 'asc' which is used for single keys #} +{% set base_remote_apt_keys = [ + {'name': 'docker-ce', 'url': 'https://download.docker.com/linux/debian/gpg'}, + {'name': 'fluentd', 'url': 'https://fluentd.cdn.cncf.io/GPG-KEY-fluent-package'}, + {'name': 'grafana', 'url': 'https://rpm.grafana.com/gpg.key'}, + {'name': 'mariadb', 'url': 'https://downloads.mariadb.com/MariaDB/mariadb-keyring-2019.gpg', 'type': 'gpg'}, + {'name': 'opensearch', 'url': 'https://artifacts.opensearch.org/publickeys/opensearch-release.pgp'}, + {'name': 'proxysql', 'url': 'https://repo.proxysql.com/ProxySQL/proxysql-3.0.x/repo_pub_key'}, +] %} + +{% block base_ubuntu_package_installation %} + {%- block base_ubuntu_package_key_installation %} + {% for key in base_apt_keys | customizable('apt_keys') %} + {%- if loop.first %}RUN mkdir -p /etc/kolla/apt-keys/{% endif %} \ + && gpg --keyserver hkp://keyserver.ubuntu.com:80 \ + {% if env.http_proxy %} --keyserver-options "http-proxy={{ env.http_proxy }}" {% endif %}\ + --recv-keys {{ key.keyid }} \ + && gpg --export {{ key.keyid }} >/etc/kolla/apt-keys/{{ key.name }}.gpg + {%- if not loop.last %} \ + {% endif -%} + {% endfor %} + {% for key in base_remote_apt_keys | customizable('remote_apt_keys') %} + {%- if loop.first %} RUN mkdir -p /etc/kolla/apt-keys/ {% endif %} \ + && curl {{ key.url }} -o /etc/kolla/apt-keys/{{ key.name }}.{{ key.type | default('asc') }} + {%- if not loop.last %} \ + {% endif -%} + {% endfor %} + {% endblock %} +RUN apt-get --error-on=any update \ + && apt-get -y install locales \ + && sed -e "s/# $LANG UTF-8/$LANG UTF-8/g" /etc/locale.gen -i \ + && locale-gen "$LANG" \ + && apt-get -y upgrade \ + && apt-get -y dist-upgrade \ + && {{ macros.install_packages(base_apt_packages | customizable('apt_packages'), True) }} \ + # NOTE: python3-pip installs dependent tzdata package and blocks mount in docker - 2091161 + && unlink /etc/localtime + +{% endblock %} + +{# endif base_package_type deb #} +{% endif %} + +{# NOTE(SamYaple): Avoid uid/gid conflicts by creating each user/group up front. #} +{# Specifics required such as homedir or shell are configured within the service specific image #} +{%- for name, user in users | dictsort() %} +{% if loop.first -%}RUN {% else %} && {% endif -%} + groupadd --gid {{ user.gid }} {{ user.group }} \ + && useradd -l -M --shell /usr/sbin/nologin --uid {{ user.uid }} --gid {{ user.gid }} {{ name }} + {%- if not loop.last %} \{% endif -%} +{%- endfor %} + +{% if base_distro == 'centos' %} +RUN sed -ri '/-session(\s+)optional(\s+)pam_systemd.so/d' /etc/pam.d/system-auth \ + && sed -ri '/^[^#]/ s/systemd//g' /etc/nsswitch.conf +{% endif %} + +COPY set_configs.py /usr/local/bin/kolla_set_configs +COPY start.sh /usr/local/bin/kolla_start +COPY copy_cacerts.sh /usr/local/bin/kolla_copy_cacerts +COPY install_projects.sh /usr/local/bin/kolla_install_projects +COPY kolla_patch.sh /usr/local/bin/kolla_patch +COPY sudoers /etc/sudoers + +{% if use_dumb_init %} +ENTRYPOINT ["dumb-init", "--single-child", "--"] +{% endif %} + +{% if docker_healthchecks %} +{% block healthcheck_installation %} + +COPY healthcheck_curl healthcheck_filemod healthcheck_listen healthcheck_port healthcheck_socket /usr/local/bin/ +RUN chmod 755 /usr/local/bin/healthcheck_* + +{% endblock %} +{% endif %} + +RUN touch /usr/local/bin/kolla_extend_start \ + && chmod 755 /usr/local/bin/kolla_start \ + /usr/local/bin/kolla_set_configs \ + /usr/local/bin/kolla_copy_cacerts \ + /usr/local/bin/kolla_install_projects \ + /usr/local/bin/kolla_patch \ + && chmod 644 /usr/local/bin/kolla_extend_start \ + && chmod 440 /etc/sudoers \ + && mkdir -p /var/log/kolla \ + && chown :kolla /var/log/kolla \ + && chmod 2775 /var/log/kolla \ + && rm -f /tmp/kolla_bashrc + +{% block base_pip_conf %} +# the variables like PIP_INDEX_URL, PIP_EXTRA_INDEX_URL, PIP_TRUSTED_HOST etc. should be defined here. +# ENV PIP_INDEX_URL=https://pypi.python.org/simple +# ENV PIP_TRUSTED_HOST=pypi.python.org +{% endblock %} + +{{ macros.kolla_patch_sources() }} + +{% block base_footer %}{% endblock %} +CMD ["kolla_start"] diff --git a/kolla/docker/base/apt_preferences b/kolla/docker/base/apt_preferences new file mode 100644 index 000000000..dc5b993f1 --- /dev/null +++ b/kolla/docker/base/apt_preferences @@ -0,0 +1 @@ +# used for adding custom options by 'apt_preferences' option of kolla-build.conf diff --git a/kolla/docker/base/apt_preferences.debian b/kolla/docker/base/apt_preferences.debian new file mode 100644 index 000000000..08e21ff22 --- /dev/null +++ b/kolla/docker/base/apt_preferences.debian @@ -0,0 +1,4 @@ +# NOTE(hrw): we do not want backports unless requested +Package: * +Pin: release n=trixie-backports +Pin-Priority: -1000 diff --git a/kolla/docker/base/apt_preferences.ubuntu b/kolla/docker/base/apt_preferences.ubuntu new file mode 100644 index 000000000..7cc49b4ad --- /dev/null +++ b/kolla/docker/base/apt_preferences.ubuntu @@ -0,0 +1,3 @@ +Package: * +Pin: origin dlm.mariadb.com +Pin-Priority: 1000 diff --git a/kolla/docker/base/ci-centos.repo b/kolla/docker/base/ci-centos.repo new file mode 100644 index 000000000..4c08f1fea --- /dev/null +++ b/kolla/docker/base/ci-centos.repo @@ -0,0 +1,75 @@ +[baseos] +name=(OpenDev mirror) CentOS Stream $releasever - BaseOS +baseurl=http://MIRROR/centos-stream/$stream/BaseOS/$basearch/os/ +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial +gpgcheck=1 +repo_gpgcheck=0 +enabled=1 + +[appstream] +name=(OpenDev mirror) CentOS Stream $releasever - AppStream +baseurl=http://MIRROR/centos-stream/$stream/AppStream/$basearch/os/ +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial +gpgcheck=1 +repo_gpgcheck=0 +enabled=1 + +[extras-common] +name=(OpenDev mirror) CentOS Stream $releasever - Extras packages +baseurl=http://MIRROR/centos-stream/SIGs/$stream/extras/$basearch/extras-common/ +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-Extras-SHA512 +gpgcheck=1 +repo_gpgcheck=0 +enabled=1 + +[crb] +name=(OpenDev mirror) CentOS Stream $releasever - CRB +baseurl=http://MIRROR/centos-stream/$stream/CRB/$basearch/os/ +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial +gpgcheck=1 +repo_gpgcheck=0 +enabled=0 + +[highavailability] +name=(OpenDev mirror) CentOS Stream $releasever - HighAvailability +baseurl=http://MIRROR/centos-stream/$stream/HighAvailability/$basearch/os/ +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial +gpgcheck=1 +repo_gpgcheck=0 +enabled=0 + +[centos-ceph-reef] +name=(OpenDev mirror) CentOS-$stream - Ceph Reef +baseurl=http://MIRROR/centos-stream/SIGs/$stream/storage/$basearch/ceph-reef/ +gpgcheck=1 +enabled=0 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-Storage + +[centos-nfv-openvswitch] +name=(OpenDev mirror) CentOS Stream $releasever - NFV OpenvSwitch +baseurl=http://MIRROR/centos-stream/SIGs/$stream/nfv/$basearch/openvswitch-2/ +gpgcheck=1 +enabled=0 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-NFV +module_hotfixes=1 + +[centos-opstools] +name=(OpenDev mirror) CentOS Stream $releasever - OpsTools - collectd +baseurl=http://MIRROR/centos-stream/SIGs/$stream/opstools/$basearch/collectd-5/ +gpgcheck=1 +enabled=0 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-OpsTools + +[centos-rabbitmq-38] +name=(OpenDev mirror) CentOS-9 - RabbitMQ 38 +baseurl=http://MIRROR/centos-stream/SIGs/$stream/messaging/$basearch/rabbitmq-38 +gpgcheck=1 +enabled=0 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-SIG-Messaging + +[epel] +name=(OpenDev mirror) Extra Packages for Enterprise Linux $releasever - $basearch +baseurl=http://MIRROR/epel/$releasever/Everything/$basearch/ +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-$releasever diff --git a/kolla/docker/base/copy_cacerts.sh b/kolla/docker/base/copy_cacerts.sh new file mode 100644 index 000000000..213033c0c --- /dev/null +++ b/kolla/docker/base/copy_cacerts.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Copy custom CA certificates to system trusted CA certificates folder +# and run CA update utility + +if [[ -e "/etc/debian_version" ]]; then + ca_dst_path="/usr/local/share/ca-certificates" + update_command="update-ca-certificates" +elif [[ -e "/etc/redhat-release" ]]; then + ca_dst_path="/etc/pki/ca-trust/source/anchors" + update_command="update-ca-trust" +else + echo "Unsupported OS" + exit 1 +fi + +# Initialize update_needed variable +update_needed="false" + +# Remove old certificates +if find /etc/ssl/certs/ \ + /usr/local/share/ca-certificates/ \ + /etc/pki/ca-trust/source/anchors/ \ + -name 'kolla*' -exec rm -f {} + 2>/dev/null; then + update_needed="true" +fi + +# Determine source path for CA certificates +if grep -q '"source": "/var/lib/kolla/share/ca-certificates"' /etc/kolla/defaults/state; then + ca_src_path="/var/lib/kolla/share/ca-certificates" +else + ca_src_path="/var/lib/kolla/config_files/ca-certificates" +fi + +# Check if the source path exists and is not empty +if [[ -d ${ca_src_path} && $(ls -A "${ca_src_path}" 2>/dev/null) ]]; then + # Copy certificates and update CA + for cert in "${ca_src_path}"/*; do + file=$(basename "${cert}") + cp ${cert} ${ca_dst_path}/kolla-customca-${file} + update_needed="true" + done +fi + +# Run the update command if needed +if [[ "${update_needed}" == "true" ]]; then + ${update_command} +fi diff --git a/kolla/docker/base/curlrc b/kolla/docker/base/curlrc new file mode 100644 index 000000000..3b1ecea06 --- /dev/null +++ b/kolla/docker/base/curlrc @@ -0,0 +1,8 @@ +# curl default options +--fail +--location +--retry 5 +--retry-all-errors +--silent +--show-error +--write-out "curl (%{url_effective}): response: %{http_code}, time: %{time_total}, size: %{size_download}\n" diff --git a/kolla/docker/base/debian.sources b/kolla/docker/base/debian.sources new file mode 100644 index 000000000..28c61c81d --- /dev/null +++ b/kolla/docker/base/debian.sources @@ -0,0 +1,13 @@ +Types: deb +# http://snapshot.debian.org/archive/debian/20251020T000000Z +URIs: http://deb.debian.org/debian +Suites: trixie trixie-updates trixie-backports +Components: main +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg + +Types: deb +# http://snapshot.debian.org/archive/debian-security/20251020T000000Z +URIs: http://deb.debian.org/debian-security +Suites: trixie-security +Components: main +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg diff --git a/kolla/docker/base/dnf.conf b/kolla/docker/base/dnf.conf new file mode 100644 index 000000000..6ca687545 --- /dev/null +++ b/kolla/docker/base/dnf.conf @@ -0,0 +1,12 @@ +[main] +keepcache=0 +logfile=/var/log/dnf.log +exactarch=1 +obsoletes=1 +gpgcheck=1 +plugins=1 +installonly_limit=0 +skip_missing_names_on_install=False +clean_requirements_on_remove=True +tsflags=nodocs +install_weak_deps=False diff --git a/kolla/docker/base/dpkg_reducing_disk_footprint b/kolla/docker/base/dpkg_reducing_disk_footprint new file mode 100644 index 000000000..87b8255f9 --- /dev/null +++ b/kolla/docker/base/dpkg_reducing_disk_footprint @@ -0,0 +1,9 @@ +path-exclude /usr/share/doc/* +path-include /usr/share/doc/*/copyright +path-exclude /usr/share/man/* +path-exclude /usr/share/groff/* +path-exclude /usr/share/info/* +path-exclude /usr/share/lintian/* +path-exclude /usr/share/linda/* +path-exclude /usr/share/locale/* +path-include /usr/share/locale/en* diff --git a/kolla/docker/base/healthcheck_curl b/kolla/docker/base/healthcheck_curl new file mode 100755 index 000000000..6df232fe8 --- /dev/null +++ b/kolla/docker/base/healthcheck_curl @@ -0,0 +1,12 @@ +#!/bin/bash +: ${HEALTHCHECK_CURL_MAX_TIME:=10} +: ${HEALTHCHECK_CURL_USER_AGENT:=curl-healthcheck} +: ${HEALTHCHECK_CURL_WRITE_OUT:='\n%{http_code} %{remote_ip}:%{remote_port} %{time_total} seconds\n'} +: ${HEALTHCHECK_CURL_OUTPUT:='/dev/null'} + +export NSS_SDB_USE_CACHE=no +curl -q -g -k -s -S --fail -o "${HEALTHCHECK_CURL_OUTPUT}" \ + --max-time "${HEALTHCHECK_CURL_MAX_TIME}" \ + --user-agent "${HEALTHCHECK_CURL_USER_AGENT}" \ + --write-out "${HEALTHCHECK_CURL_WRITE_OUT}" \ + "$@" || exit 1 diff --git a/kolla/docker/base/healthcheck_filemod b/kolla/docker/base/healthcheck_filemod new file mode 100755 index 000000000..007643d9b --- /dev/null +++ b/kolla/docker/base/healthcheck_filemod @@ -0,0 +1,15 @@ +#!/bin/bash +file_path=$1 +limit_seconds=$2 + +# if the file doesn't exist, return 1 +if [ ! -f $file_path ]; then + echo "${file_path} does not exist for file modification check" + exit 1 +fi +curr_time=$(date +%s) +last_mod=$(stat -c '%Y' $file_path) +limit_epoch=$(( curr_time-limit_seconds )) +if [ ${limit_epoch} -gt ${last_mod} ]; then + exit 1 +fi diff --git a/kolla/docker/base/healthcheck_listen b/kolla/docker/base/healthcheck_listen new file mode 100755 index 000000000..70273339c --- /dev/null +++ b/kolla/docker/base/healthcheck_listen @@ -0,0 +1,8 @@ +#!/bin/bash +process=$1 + +shift 1 +args=$@ +ports=${args// /|} +pids=$(pgrep -d '|' -f $process) +ss -lnp | grep -qE ":($ports).*,pid=($pids)," diff --git a/kolla/docker/base/healthcheck_port b/kolla/docker/base/healthcheck_port new file mode 100755 index 000000000..9531bd77a --- /dev/null +++ b/kolla/docker/base/healthcheck_port @@ -0,0 +1,8 @@ +#!/bin/bash +process=$1 + +shift 1 +args=$@ +ports=${args// /|} +pids=$(pgrep -d '|' -f $process) +ss -ntp | grep -qE ":($ports).*,pid=($pids)," diff --git a/kolla/docker/base/healthcheck_socket b/kolla/docker/base/healthcheck_socket new file mode 100755 index 000000000..cd65bae69 --- /dev/null +++ b/kolla/docker/base/healthcheck_socket @@ -0,0 +1,10 @@ +#!/bin/bash +process=$1 +socket=$2 + +# lsof truncate command name to 15 characters and this behaviour +# cannot be disabled +if [ ${#process} -gt 15 ] ; then + process=${process:0:15} +fi +lsof -Fc -Ua $socket | grep -q "c$process" diff --git a/kolla/docker/base/install_projects.sh b/kolla/docker/base/install_projects.sh new file mode 100644 index 000000000..c3f7182b5 --- /dev/null +++ b/kolla/docker/base/install_projects.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# This script finds all projects in /dev-mode folder and +# installs them using pip +# The projects are mounted there when dev mode is enabled for them +# in kolla-ansible + +VENV_PATH="/var/lib/kolla/venv" + +if [ -d "/dev-mode" ] && [ -d "$VENV_PATH" ]; then + for project in /dev-mode/*; do + pip install -U "$project" + done +fi diff --git a/kolla/docker/base/kolla_bashrc b/kolla/docker/base/kolla_bashrc new file mode 100644 index 000000000..09b19cc06 --- /dev/null +++ b/kolla/docker/base/kolla_bashrc @@ -0,0 +1,5 @@ +#### Custom data added by Kolla +_KBOLD="\[$(tput bold)\]" +_KRESET="\[$(tput sgr0)\]" + +PS1="${_KBOLD}(${KOLLA_SERVICE_NAME})${_KRESET}[\u@\h \W]\\$ " diff --git a/kolla/docker/base/kolla_patch.sh b/kolla/docker/base/kolla_patch.sh new file mode 100755 index 000000000..b17a5b61d --- /dev/null +++ b/kolla/docker/base/kolla_patch.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# This script works as debian quilt patch. +# So, patch files included in /patches/series +# are applied and information what was applied +# is stored in /etc/kolla/patched. +# +# No more, no less :) + +cd / + +# If exist /patches/series +# let's try to apply patches +if [ -e "/patches/series" ]; then + # If there is /patches/series.applied + # then it means previous run already applied + # some patches - from another intermediate container + # + # So, let's add patches again to /patches/series + # and let's script to handle it + if [ -e "/patches/series.applied" ]; then + grep -v '^#' /patches/series.applied > /tmp/series.tmp + grep -v '^#' /patches/series >> /tmp/series.tmp + rm -f /patches/series + mv /tmp/series.tmp /patches/series + fi + touch /etc/kolla/patched + for patchfile in $(grep -v '^#' /patches/series); do + # If patch is not applied, try to apply it, otherwise + # inform user that patchfile is already applied + if ! grep -q "$patchfile" /etc/kolla/patched; then + echo "[i] Applying /patches/${patchfile}" + patch -p0 --fuzz=0 --ignore-whitespace < /patches/${i}/${patchfile} + # If apply patch was successful inform user, + # otherwise fail build process and inform user + # to check/fix patch + if [ $? -eq 0 ]; then + echo "[i] Applied /patches/${patchfile}" >> /etc/kolla/patched + else + echo "[i] Patch /patches/${patchfile} failed, please fix your patchfiles." + exit 1 + fi + else + echo "[i] /patches/${patchfile} already applied." + fi + done + # Ignore files which are commented and move + # to /patches/series.applied as /patch/series + # can be potentially replaced by another files + # from different intermediate container + grep -v '^#' /patches/series > /patches/series.applied + rm -f /patches/series +else + echo "[i] No series file found, nothing to patch." +fi diff --git a/kolla/docker/base/set_configs.py b/kolla/docker/base/set_configs.py new file mode 100644 index 000000000..4b025d55a --- /dev/null +++ b/kolla/docker/base/set_configs.py @@ -0,0 +1,759 @@ +#!/usr/bin/env python3 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import glob +import grp +import json +import logging +import os +import pwd +import re +import shutil +import stat +import sys +import yaml + + +# TODO(rhallisey): add docstring. +logging.basicConfig( + format='%(asctime)s.%(msecs)03d %(levelname)s %(message)s', + level=logging.INFO, + datefmt='%Y-%m-%d %H:%M:%S' +) +LOG = logging.getLogger(__name__) +LOG.setLevel(logging.INFO) + +KOLLA_DEFAULTS = "/etc/kolla/defaults" +KOLLA_DEFAULTS_STATE = KOLLA_DEFAULTS + '/' + 'state' + + +class ExitingException(Exception): + def __init__(self, message, exit_code=1): + super(ExitingException, self).__init__(message) + self.exit_code = exit_code + + +class ImmutableConfig(ExitingException): + pass + + +class InvalidConfig(ExitingException): + pass + + +class MissingRequiredSource(ExitingException): + pass + + +class UserNotFound(ExitingException): + pass + + +class ConfigFileBadState(ExitingException): + pass + + +class ConfigFileCommandDiffers(ExitingException): + pass + + +class StateMismatch(ExitingException): + pass + + +class ConfigFile(object): + + def __init__(self, source, dest, owner=None, perm=None, optional=False, + preserve_properties=False, merge=False): + self.source = source + self.dest = dest + self.owner = owner + self.perm = perm + self.optional = optional + self.merge = merge + self.preserve_properties = preserve_properties + + def __str__(self): + return '<ConfigFile source:"{}" dest:"{}">'.format(self.source, + self.dest) + + def _copy_file(self, source, dest): + self._delete_path(dest) + # dest endswith / means copy the <source> to <dest> folder + LOG.info('Copying %s to %s', source, dest) + if self.merge and self.preserve_properties and os.path.islink(source): + link_target = os.readlink(source) + os.symlink(link_target, dest) + else: + shutil.copy(source, dest) + self._set_properties(source, dest) + + def _merge_directories(self, source, dest): + if os.path.isdir(source): + if os.path.lexists(dest) and not os.path.isdir(dest): + self._delete_path(dest) + if not os.path.isdir(dest): + LOG.info('Creating directory %s', dest) + os.makedirs(dest) + self._set_properties(source, dest) + + dir_content = os.listdir(source) + for to_copy in dir_content: + self._merge_directories(os.path.join(source, to_copy), + os.path.join(dest, to_copy)) + else: + self._copy_file(source, dest) + + def _delete_path(self, path): + if not os.path.lexists(path): + return + LOG.info('Deleting %s', path) + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.remove(path) + + def _create_parent_dirs(self, path): + parent_path = os.path.dirname(path) + if not os.path.exists(parent_path): + os.makedirs(parent_path) + + def _set_properties(self, source, dest): + if self.preserve_properties: + self._set_properties_from_file(source, dest) + else: + self._set_properties_from_conf(dest) + + def _set_properties_from_file(self, source, dest): + LOG.info('Copying permissions from %s onto %s', source, dest) + shutil.copystat(source, dest) + stat = os.stat(source) + os.chown(dest, stat.st_uid, stat.st_gid) + + def _set_properties_from_conf(self, path): + config = {'permissions': + [{'owner': self.owner, 'path': path, 'perm': self.perm}]} + handle_permissions(config) + + def copy(self): + + sources = glob.glob(self.source) + + if not self.optional and not sources: + raise MissingRequiredSource('%s file is not found' % self.source) + # skip when there is no sources and optional + elif self.optional and not sources: + return + + for source in sources: + dest = self.dest + # dest endswith / means copy the <source> into <dest> folder, + # otherwise means copy the source to dest + if dest.endswith(os.sep): + dest = os.path.join(dest, os.path.basename(source)) + if not self.merge: + self._delete_path(dest) + self._create_parent_dirs(dest) + try: + self._merge_directories(source, dest) + except OSError: + # If a source is tried to merge with a read-only mount, it + # may throw an OSError. Because we don't print the source or + # dest anywhere, let's catch the exception and log a better + # message to help with tracking down the issue. + LOG.error('Unable to merge %s with %s', source, dest) + raise + + def _cmp_file(self, source, dest): + # check exist + if (os.path.exists(source) and + not os.path.exists(dest)): + return False + if (os.path.exists(dest) and + not os.path.exists(source)): + return False + # check content + with open(source, 'rb') as f1, open(dest, 'rb') as f2: + if f1.read() != f2.read(): + LOG.error('The content of source file(%s) and' + ' dest file(%s) are not equal.', source, dest) + return False + # check perm + file_stat = os.stat(dest) + actual_perm = oct(file_stat.st_mode)[-4:] + if self.perm != actual_perm: + LOG.error('Dest file does not have expected perm: %s, actual: %s', + self.perm, actual_perm) + return False + # check owner + desired_user, desired_group = user_group(self.owner) + actual_user = pwd.getpwuid(file_stat.st_uid) + if actual_user.pw_name != desired_user: + LOG.error('Dest file does not have expected user: %s,' + ' actual: %s ', desired_user, actual_user.pw_name) + return False + actual_group = grp.getgrgid(file_stat.st_gid) + if actual_group.gr_name != desired_group: + LOG.error('Dest file does not have expected group: %s,' + ' actual: %s ', desired_group, actual_group.gr_name) + return False + return True + + def _cmp_dir(self, source, dest): + for root, dirs, files in os.walk(source): + for dir_ in dirs: + full_path = os.path.join(root, dir_) + dest_full_path = os.path.join(dest, os.path.relpath(source, + full_path)) + dir_stat = os.stat(dest_full_path) + actual_perm = oct(dir_stat.st_mode)[-4:] + if self.perm != actual_perm: + LOG.error('Dest dir does not have expected perm: %s,' + ' actual %s', self.perm, actual_perm) + return False + for file_ in files: + full_path = os.path.join(root, file_) + dest_full_path = os.path.join(dest, os.path.relpath(full_path, + source)) + if not self._cmp_file(full_path, dest_full_path): + return False + return True + + def check(self): + bad_state_files = [] + sources = glob.glob(self.source) + + if not sources and not self.optional: + raise MissingRequiredSource('%s file is not found' % self.source) + elif self.optional and not sources: + return + + for source in sources: + dest = self.dest + # dest endswith / means copy the <source> into <dest> folder, + # otherwise means copy the source to dest + if dest.endswith(os.sep): + dest = os.path.join(dest, os.path.basename(source)) + if os.path.isdir(source): + if not self._cmp_dir(source, dest): + bad_state_files.append(source) + elif not self._cmp_file(source, dest): + bad_state_files.append(source) + if len(bad_state_files) != 0: + msg = 'Following files are in bad state: %s' % bad_state_files + raise ConfigFileBadState(msg) + + +def validate_config(config): + required_keys = {'source', 'dest'} + required_path_keys = {'path', 'owner', 'perm'} + + if 'command' not in config: + raise InvalidConfig('Config is missing required "command" key') + + # Validate config sections + for data in config.get('config_files', list()): + # Verify required keys exist. + if not set(data.keys()) >= required_keys: + message = 'Config is missing required keys: %s' % required_keys + raise InvalidConfig(message) + if ('owner' not in data or 'perm' not in data) \ + and not data.get('preserve_properties', False): + raise InvalidConfig( + 'Config needs preserve_properties or owner and perm') + + for section in ('directories', 'files'): + for data in config.get(section, list()): + missing = required_path_keys - set(data.keys()) + if missing: + raise InvalidConfig( + '%s config is missing required keys: %s' + % (section, missing)) + + for key in required_path_keys: + if data.get(key) is None: + raise InvalidConfig( + '%s config key "%s" must not be null' + % (section, key)) + + if glob.has_magic(data['path']): + raise InvalidConfig( + '%s config does not support glob paths: %s' + % (section, data['path'])) + + +def validate_source(data): + source = data.get('source') + + # Only check existence if no wildcard found + if '*' not in source: + if not os.path.exists(source): + if data.get('optional'): + LOG.info("%s does not exist, but is not required", source) + return False + else: + raise MissingRequiredSource( + "The source to copy does not exist: %s" % source) + + return True + + +def load_config(): + def load_from_file(): + yaml_config_file = '/var/lib/kolla/config_files/config.yaml' + json_config_file = '/var/lib/kolla/config_files/config.json' + + if os.path.exists(yaml_config_file): + config_file = yaml_config_file + LOG.info("Loading config file at %s", config_file) + try: + with open(config_file) as f: + config = yaml.safe_load(f) + + if config is None: + raise InvalidConfig( + "Invalid yaml file found at %s: " + "file is empty" % config_file) + return config + except yaml.YAMLError: + raise InvalidConfig( + "Invalid yaml file found at %s" % config_file) + except IOError as e: + raise InvalidConfig( + "Could not read file %s: %r" % (config_file, e)) + else: + config_file = json_config_file + LOG.info("Loading config file at %s", config_file) + try: + with open(config_file) as f: + return json.load(f) + except ValueError: + raise InvalidConfig( + "Invalid json file found at %s" % config_file) + except IOError as e: + raise InvalidConfig( + "Could not read file %s: %r" % (config_file, e)) + + config = load_from_file() + + LOG.info('Validating config file') + validate_config(config) + return config + + +def copy_config(config): + if 'config_files' in config: + LOG.info('Copying service configuration files') + for data in config['config_files']: + config_file = ConfigFile(**data) + config_file.copy() + else: + LOG.debug('No files to copy found in config') + + LOG.info('Writing out command to execute') + LOG.debug("Command is: %s", config['command']) + # The value from the 'command' key will be written to '/run_command' + cmd = '/run_command' + with open(cmd, 'w+') as f: + f.write(config['command']) + # Make sure the generated file is readable by all users + try: + os.chmod(cmd, 0o644) + except OSError: + LOG.exception('Failed to set permission of %s to 0o644', cmd) + + +def create_directories(config): + if 'directories' not in config: + return + LOG.info('Creating directories') + for data in config['directories']: + path = data['path'] + try: + os.makedirs(path, exist_ok=True) + except Exception: + raise OSError("Can't create destination directory (%s)" % (path)) + handle_permissions({'permissions': [data]}) + + +def create_files(config): + if 'files' not in config: + return + LOG.info('Creating files') + for data in config['files']: + path = data['path'] + if os.path.islink(path): + raise OSError( + "Path is a symbolic link, expected regular file (%s)" % path) + if os.path.isdir(path): + raise OSError( + "Path is a directory, expected file (%s)" % path) + parent = os.path.dirname(path) + if parent and not os.path.isdir(parent): + raise OSError( + "Parent directory does not exist (%s)" % parent) + if not os.path.exists(path): + with open(path, 'a'): + pass + handle_permissions({'permissions': [data]}) + + +def user_group(owner): + if ':' in owner: + user, group = owner.split(':', 1) + if not group: + group = user + else: + user, group = owner, owner + return user, group + + +def handle_permissions(config): + for permission in config.get('permissions', list()): + path = permission.get('path') + owner = permission.get('owner') + recurse = permission.get('recurse', False) + perm = permission.get('perm') + exclude = permission.get('exclude', []) + + desired_user, desired_group = user_group(owner) + uid = pwd.getpwnam(desired_user).pw_uid + gid = grp.getgrnam(desired_group).gr_gid + + def set_perms(path, uid, gid, perm): + LOG.info('Setting permission for %s', path) + if not os.path.exists(path): + LOG.warning('%s does not exist', path) + return + + try: + os.chown(path, uid, gid) + except OSError: + LOG.exception('Failed to change ownership of %s to %s:%s', + path, uid, gid) + + if perm: + # Accept valid UNIX permission notation + # (e.g. 644, 0600, 1777, 2755) + # or equivalent Python octal literals + # (e.g. 0o644, 0o1777, 0o2755). + if re.fullmatch(r'[0-7]{3,4}', perm): + perm = int(perm, 8) + elif re.fullmatch(r'0o[0-7]{3,4}', perm): + perm = int(perm, 0) + else: + raise ValueError( + f"Invalid permission '{perm}'. Expected a valid UNIX " + "permission (e.g. 644, 0600, 2755) or a Python octal " + "literal (e.g. 0o644, 0o0600, 0o2755)." + ) + + # Ensure execute bit on directory if read bit is set + if os.path.isdir(path): + if perm & stat.S_IRUSR: + perm |= stat.S_IXUSR + if perm & stat.S_IRGRP: + perm |= stat.S_IXGRP + if perm & stat.S_IROTH: + perm |= stat.S_IXOTH + + try: + os.chmod(path, perm) + except OSError: + LOG.exception('Failed to set permission of %s to %s', + path, perm) + + def handle_exclusion(root, path_suffix): + full_path = os.path.join(root, path_suffix) + LOG.debug("Checking for exclusion: %s" % full_path) + if exclude: + for exclude_ in exclude: + if not re.search(exclude_, full_path): + set_perms(full_path, uid, gid, perm) + else: + set_perms(full_path, uid, gid, perm) + + for dest in glob.glob(path): + set_perms(dest, uid, gid, perm) + if recurse and os.path.isdir(dest): + for root, dirs, files in os.walk(dest): + for dir_ in dirs: + handle_exclusion(root, dir_) + for file_ in files: + handle_exclusion(root, file_) + + +def get_defaults_state(): + """Retrieve the saved default configuration state from default state file. + + This function creates the directory for Kolla defaults if it does not + exist, and then attempts to read the current configuration state from + a JSON file. If the file exists, it reads and returns the content. + If not, it returns an empty dictionary. + + Simply said, when the container starts for the first time, the state file + doesn't exist, and it returns an empty dictionary. + However, if it has already been started before, it will contain the state + as it was when it first ran. + + Returns: + dict: The configuration state stored in the Kolla defaults state file. + + Example: + { + "/etc/cinder/cinder.conf": { + "source": "/etc/cinder/cinder.conf", + "preserve_properties": true, + "dest": null + }, + "/etc/apache2/conf-enabled/cinder-wsgi.conf": { + "source": "/etc/apache2/conf-enabled/cinder-wsgi.conf", + "preserve_properties": true, + "dest": null + }, + "/etc/cinder/cinder_audit_map.conf": { + "source": "/etc/cinder/cinder_audit_map.conf", + "preserve_properties": true, + "dest": "/etc/kolla/defaults/etc/cinder/cinder_audit_map.conf" + } + } + + From above example: + /etc/cinder/cinder.conf didn't exist + /etc/apache2/conf-enabled/cinder-wsgi.conf didn't exist + /etc/cinder/cinder_audit_map.conf exists and saved + """ + os.makedirs(KOLLA_DEFAULTS, exist_ok=True) + if os.path.exists(KOLLA_DEFAULTS_STATE): + with open(KOLLA_DEFAULTS_STATE, 'r') as f: + return json.load(f) + else: + return {} + + +def set_defaults_state(state): + """Save the provided configuration state to the defaults state file. + + This function writes the provided state (a dictionary) to a JSON file at + the specified Kolla defaults state location, ensuring that it is properly + formatted with indentation for readability. + + Args: + state (dict): The configuration state to save to the Kolla defaults + state file. + """ + with open(KOLLA_DEFAULTS_STATE, 'w') as f: + json.dump(state, f, indent=4) + + +def remove_or_restore_configs(state): + """Remove or restore configuration files based on their current state. + + This function iterates over the configuration files in the provided state. + If the destination is `None`, it removes the file or directory. Otherwise, + it swaps the source and destination, restoring the configuration file + by copying it back to its original location. + + Args: + state (dict): The current default state of configuration files, mapping + file paths to their source and destination information. + """ + for k, v in state.items(): + if v['dest'] is None: + if os.path.exists(k): + if os.path.isfile(k): + os.remove(k) + else: + shutil.rmtree(k) + else: + v['source'], v['dest'] = v['dest'], v['source'] + config_file = ConfigFile(**v) + config_file.copy() + + +def backup_configs(config, state): + """Back up new configuration files and update the default state. + + This function processes new configuration files provided in the + input `config`. For each file, it checks if the destination exists in the + current state. If not, it backs up the file by copying it to the default + directory. It then updates the state with the new configuration file's + information. + + Args: + config (dict): The input configuration containing a list of config + files. + state (dict): The current default state to be updated with the new + config files. + """ + if 'config_files' in config: + for data in config['config_files']: + if data['dest'] in state.keys(): + continue + src = data['source'] + if data['dest'].endswith('/'): + dst = data['dest'] + data['source'].split('/')[-1] + else: + dst = data['dest'] + default = KOLLA_DEFAULTS + dst + if os.path.exists(src): + copy = {'source': dst, 'preserve_properties': True} + if os.path.exists(dst): + copy['dest'] = default + if dst not in state: + config_file = ConfigFile(**copy) + config_file.copy() + state[dst] = copy + else: + copy['dest'] = None + if dst not in state: + state[dst] = copy + + +def handle_defaults(config): + """Handle the default config files by copying/removing them as needed. + + This function loads the current default state and manages the configuration + files. It first processes existing configuration files in the default + state, either removing or restoring them based on their destination status. + It then backs up any new configuration files from the input config, + updating the default state accordingly. + + Args: + config (dict): A dictionary containing the list of configuration files + to be handled. + """ + state = get_defaults_state() + remove_or_restore_configs(state) + backup_configs(config, state) + set_defaults_state(state) + + +def execute_config_strategy(config): + config_strategy = os.environ.get("KOLLA_CONFIG_STRATEGY") + LOG.info("Kolla config strategy set to: %s", config_strategy) + if config_strategy == "COPY_ALWAYS": + create_directories(config) + create_files(config) + handle_defaults(config) + copy_config(config) + handle_permissions(config) + elif config_strategy == "COPY_ONCE": + if os.path.exists('/configured'): + raise ImmutableConfig( + "The config strategy prevents copying new configs", + exit_code=0) + else: + create_directories(config) + create_files(config) + handle_defaults(config) + copy_config(config) + handle_permissions(config) + os.mknod('/configured') + else: + raise InvalidConfig('KOLLA_CONFIG_STRATEGY is not set properly') + + +def execute_command_check(config): + cmd = config.get('command') + with open("/run_command", "r") as f: + cmd_running = f.read() + if cmd != cmd_running: + msg = "Running command differs. " + cmd + " != " + cmd_running + raise ConfigFileCommandDiffers(msg) + + +def execute_config_check(config): + """Check configuration state consistency and validate config file entries. + + This function compares the current config file destinations from the + provided config dictionary with those stored in the defaults state file. + If any destinations are found in the state file but not in the config, + a StateMismatch exception is raised. These missing files would otherwise + be restored or removed depending on their backup state. + + After validating consistency, the function performs standard checks on + each declared configuration file, including content, permissions, and + ownership validation. + + Args: + config (dict): The configuration dictionary containing 'config_files' + entries as expected by Kolla. + + Raises: + StateMismatch: If there are entries in the defaults state not present + in the provided config. + """ + state = get_defaults_state() + + # Build a set of all current destination paths from the config + # If the destination is a directory, we append the + # basename of the source + current_dests = { + entry['dest'] if not entry['dest'].endswith('/') else + os.path.join(entry['dest'], os.path.basename(entry['source'])) + for entry in config.get('config_files', []) + if entry.get('dest') + } + + # Detect any paths that are present in the state file but + # missing from the config file. + # These would be either restored (if state[dest] has a backup) + # or removed (if dest is null) + removed_dests = [ + path for path in state.keys() + if path not in current_dests + ] + + if removed_dests: + raise StateMismatch( + f"The following config files are tracked in state but missing " + f"from the config file. " + f"They would be restored or removed: {sorted(removed_dests)}" + ) + + # Perform the regular content, permissions, and ownership + # checks on the declared files + for data in config.get('config_files', []): + config_file = ConfigFile(**data) + config_file.check() + + +def main(): + try: + parser = argparse.ArgumentParser() + parser.add_argument('--check', + action='store_true', + required=False, + help='Check whether the configs changed') + args = parser.parse_args() + config = load_config() + + if args.check: + execute_command_check(config) + execute_config_check(config) + else: + execute_config_strategy(config) + except ExitingException as e: + LOG.error("%s: %s", e.__class__.__name__, e) + return e.exit_code + except Exception: + LOG.exception('Unexpected error:') + return 2 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/kolla/docker/base/sources.list b/kolla/docker/base/sources.list new file mode 100644 index 000000000..1acbbcb59 --- /dev/null +++ b/kolla/docker/base/sources.list @@ -0,0 +1 @@ +# used for adding custom repos by 'apt_sources_list' option of kolla-build.conf diff --git a/kolla/docker/base/start.sh b/kolla/docker/base/start.sh new file mode 100644 index 000000000..7ac505aea --- /dev/null +++ b/kolla/docker/base/start.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset +set -o xtrace + +# Processing /var/lib/kolla/config_files/config.json as root. This is necessary +# to permit certain files to be controlled by the root user which should +# not be writable by the dropped-privileged user, especially /run_command +sudo -E kolla_set_configs +CMD=$(cat /run_command) +ARGS="" + +# Install/remove custom CA certificates +sudo kolla_copy_cacerts + +# Install projects that are in /dev-mode +sudo kolla_install_projects + +if [[ ! "${!KOLLA_SKIP_EXTEND_START[@]}" ]]; then + # Run additional commands if present + . kolla_extend_start +fi + +echo "Running command: '${CMD}${ARGS:+ $ARGS}'" +exec ${CMD} ${ARGS} diff --git a/kolla/docker/base/sudoers b/kolla/docker/base/sudoers new file mode 100644 index 000000000..eb31bc98b --- /dev/null +++ b/kolla/docker/base/sudoers @@ -0,0 +1,26 @@ +# The idea here is a container service adds their UID to the kolla group +# via usermod -a -G kolla <uid>. Then the kolla_start may run +# kolla_set_configs via sudo as the root user which is necessary to protect +# the immutability of the container + +# anyone in the kolla group may sudo -E (set the environment) +Defaults: %kolla setenv + +Defaults secure_path="/var/lib/kolla/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +# root may run any commands via sudo as the network seervice user. This is +# neededfor database migrations of existing services which have not been +# converted to run as a non-root user, but instead do that via sudo -E glance +root ALL=(ALL) ALL + +# anyone in the kolla group may run /usr/local/bin/kolla_set_configs as the +# root user via sudo without password confirmation +%kolla ALL=(root) NOPASSWD: /usr/local/bin/kolla_set_configs + +# Copy custom CA certificates to containers +%kolla ALL=(root) NOPASSWD: /usr/local/bin/kolla_copy_cacerts + +# Install projects in dev-mode +%kolla ALL=(root) NOPASSWD: /usr/local/bin/kolla_install_projects + +#includedir /etc/sudoers.d diff --git a/kolla/docker/base/ubuntu.sources b/kolla/docker/base/ubuntu.sources new file mode 100644 index 000000000..f8f609cee --- /dev/null +++ b/kolla/docker/base/ubuntu.sources @@ -0,0 +1,24 @@ +Types: deb +URIs: http://archive.ubuntu.com/ubuntu/ +Suites: noble noble-updates noble-backports +Components: main universe +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg + +## Ubuntu security updates. Aside from URIs and Suites, +## this should mirror your choices in the previous section. +Types: deb +URIs: http://security.ubuntu.com/ubuntu/ +Suites: noble-security +Components: main universe +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg + +## Ubuntu Cloud Archive +Types: deb +URIs: http://ubuntu-cloud.archive.canonical.com/ubuntu +Suites: noble-updates/gazpacho +Components: main +Signed-By: /usr/share/keyrings/ubuntu-cloud-keyring.gpg + +# NOTE(hrw): extra repositories are added into image when they are needed as +# separate files in /etc/apt/sources.list.d/ directory. For that purpose they +# are defined in kolla/template/repos.yaml file. diff --git a/kolla/docker/base/ubuntu.sources.arm64 b/kolla/docker/base/ubuntu.sources.arm64 new file mode 100644 index 000000000..751af7ccf --- /dev/null +++ b/kolla/docker/base/ubuntu.sources.arm64 @@ -0,0 +1,24 @@ +Types: deb +URIs: http://ports.ubuntu.com/ubuntu-ports/ +Suites: noble noble-updates noble-backports +Components: main universe +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg + +## Ubuntu security updates. Aside from URIs and Suites, +## this should mirror your choices in the previous section. +Types: deb +URIs: http://ports.ubuntu.com/ubuntu-ports/ +Suites: noble-security +Components: main universe +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg + +## Ubuntu Cloud Archive +Types: deb +URIs: http://ubuntu-cloud.archive.canonical.com/ubuntu +Suites: noble-updates/gazpacho +Components: main +Signed-By: /usr/share/keyrings/ubuntu-cloud-keyring.gpg + +# NOTE(hrw): extra repositories are added into image when they are needed as +# separate files in /etc/apt/sources.list.d/ directory. For that purpose they +# are defined in kolla/template/repos.yaml file. |
